I/O多路复用之epoll

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了I/O多路复用之epoll相关的知识,希望对你有一定的参考价值。

一、什么是epoll

    epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。


二、epoll的相关系统调用 

epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用。 

  1. int epoll_create(int size); 

    技术分享

创建一个epoll的句柄。从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,

技术分享

所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

返回值:

技术分享  

2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 

技术分享

epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,是在这里先注册要监听的事件类型。 

第一个参数是epoll_create()的返回值。 

第二个参数表示动作,用三个宏来表示: 

技术分享

EPOLL_CTL_ADD:注册新的fd到epfd中; 

EPOLL_CTL_MOD:修改已经注册的fd的监听事件; 

EPOLL_CTL_DEL:从epfd中删除一个fd; 

第三个参数是需要监听的fd。 

第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

技术分享

其中events表示感兴趣的事件,data表示保存触发事件的某个文件描述符的相关数据。

events可以是以下个宏的集合: 

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); 

EPOLLOUT:表示对应的文件描述符可以写; 

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这应该表有带外数据到来); 

EPOLLERR:表示对应的文件描述符发生错误; 

EPOLLHUP:表示对应的文件描述符被挂断; 

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level 

Triggered)来说的。 

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列中。

返回值:

技术分享


3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

技术分享 

收集在epoll监控的事件中已经发送的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告之内核这个events有多少,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。

技术分享

如果函数调用成功,返回对应I/O上已准备好的文件描述符数,如返回0表示已超时。

 

epoll工作原理 

epoll同样只告知那些就绪的文件描述符,并且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。 

  

另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,epoll事先通过epoll_ctl()来注册某个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。


三、epoll有两种工作方式-LT(水平触发)和ET(边缘触发)


(1)以LT方式调用epoll接口的时候,它就相当于一个速度比较较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。 

LT(level triggered)是epoll缺省的工作方式,并且同时支持block和no-block socket.在这种做法

中,内核告诉你某个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

LT模式代码如下:

技术分享

技术分享

技术分享

技术分享

技术分享

技术分享

运行结果:用telnet测试,结果如图

技术分享

sever端收到消息后再将消息在发送回去,为一问一答模式。

用浏览器访问,修改代码如下:

技术分享

运行结果为:

技术分享

(2)ET工作模式

如果我们将某个文件描述符添加到epoll描述符的时候使用了EPOLLET标志,那么在调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,并且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET工作模式才会汇报事件。因此,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。

epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口。 

   i    基于非阻塞文件句柄 

   ii   只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生意个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此时读事件已处理完成。 

代码如下:

技术分享

技术分享

技术分享

技术分享

技术分享

技术分享

运行结果如下:

技术分享

ET (edge-triggered)是高速工作模式,只支持no-block socket,它效率要比LT更。ET与LT的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式正好相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。 

因此,LT模式下开发基于epoll的应用要简单些,不太容易出错。如果在ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。


 epollET模式fd为何必须设置为非阻塞:

ET(边缘触发)数据就绪只会通知以次,也就是说,如果要使用ET模式,当数据就绪时,需要一直read,直到出错或完成为止。但倘若当前fd为阻塞(默认),那么在当读完缓冲区内的数据时,如果对端并没有关闭写端,那么该read函数会一直阻塞,影响其他fd以及后续逻辑!所以此时将该fd设置为非阻塞,当没有数据的时候,read虽然读取不到任何内容,但是肯定不会被hang住,那么此时,说明缓冲区数据已经读取完毕,需要继续处理后续逻辑(读取其他fd或者进行wait)。 


本文出自 “流苏” 博客,请务必保留此出处http://ab6107.blog.51cto.com/10538332/1793084

以上是关于I/O多路复用之epoll的主要内容,如果未能解决你的问题,请参考以下文章

I/O多路复用之select

I/O多路复用之Select

I/O多路复用之——select

高级I/O---多路复用---epoll

I/O多路复用之 epoll 详解

I/O多路复用之poll