本文部分转载于:
http://blog.csdn.net/wqx521/article/details/53783029
http://blog.csdn.net/orz415678659/article/details/8958415
epoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linuxkernel 2.5.44),它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
目前epell是linux大规模并发网络程序中的热门首选模型。
epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
epoll有2种工作方式:LT和ET:
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET (edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。
ET和LT的区别就在这里体现,LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。而ET则只在事件发生之时通知。可以简单理解为LT是水平触发,而ET则为边缘触发。LT模式只要有事件未处理就会触发,而ET则只在高低电平变换时(即状态从1到0或者0到1)触发。
相关API:
1、创建一个epoll句柄,参数size用来告诉内核监听的文件描述符个数
#include
int epoll_create(int size)
size:告诉内核监听的数目
2、控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epfd:为epoll_creat的句柄
op: 表示动作,用3个宏来表示:
EPOLL_CTL_ADD (注册新的fd到epfd),
EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
EPOLL_CTL_DEL (从epfd删除一个fd);
event:告诉内核需要监听的事件
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
其中上面的结构体中的events可选参数是:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(LevelTriggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
其中上面结构体中的epoll_data_t结构体为:
typedef union epoll_data{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
一般会把注册的文件描述符放进去,这样当事件被激活时,返回epoll_event结构体,这样在epoll_event的data中就可以知道是哪一个被激活了。
3、等待所监控文件描述符上有事件的产生,类似于select()调用。
#include
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
events:用来从内核得到事件的集合,
maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
timeout:是超时时间
-1:阻塞
0:立即返回,非阻塞
>0:指定微秒
返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
查看一个进程打开大数目的socket描述符:
cat /proc/sys/fs/file-max
设置最大打开文件描述符限制
sudo vi /etc/security/limits.conf
写入以下配置,soft软限制,hard硬限制
* soft nofile 65536
* hard nofile 100000
C/S模型举例:
注:其中wrap.h和wrap.c请查看第五十篇博客
/* service.c */
#include
#include
#include
#include
#include
#include
#include
#include "wrap.h"
#define MAXLINE 80 //一次读read的长度
#define SERV_PORT 8000 //端口号
#define OPEN_MAX 1024 //最大epoll监控的数量,即可监控的客户端
int main(int argc, char *argv[])
{
int i, j, maxi, listenfd, connfd, sockfd;
int nready, efd, res;
ssize_t n;
char buf[MAXLINE], str[INET_ADDRSTRLEN];//buf为read到的数据,str为客户端IP地址用于打印
socklen_t clilen;
int client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
struct epoll_event tep, ep[OPEN_MAX];//ep为活跃的客户端,即有信息到达
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
Listen(listenfd, 20);
/* 初始化客户表,可以不需要使用该client */
for (i = 0; i < OPEN_MAX; i++)
client[i] = -1;
maxi = -1;
/* 创建一个epoll树,并将其句柄保存到efd中 */
efd = epoll_create(OPEN_MAX);
if (efd == -1)
perr_exit("epoll_create");
/******************开始注册*********************/
tep.events = EPOLLIN; //注册监听读事件
tep.data.fd = listenfd; //当事件激活返回时,通过data知道是哪个文件描述符
res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);//注册
/*****************注册结束**********************/
if (res == -1)
perr_exit("epoll_ctl");
while(1)
{
nready = epoll_wait(efd, ep, OPEN_MAX, -1); /* 阻塞监听*/
if (nready == -1)
perr_exit("epoll_wait");
for (i = 0; i < nready; i++)
{
if (!(ep[i].events & EPOLLIN))
continue;//该文件没有读数据到达 则跳过
if (ep[i].data.fd == listenfd)
{//该文件有数据到达,则data.fd中保存的就是该文件的文件描述符
//如果是listendfd则接收客户端请求,建立socket链接
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
//将用于和客户端通信的文件描述符保存到client[]数组中
for (j = 0; j < OPEN_MAX; j++)
if (client[j] < 0)
{
client[j] = connfd; /* save descriptor */
break;
}
if (j == OPEN_MAX)
perr_exit("too many clients");
if (j > maxi)
maxi = j; /* max index in client[] array */
/* 将客户端的文件描述符也注册到epoll树中,加以监听 */
tep.events = EPOLLIN;
tep.data.fd = connfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
if (res == -1)
perr_exit("epoll_ctl");
}
else
{//如果客户端有数据到达
sockfd = ep[i].data.fd;//从data.fd中取出和客户端通信的socket
n = Read(sockfd, buf, MAXLINE);
if (n == 0)
{//客户端断开
for (j = 0; j <= maxi; j++)
{//将该处的用户置为-1
if (client[j] == sockfd)
{
client[j] = -1;
break;
}
}
//删除监听该客户端
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
if (res == -1)
perr_exit("epoll_ctl");
//关闭该文件描述符socket
Close(sockfd);
printf("client[%d] closed connection\n", j);
}
else
{
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Writen(sockfd, buf, n);
}
}
}
}
close(listenfd);
close(efd);
return 0;
}
/* client.c */
#include
#include
#include
#include
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL)
{
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}