epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,可以理解为消息就绪处理机制,或者多路复用机制。相对于select和poll来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。优点如下:
1.没有最大并发连接的限制,能打开的fd上限远大于1024(1G的内存能监听约10万个端口)
2.采用回调的方式,效率提升。只有活跃可用的fd才会调用callback函数,也就是说 epoll 只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。
3.内存拷贝。使用mmap()文件映射内存来加速与内核空间的消息传递,减少复制开销。
epoll对文件描述符的操作有两种模式:LT(level trigger,水平触发)和ET(edge trigger)。
水平触发:默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件。
边缘触发:当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时通知一次)。
ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。
Linux中提供的epoll相关函数接口如下:
1. int epoll_create(int size);
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
3. int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
流程即为:
1. 调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句 柄数,多于这个最大数时内核可不保证效果。
2. epoll_ctl可以操作上面建立的epoll,将刚建立的socket加入到epoll中让其监控,或者把epoll正在监控的某个socket句柄移出epoll,不再监控它等等。
3. epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时, 就返回用户态的进程。
流程分析:
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:
1. struct eventpoll{
2. ....
3./*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
4. struct rb_root rbr;
5./*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
6. struct list_head rdlist;
7. ....
8. };
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:
1. struct epitem{
2. struct rb_node rbn;//红黑树节点
3. struct list_head rdllink;//双向链表节点
4. struct epoll_filefd ffd; //事件句柄信息
5. struct eventpoll *ep; //指向其所属的eventpoll对象
6. struct epoll_event event; //期待发生的事件类型
7. }
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
当我们调用epoll_ctl往里塞入百万个句柄时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
最后摘要:
调用epoll_create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。
在内核里,一切皆文件。epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。
epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。