在linux的网络编程中,很长的时间都在使用select来作事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。相比于select,epoll最大的好处在于它不会随着监听fd数目的增加而下降效率。由于在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,天然耗时越多。而且,linux/posix_types.h头文件有这样的声明:linux
#define__FD_SETSIZE 1024
表示select最多同时监听1024个fd,固然,能够经过修改头文件再重编译内核来扩大这个数目,但这彷佛并不治本。编程
epoll的接口很是简单,一共就三个函数:
1.建立epoll句柄
int epfd = epoll_create(intsize); 数组
建立一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不一样于select()中的第一个参数,给出最大监听的fd+1的值。须要注意的是,当建立好epoll句柄后,它就是会占用一个fd值,在linux下若是查看/proc/进程id/fd/,是可以看到这个fd的,因此在使用完epoll后,必须调用close()关闭,不然可能致使fd被耗尽。
函数声明:int epoll_create(int size)
该 函数生成一个epoll专用的文件描述符。它实际上是在内核申请一空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。size就是你在这个epoll fd上能关注的最大socket fd数。随你定好了。只要你有空间。可参见上面与select之不一样
2.将被监听的描述符添加到epoll句柄或从epool句柄中删除或者对监听事件进行修改。网络
函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
该函数用于控制某个epoll文件描述符上的事件,能够注册事件,修改事件,删除事件。
参数:
epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操做例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除socket
fd:关联的文件描述符;
event:指向epoll_event的指针;
若是调用成功返回0,不成功返回-1函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event*event); ui
epoll的事件注册函数,它不一样与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。spa
第一个参数是epoll_create()的返回值,
第二个参数表示动做,用三个宏来表示:
EPOLL_CTL_ADD: 注册新的fd到epfd中;
EPOLL_CTL_MOD: 修改已经注册的fd的监听事件;
EPOLL_CTL_DEL: 从epfd中删除一个fd;
第三个参数是须要监听的fd,
第四个参数是告诉内核须要监听什么事件,structepoll_event结构以下:指针
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; 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)模式,这是相对于水平触发(Level Triggered)来讲的。
EPOLLONESHOT: 只监听一次事件,当监听完此次事件以后,若是还须要继续监听这个socket的话,须要再次把这个socket加入到EPOLL队列里。
如:
struct epoll_event ev;
//设置与要处理的事件相关的文件描述符
ev.data.fd=listenfd;
//设置要处理的事件类型
ev.events=EPOLLIN|EPOLLET;
//注册epoll事件
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
3.等待事件触发,当超过timeout尚未事件触发时,就超时。
int epoll_wait(int epfd, struct epoll_event * events, intmaxevents, int timeout);
等待事件的产生,相似于select()调用。参数events用来从内核获得事件的集合,maxevents告以内核这个events有多大(数组成员的个数),这个maxevents的值不能大于建立epoll_create()时的size,参数timeout是超时时间(毫秒,0会当即返回,-1将不肯定,也有说法说是永久阻塞)。
该函数返回须要处理的事件数目,如返回0表示已超时。
返回的事件集合在events数组中,数组中实际存放的成员个数是函数的返回值。返回0表示已经超时。
函数声明:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
该函数用于轮询I/O事件的发生;
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值(单位我也不太清楚);-1至关于阻塞,0至关于非阻塞。通常用-1便可
返回发生事件数。
epoll_wait运行的原理是
等侍注册在epfd上的socket fd的事件的发生,若是发生则将发生的sokct fd和事件类型放入到events数组中。
并 且将注册在epfd上的socket fd的事件类型给清空,因此若是下一个循环你还要关注这个socket fd的话,则须要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来从新设置socket fd的事件类型。这时不用EPOLL_CTL_ADD,由于socket fd并未清空,只是事件类型清空。这一步很是重要。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------blog
从man手册中,获得ET和LT的具体描述以下
EPOLL事件有两种模型:
Edge Triggered(ET) //高速工做方式,错误率比较大,只支持no_block socket (非阻塞socket)
LevelTriggered(LT) //缺省工做方式,即默认的工做方式,支持blocksocket和no_blocksocket,错误率比较小。
假若有这样一个例子:(LT方式,即默认方式下,内核会继续通知,能够读数据,ET方式,内核不会再通知,能够读数据)
1.咱们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
2. 这个时候从管道的另外一端被写入了2KB的数据
3. 调用epoll_wait(2),而且它会返回RFD,说明它已经准备好读取操做
4. 而后咱们读取了1KB的数据
5. 调用epoll_wait(2)......
Edge Triggered工做模式:
若是咱们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)以后将有可能会挂起,由于剩余的数据还存在于文件的输入缓冲区内,并且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候ET工做模式才会汇报事件。所以在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,由于在第2步执行了一个写操做,而后,事件将会在第3步被销毁。由于第4步的读取操做没有读空文件输入缓冲区内的数据,所以咱们在第5步调用epoll_wait(2)完成后,是否挂起是不肯定的。epoll工做在ET模式的时候,必须使用非阻塞套接口,以免因为一个文件句柄的阻塞读/阻塞写操做把处理多个文件描述符的任务饿死。最好如下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。(LT方式能够解决这种缺陷)
i 基于非阻塞文件句柄
ii 只有当read(2)或者write(2)返回EAGAIN时(认为读完)才须要挂起,等待。但这并非说每次read()时都须要循环读,直到读到产生一个EAGAIN才认为这次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时(即小于sizeof(buf)),就能够肯定此时缓冲中已没有数据了,也就能够认为此事读事件已处理完成。
Level Triggered工做模式 (默认的工做方式)
相反的,以LT方式调用epoll接口的时候,它就至关于一个速度比较快的poll(2),而且不管后面的数据是否被使用,所以他们具备一样的职能。由于即便使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者能够设定EPOLLONESHOT标志,在epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。所以当EPOLLONESHOT设定后,使用带有EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须做的事情。
而后详细解释ET, LT:
//没有对就绪的fd进行IO操做,内核会不断的通知。
LT(leveltriggered)是缺省的工做方式,而且同时支持block和no-blocksocket。在这种作法中,内核告诉你一个文件描述符是否就绪了,而后你能够对这个就绪的fd进行IO操做。若是你不做任何操做,内核仍是会继续通知你的,因此,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的表明。
//没有对就绪的fd进行IO操做,内核不会再进行通知。
ET(edge-triggered)是高速工做方式,只支持no-blocksocket。在这种模式下,当描述符从未就绪变为就绪时,内核经过epoll告诉你。而后它会假设你知道文件描述符已经就绪,而且不会再为那个文件描述符发送更多的就绪通知,直到你作了某些操做致使那个文件描述符再也不为就绪状态了(好比,你在发送,接收或者接收请求,或者发送接收的数据少于必定量时致使了一个EWOULDBLOCK错误)。可是请注意,若是一直不对这个fd做IO操做(从而致使它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍须要更多的benchmark确认(这句话不理解)。
另外,当使用epoll的ET模型(epoll的非默认工做方式)来工做时,当产生了一个EPOLLIN事件后,
读数据的时候须要考虑的是当recv()返回的大小若是等于要求的大小,即sizeof(buf),那么颇有多是缓冲区还有数据未读完,也意味着该次事件尚未处理完,因此还须要再次读取:
while(rs) //ET模型
{
buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen < 0)
{
//因为是非阻塞的模式,因此当errno为EAGAIN时,表示当前缓冲区已无数据可读
// 在这里就看成是该次事件已处理处.
if(errno== EAGAIN || errno == EINT) //即当buflen<0且errno=EAGAIN时,表示没有数据了。(读/写都是这样) break; else