当某一个进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体:
struct eventpoll {
…
/*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,也就是这个epoll监控的事件*/
struct rb_root rbr;
// 双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件
struct list_head rdllist;
…
};
每一个epoll对象都有一个独立的eventpoll结构体,用于存储使用epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂到rbr红黑树中,这样,重复添加的事件就可以通过红黑树而高效地识别出来。
所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说,相应的事件发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。当调用epoll_wait检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rdllist链表不为空,则把这里的事件复制到用户态内存中,同时将事件数量返回给用户。
epoll有两种工作模式:LT(水平触发)模式和ET(边缘触发)模式。ET模式与LT模式的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的;而LT模式则相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。默认情况下,Nginx是通过ET模式使用epoll的。
在ngx_epoll_module模块中定义了如下上下文结构体:
ngx_event_module_t ngx_epoll_module_ctx = {
&epoll_name,
ngx_epoll_create_conf,
ngx_epoll_init_conf,
{
// 对应于add方法
ngx_epoll_add_event,
// 对应于del方法
ngx_epoll_del_event,
// 对应于enable方法,与 add方法一致
ngx_epoll_add_event,
// 对应于disable方法,与del方法一致
ngx_epoll_del_event,
// 对应于add_conn方法
ngx_epoll_add_connection,
// 对应于del_conn方法
ngx_epoll_del_connection,
// 未实现process_changes方法
NULL,
// 对应于process_events方法
ngx_epoll_process_events,
// 对应于init方法
ngx_epoll_init,
// 对应于done方法
ngx_epoll_done,
}
};
其中,ngx_epoll_init方法主要调用epoll_create方法和创建event_list数组(用于获取事件)。ngx_epoll_add_event和ngx_epoll_del_event方法主要通过调用epoll_ctl向epoll中添加事件或从epoll中删除事件。
ngx_epoll_process_events是实现收集、分发事件的process_events接口。ngx_epoll_process_events方法会收集当前触发的所有事件,对于不需要加入到post队列延后处理的事件,该方法会立刻执行它们的回调方法,这其实是在做分发事件的工作,只是它会在自己的进程中调用这些回调方法而已,因此,每一个回调方法都不能导致进程休眠或者消耗太多的时间,以免epoll不能即时地处理其他事件。
过期事件是怎么回事呢?举个例子,假设epoll_wait一次返回3个事件,在第1个事件的处理过程中,由于业务的需要,所以关闭了一个连接,而这个连接恰好对应第3个事件。这样的话,在处理到第3个事件时,这个事件就已经是过期事件了,一旦处理必然出错。既然如此,把关闭的这个连接的fd套接字置为–1能解决问题吗?答案是不能处理所有情况。
下面先来看看这种貌似不可能发生的场景到底是怎么发生的:假设第3个事件对应的ngx_connection_t连接中的fd套接字原先是50,处理第1个事件时把这个连接的套接字关闭了,同时置为–1,并且调用ngx_free_connection将该连接归还给连接池。在ngx_epoll_process_events方法的循环中开始处理第2个事件,恰好第2个事件是建立新连接事件,调用ngx_get_connection从连接池中取出的连接非常可能就是刚刚释放的第3个事件对应的连接。由于套接字50刚刚被释放,Linux内核非常有可能把刚刚释放的套接字50又分配给新建立的连接。因此,在循环中处理第3个事件时,这个事件就是过期的了!它对应的事件是关闭的连接,而不是新建立的连接。如何解决这个问题?依靠instance标志位。当调用ngx_get_connection从连接池中获取一个新连接时,instance标志位就会置反,这样,当这个ngx_connection_t连接重复使用时,它的instance标志位一定是不同的。因此,在ngx_epoll_process_events方法中一旦判断instance发生了变化,就认为这是过期事件而不予处理。
instance标志位为什么可以判断事件是否过期?它利用了指针的最后一位一定是0这一特性(malloc出来的指针会内存对齐,总是一个偶数的倍数)。既然最后一位始终都是0,那么不如用来表示instance。这样,在使用ngx_epoll_add_event方法向epoll中添加事件时,就把epoll_event中联合成员data的ptr成员指向ngx_connection_t连接的地址,同时把最后一位置为这个事件的instance标志。而在ngx_epoll_process_events方法中取出指向连接的ptr地址时,先把最后一位instance取出来,再把ptr还原成正常的地址赋给ngx_connection_t连接。这样,instance究竟放在何处的问题也就解决了。
参考文章:《深入理解Nginx-模块开发与架构解析》