http://blog.csdn.net/zcc_0015/article/details/37618717
man epoll:
Possible Pitfalls and Ways to Avoid Them
o Starvation (edge-triggered)
If there is a large amount of I/O space, it is possible that by trying to drain it the other files will not get pro-
cessed causing starvation. (This problem is not specific to epoll.)
The solution is to maintain a ready list and mark the file descriptor as ready in its associated data structure,
thereby allowing the application to remember which files need to be processed but still round robin amongst all the
ready files. This also supports ignoring subsequent events you receive for file descriptors that are already ready.
o If using an event cache...
If you use an event cache or store all the file descriptors returned from epoll_wait(2), then make sure to provide a
way to mark its closure dynamically (i.e., caused by a previous event’s processing). Suppose you receive 100 events
from epoll_wait(2), and in event #47 a condition causes event #13 to be closed. If you remove the structure and
close(2) the file descriptor for event #13, then your event cache might still say there are events waiting for that
file descriptor causing confusion.
One solution for this is to call, during the processing of event 47, epoll_ctl(EPOLL_CTL_DEL) to delete file descriptor
13 and close(2), then mark its associated data structure as removed and link it to a cleanup list. If you find another
event for file descriptor 13 in your batch processing, you will discover the file descriptor had been previously
removed and there will be no confusion.
Nginx为提高效率采用描述符缓冲池(连接池)来处理tcp连接,一个连接对应一个读事件和一个写事件,nginx在启动的时候会创建好所用连接和事件,当事件来的时候不用再创建,然而连接池的使用却存在stale事件的问题,以下将详细分析Nginx是如何处理stale事件的,该问题涉及到epoll、Nginx连接与事件的相关知识。
1 Epoll的实现原理
epoll相关的系统调用有:epoll_create, epoll_ctl和epoll_wait。Linux-2.6.19又引入了可以屏蔽指定信号的epoll_wait: epoll_pwait。至此epoll家族已全。其中:
a、epoll_create用来创建一个epoll文件描述符,
b、epoll_ctl用来添加/修改/删除需要侦听的文件描述符及其事件,在ngx_epoll_add_event/ngx_epoll_del_event/ngx_epoll_add_connection/ngx_epoll_del_connection中使用
c、epoll_wait/epoll_pwait接收发生在被侦听的描述符上的,用户感兴趣的IO事件,在ngx_epoll_process_event
d、epoll文件描述符用完后,直接用close关闭即可。事实上,任何被侦听的文件符只要其被关闭,那么它也会自动从被侦听的文件描述符集合中删除。
(1)epoll和select相比,最大不同在于:
1epoll返回时已经明确的知道哪个sokcet fd发生了事件,不用再一个个比对。这样就提高了效率。
2select的FD_SETSIZE是有限止的,而epoll是没有限止的只与系统资源有关。
(2)Epoll在Nginx中的使用:
1)ngx_epoll_init 通过epoll_create函数创建了epfd句柄,这是epoll的接口,之后所有的函数调用都要使用该epfd句柄。
2)之后在ngx_event_process_init中,通过epoll_ctl将监听套接字和其对应的事件类型添加到epfd句柄中。
通过这两步,就完成了epoll事件处理的三部曲中的前两部,接下来就是由epoll_wait等待事件的发生。
2 Nginx的连接与事件
在Nginx中事件不需要创建,因为在Nginx启动时初始化ngx_cycle_t的过程中就分配了所有读、写事件的空间。这样,每个连接就对应了一个写事件和一个读事件。在Nginx中定义了两种连接:ngx_connection_t:被动连接,由客户端主动发起。ngx_peer_connection_t:主动连接。用于主动和上游服务器进行通信。当Nginx服务器接和客户端建立连接后,就会获得一个ngx_connection_t实体。和事件一样,连接不需要额外创建,它是从ngx_connection_t连接池中获得的,连接池在Nginx启动阶段就已经分配好了,由ngx_cycle_t结构体的connections成员和free_connections成员共同管理,free_connections相当于一个栈,每次从链表头插入,从链表头取(先进后出)。
3 如何处理stale事件
在epoll使用描述符缓存的情况下,我们大致可以将描述符的状态分为3组:
Nginx连接的文件描述符状态转移如下图所示
stale event(过期事件)有两种情况:
第一:在处理状态2的描述符集合的前面的描述符时,将此组中后面的描述符关闭了,那么后面的那个描述符就应该进入到了状态1,也就是空闲的描述符组,但是它仍然还存在于状态2集合中,因为它是由一次的epoll_wait返回的,我们没有办法在下次系统调用epoll_wait之前将它从此组中剔除。
在处理状态2的描述符的后续循环中仍然会处理它。此时就需要一个标志来标识此时的描述符状态:虽然仍在状态2的集合里面,但实际上已经进入状态1的状态了,通过将状态2集合中的fd置为-1(也就是starvation中提到的解决方法),来标识状态2集合内的描述符实际上已经回到了状态1的集合,在顺序处理的过程中,检查此标识,如果fd为-1那么就跳过不进行处理了。
第二、在处理状态2的描述符中有:#1, #2, ….. #40, ….,在处理#2的时候将#40关闭了,此时继续向下处理,但是还没到#40,此时又有新情况发生了,又来了新的连接,accept函数为他分配了新的描述符,恰好是#40,那么我们刚刚已经将#40的描述符的fd置为-1,现在它又被accept置为有效的值了,实际上已经进入了状态3。实际上此时的#40应该在下次epoll_wait的时候才返回,但是它现在还在我们的二组中,而还没处理到呢,一旦开始处理的时候,是不应该进行处理的,但是现在无法判断。
此种情况需要另一个标志instance来标识状态2和状态3的区别。在ngx_get_connection函数中获得一个新的connection的时候,将instance用上次值的反来初始化,假如此次初始化后的值为一bit位的x。
instance = rev->instance;
ngx_memzero(rev, sizeof(ngx_event_t));
ngx_memzero(wev, sizeof(ngx_event_t));
rev->instance = !instance;
wev->instance = !instance;
在epoll中添加(add_event),对应着描述符从状态1到状态3
删除(del_event),对应着从状态2或者状态3到状态1转换中,用event.data.ptr的最后一比特记录这个instance的值,这时这个值还是x。
ee.events = events | (uint32_t) flags;
ee.data.ptr = (void *) ((uintptr_t) c | ev->instance);
在ngx_epoll_process_events中,c->fd == -1是第一种情况, rev->instance != instance是指第二种情况,instance代表是ngx_epoll_add_event将该事件添加到epoll中时所记录的值,rev->instance代表该连接connection自己的值,正常情况下,获得连接时rev->instance是刚释放连接取反,它先经过epoll_ctl添加进epoll时被记录,然后在处理事件的epoll_wait的返回后,取出instance,此时它与rev->instance是相等的,然而在第二种情况下发生了变化,重新获得了文件描述符#40的连接,rev->instance立刻取反,而此时添加动作还没来得及进行(而此时还在处理epoll_wait返回的事件,添加动作还没生效),即epoll_ctl的添加动作未发生,即这个新获得连接的标识没有被记录,此时(此时指:还在处理epoll_wait返回的事件的时候)的instance仍然是#40的第一次连接保持的值,当然两者是不相等,只有等到新连接被添加后,再次进行epoll_wait时instance才会被更新为rev->instance,即两者相等,才可以被处理,而本次的epoll_wait将跳过该事件。
for (i = 0; i < events; i++) {
c = event_list[i].data.ptr;
instance = (uintptr_t) c & 1;
c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);
rev = c->read;
if (c->fd == -1 || rev->instance != instance) {
/*
* the stale event from a file descriptor
* that was just closed in this iteration
*/
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll: stale event %p", c);
continue;
}