epoll是Linux特有的I/O复用函数,它在实现和使用上与select、poll有很大差异。

epoll使用一组函数来完成任务,而不是单个函数。

epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需像select、poll那样每次调用都要重复传入文件描述符集或事件集。

但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。


epoll API

epoll有epoll_create、epoll_ctl、epoll_wait三个系统调用。


1.epoll_create

epoll_create创建一个额外的文件描述符,来唯一标识内核中的这个事件表。

I/O多路复用——epoll_epoll

1)size参数现在不起作用,只是给内核一个提示,告诉它事件表需要多大。

2)该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。


2.epoll_ctl

epoll_ctl用来操作epoll的内核事件表。

I/O多路复用——epoll_epoll_02

1)fd参数是要操作的文件描述符。

2)op参数指定操作类型,操作类型有如下3种:


EPOLL_CTL_ADD往事件表上注册fd上的事件
EPOLL_CTL_MOD修改fd上的注册事件
EPOLL_CTL_DEL删除fd上注册的事件


3)event参数指定事件,它是epoll_event结构指针类型。

I/O多路复用——epoll_复用_03

events成员描述事件类型。epoll支持的文件类型和poll基本相同。表示epoll事件类型的宏是在poll对应的宏前加上“E”。但epoll有两个额外的事件类型——EPOLLET和EPOLLONESHOT,它们对于epoll高效运作非常关键。

I/O多路复用——epoll_复用_04


data成员用于存储用户数据,其类型epoll_data_t定义如下:

I/O多路复用——epoll_epoll_05

epoll_data是一个联合体,其四个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。

ptr成员可用来指定与fd相关的用户数据。但由于epoll_data_t是一个联合体,我们不能同时使用其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能使用其他手段,比如放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。


epoll_ctl 成功时返回0,失败返回-1并设置errno。


3.epoll_wait

它在一段超时时间内等待一组文件描述符上的事件。

I/O多路复用——epoll_epoll_06


参数从后往前

1)timeout参数的含义与poll接口的timeout参数相同。

2)maxevents参数指定最多监听多少个事件。

3)epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中拷贝到它的第二个参数events指向的数组中。这个数组只用于输出epoll检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大提高了应用程序索引就绪文件描述符的效率。


epoll_wait成功时返回就绪的文件描述符个数,失败时返回-1并设置errno。




LT和ET模式

        epoll对文件描述符的操作有两种模式:LT(Level Trigger,水平触发)模式和ET(Edge Trigger,边缘触发)模式。


        LT工作模式是默认的工作模式,这种模式下epoll相当于一个效率较高的poll。当epoll_wait检测到其上有时间发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用eoll_wait时,epoll_wait还会再次向应用程序通告该事件,直到该事件被处理。


        当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。当epoll_wait检测到其上有事件发生并将此通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。

                                                                                                    ——《Linux高性能服务器编程》


        LT同时支持block和non-block socket。这种模式中,内核告诉我们一个文件描述符是否就绪了,然后我们可以对这个就绪的fd进行I/O操作。如果我们不做任何操作,内核还是会继续通知我们 。所以,这种模式编程出错的可能性要小一点,传统的select/poll都是这种模式的代表。


        ET是高速的工作方式,只支持non-block socket,它的效率要比LT更高。ET与LT的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式正好相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。 因此,LT模式下开发基于epoll的应要简单些,不太容易出错。而在ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。 


Nginx默认使用ET模式来使用epoll。



 epoll ET模式为何fd必须要设置为非阻塞

        ET(边缘触发)数据就绪只会通知一次,也就是说,如果要使用ET模式,当数据就绪时,需要一直read,直到出错或完成为止。但倘若当前fd为阻塞(默认),那么在当读完缓冲区的数据时,如果对端并没有关闭写端,那么该read函数会一直阻塞,影响其他fd以及后续逻

辑。所以把fd设置为非阻塞,当没有数据的时候,read虽然读取不到任何内容,但是肯定不会被阻塞,那么此时,说明缓冲区数据已经读取完毕,需要继续处理后续逻辑(读取其他fd或者进行wait)。


epoll的优点:

1.支持一个进程打开大数目的socket描述符(fd)。

        select的缺点是一个进程打开的fd是有限制的,由FD_SETSIZE指定,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。要解决这个问题,我们一是选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降;二是可以选择多进程的解决方案(传统的Apache方案),不过虽然Linux上面创建进程的代价比较小,但仍是不可忽略的 ,加上进程间数据同步远不不上线程间同步高效,所以也不是一种完美的方案。不过epoll则没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048,这个数目和系统内存关系很大。例如在1GB内存的机器上大约是10w左右,具体数目可以cat /proc/sys/fs/file-max查看。


2.I/O效率不随fd数目增加而下降。

        传统的select/poll另一个缺点是当我们拥有一个很大的socket集合,不过由于网络延时,任何时间只有部分的socket是活跃的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的回调函数实现的。

        I/O多路复用——epoll_epoll_07


3.使用mmap加速内核与用户空间的消息传递。

        无论是select、poll还是epoll都要通过内核把fd消息通知给用户空间,epoll是通过内核与用户空间mmap同一块内存实现的。


使用epoll的tcp服务器,完成简单的HTTP消息回显

I/O多路复用——epoll_epoll_08


用浏览器测试:

I/O多路复用——epoll_epoll_09