在linux
没有实现epoll
事件驱动机制之前,我们一般选择用select
或者poll
等IO
多路复用的方法来实现并发服务程序。在大数据、高并发、集群等一些名词唱得火热之年代,select
和poll
的用武之地越来越有限,风头已经被epoll
占尽。
epoll实现
epoll
通过在Linux
内核中申请一个简易的文件系统。
把原先的select/poll
调用分成了3个部分:
1)调用
epoll_create
建立一个epoll
对象(在epoll
文件系统中为这个句柄对象分配资源)
2)调用epoll_ctl
向epoll
对象中添加所有连接的套接字
3)调用epoll_wait
收集发生的事件的连接
要实现高并发的多个连接,只需要在进程启动时建立一个epoll
对象,然后在需要的时候向这个epoll
对象中添加或者删除连接。同时,epoll_wait
的效率也非常高,因为调用epoll_wait
时,并没有一股脑的向操作系统复制所有连接的句柄数据,内核也不需要去遍历全部的连接(不再需要从用户态复制句柄数据结构到内核态)。
下面来看看Linux
内核具体的epoll
机制实现思路。
当某一进程调用epoll_create
方法时,Linux
内核会创建一个eventpoll
结构体,这个结构体中有两个成员与epoll
的使用方式密切相关。eventpoll
结构体如下所示:
1 | struct eventpoll{ |
每一个epoll
对象都有一个独立的eventpoll
结构体,用于存放通过epoll_ctl
方法向epoll
对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn
,其中n
为树的高度)。
而所有添加到epoll
中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback
,它会将发生的事件添加到rdlist
双链表中。
在epoll
中,对于每一个事件,都会建立一个epitem
结构体,如下所示:
1 | struct epitem{ |
当调用epoll_wait
检查是否有事件发生时,只需要检查eventpoll
对象中的rdlist
双链表中是否有epitem
元素即可。如果rdlist
不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
从上面的讲解可知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll
的高效。
OK,讲解完了epoll
的机理,我们便能很容易掌握epoll
的用法了。
一句话描述就是:三步曲。
第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。
第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。
具体流程
使用起来很清晰,首先要调用epoll_create
建立一个epoll
对象。参数size
是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。epoll_ctl
可以操作上面建立的epoll
,例如,将刚建立的socket
加入到epoll
中让其监控,或者把epoll
正在监控的某个socket
句柄移出epoll
,不再监控它等等。epoll_wait
在调用时,在给定的timeout
时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。
epoll
在被内核初始化时(操作系统启动),同时会开辟出epoll
自己的内核高速cache
区,用于安置每一个我们想监控的socket
,这些socket
会以红黑树的形式保存在内核cache
里,以支持快速的查找、插入、删除。这个内核高速cache
区,就是建立连续的物理内存页,然后在之上建立slab
层,简单的说,就是物理上分配好你想要的size
的内存对象,每次使用时都是使用空闲的已分配好的对象。
几乎所有的epoll
程序都使用下面的框架:
1 | for( ; ; ) |
LT 和 ET
epoll还提供了两种工作模式。
LT(水平触发)是缺省的工作模式
ET(边缘触发)是高速模式
用法演示
- epoll.cc,演示了epoll的通常用法,使用epoll的LT模式
- epoll-et.cc,演示了epoll的ET模式,与LT模式非常像,区别主要体现在不需要手动开关EPOLLOUT事件
LT 和 ET本质的区别是
LT
模式状态时,主线程正在epoll_wait
等待事件时,请求到了,epoll_wait
返回后没有去处理请求(recv)
,那么下次epoll_wait
时此请求还是会返回。因为没有处理时,每一次内核都会通知,所以这种模式编程出错误可能性要小一点。ET
模式状态下,这次没处理,下次epoll_wait
时将不返回。然后它会假设你知道文件描述符已经就绪,所以我们应该每次一定要处理。ET模式很大程度降低内核发送通知的次数,性能更好。