开发高性能网络程序时。windows开发人员们言必称iocp,linux开发人员们则言必称epoll。大家都明确epoll是一种IO多路复用技术,能够很高效的处理数以百万计的socket句柄,比起曾经的select和poll效率高大发了。
我们用起epoll来都感觉挺爽,确实快,那么。它究竟为什么能够快速处理这么多并发连接呢?
先简单回想下怎样使用C库封装的3个epoll系统调用吧。
[cpp] view plaincopy
- int epoll_create(int size);
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
使用起来非常清晰,首先要调用epoll_create建立一个epoll对象。參数size是内核保证可以正确处理的最大句柄数。多于这个最大数时内核可不保证效果。
epoll_ctl能够操作上面建立的epoll,比如,将刚建立的socket增加到epoll中让其监控。或者把 epoll正在监控的某个socket句柄移出epoll。不再监控它等等。
epoll_wait在调用时,在给定的timeout时间内,当在监控的全部句柄中有事件发生时,就返回用户态的进程。
从上面的调用方式就能够看到epoll比select/poll的优越之处:由于后者每次调用时都要传递你所要监控的全部socket给select/poll系统调用,这意味着须要将用户态的socket列表copy到内核态,假设以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,很低效。
而我们调用epoll_wait时就相当于以往调用select/poll,可是这时却不用传递socket句柄给内核,由于内核已经在epoll_ctl中拿到了要监控的句柄列表。
所以。实际上在你调用epoll_create后,内核就已经在内核态開始准备帮你存储要监控的句柄了。每次调用epoll_ctl仅仅是在往内核的数据结构里塞入新的socket句柄。
在内核里,一切皆文件。所以,epoll向内核注冊了一个文件系统,用于存储上述的被监控socket。
当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它仅仅服务于epoll。
epoll在被内核初始化时(操作系统启动)。同一时候会开辟出epoll自己的内核快速cache区,用于安置每个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里。以支持快速的查找、插入、删除。
这个内核快速cache区。就是建立连续的物理内存页。然后在之上建立slab层,简单的说。就是物理上分配好你想要的size的内存对象,每次使用时都是使用空暇的已分配好的对象。
[cpp] view plaincopy
- static int __init eventpoll_init(void)
- {
- ... ...
- /* Allocates slab cache used to allocate "struct epitem" items */
- epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem),
- 0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC,
- NULL, NULL);
- /* Allocates slab cache used to allocate "struct eppoll_entry" */
- pwq_cache = kmem_cache_create("eventpoll_pwq",
- sizeof(struct eppoll_entry), 0,
- EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL);
- ... ...
epoll的高效就在于,当我们调用epoll_ctl往里塞入百万个句柄时,epoll_wait仍然能够飞快的返回,并有效的将发生事件的句柄给我们用户。
这是因为我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,只观察这个list链表里有没有数据就可以。有数据就返回。没有数据就sleep,等到timeout时间到后即使链表没数据也返回。
所以,epoll_wait很高效。
并且,通常情况下即使我们要监控百万计的句柄。大多一次也仅仅返回非常少量的准备就绪句柄而已,所以,epoll_wait仅须要从内核态copy少量的句柄到用户态而已,怎样能不高效?!
那么,这个准备就绪list链表是怎么维护的呢?当我们运行epoll_ctl时,除了把socket放到epoll文件系统里file对象相应的红黑树上之外,还会给内核中断处理程序注冊一个回调函数,告诉内核。假设这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
如此。一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们攻克了大并发下的socket处理问题。运行epoll_create时,创建了红黑树和就绪链表,运行epoll_ctl时,假设添加socket句柄,则检查在红黑树中是否存在,存在马上返回。不存在则加入到树干上,然后向内核注冊回调函数,用于其中断事件来暂时向准备就绪链表中插入数据。运行epoll_wait时立马返回准备就绪链表里的数据就可以。
最后看看epoll独有的两种模式LT和ET。不管是LT和ET模式。都适用于以上所说的流程。
差别是,LT模式下,仅仅要一个句柄上的事件一次没有处理完。会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。
这件事怎么做到的呢?当一个socket句柄上有事件时。内核会把该句柄插入上面所说的准备就绪list链表。这时我们调用epoll_wait。会把准备就绪的socket复制到用户态内存。然后清空准备就绪list链表,最后。epoll_wait干了件事。就是检查这些socket,假设不是ET模式(就是LT模式的句柄了),而且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,仅仅要它上面还有事件。epoll_wait每次都会返回。而ET模式的句柄。除非有新中断到,即使socket上的事件没有处理完,也是不会次次从epoll_wait返回的。