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事件
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)和删除(del_event),对应着描述符从状态1到状态3和从状态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_ctl的添加动作未发生,即这个新获得连接的标识没有被记录,此时的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;
}