什么是过期事件
举个例子,假设epoll_wait一次返回3个事件,在第1个事件的处理过程中,由于业务的需要,所以关闭了一个连接,而这个连接恰好对应第3个事件。这样的话,在处理到第3个事件时,这个事件就已经是过期事件了,一旦处理必然出错。
nginx的处理方法
文题已经指出是通过instance标志位来区分过期事件的。
在nginx中,每一个事件都由ngx_event_t结构体来表示,instance标志位也定义在该结构体中:
typedef struct ngx_event_s ngx_event_t;
struct ngx_event_s {
void *data;
unsigned write:1;
......
/*
这个标志位用于区分当前事件是否是过期的,它仅仅是给事件驱动模块使用的,而事件消费模块可不用关心。
为什么需要这个标志位呢?当开始处理一批事件时,处理前面的事件可能会关闭一些连接,而这些连接有可能影响这批事件中还未处理到的后面的事件。
这时,可通过instance标志位来避免处理后面的已经过期的事件。
*/
unsigned instance:1;
......
};
下文我们将以ngx_epoll_module中向epoll添加事件的方法ngx_epoll_add_event,配合收集、分发事件的方法ngx_epoll_process_event为例,来说明instance标志位的用法,学习这个巧妙的设计。
static ngx_int_t ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
{
int op;
uint32_t events, prev;
ngx_event_t *e;
ngx_connection_t *c;
struct epoll_event ee;
// 每个事件的data成员都存放着其对应的ngx_connection_t连接
c = ev->data;
// 下面会根据event参数确定当前事件是读事件还是写事件,这会决定events是加上EPOLLIN还是EPOLLOUT标志位
events = (uint32_t)event;
...
// 根据active标志位确定是否为活跃事件,以决定到底是修改还是添加事件
if(e->active) {
op = EPOLL_CTL_MOD;
...
} else {
op = EPOLL_CTL_ADD;
}
// 加入flags参数到events标志位中
ee.events = events | (uint32_t)flags;
// ptr成员存储的是ngx_connection_t连接
// instance标志位,下面将配合ngx_epoll_process_events方法说明它的用法
ee.data.ptr = (void*)((uintptr_t)c | ev->instance);
// 调用epoll_ctl方法向epoll中添加事件或者在epoll中修改事件
if(epoll_ctl(ep, op, c->fd, &ee) == -1) {
ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_error, "epoll_ctl(%d, %d) failed", op, c->fd);
return NGX_ERROR;
}
// 将事件的active标志位置为1,表示当前事件是活跃的
ev->active = 1;
return NGX_OK;
}
static ngx_int_t ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
int events;
uint32_t revents;
ngx_int_t instance, i;
ngx_event_t *rev, *wev, **queue;
ngx_connection_t *c;
// 调用epoll_wait获取事件
events = epoll_wait(ep, event_list, (int)nevents, timer);
...
if(flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
ngx_time_update();
}
...
// 遍历本次epoll_wait返回的所有事件
for(i = 0; i < events; i++) {
// 从上述的ngx_epoll_add_event方法可以看到ptr成员就是ngx_connection_t连接的地址
// 但最后一位(instance)有特殊含义,需要屏蔽掉
c = event_list[i].data.ptr;
// 将地址的最后一位取出来,用instance变量标识
instance = (uintptr_t)c & 1;
// 无论是32位还是64位机器,其地址的最后一位肯定是0(利用了指针的最后一位一定是0这一特性)
// 用下面这行语句把ngx_xonnection_t的地址还原到真正的地址值
c = (ngx_connection_t *)((uintptr_t)c & (uintptr_t)~1);
// 取出读事件
rev = c->read;
// 判断这个读事件是否为过期事件
if(c->fd == -1 || rev->instance != instance) {
continue;
}
// 取出事件类型
revents = event_list[i].events;
...
// 如果是读事件且该事件是活跃的
if((revents & EPOLLIN) && rev->active) {
if(flags & NGX_POST_EVENTS) {
queue = (ngx_event_t**)(rev->accept ? &ngx_posted_accept_events : &ngx_posted_events);
ngx_locked_post_event(rev, queue);
} else {
rev->handler(rev);
}
}
// 取出写事件
wev = c->write;
if((revents & EPOLLOUT) && wev->active) {
// 判断这个写事件是否为过期事件
if(c->fd == -1 || wev->instance != instance) {
continue;
}
...
if(flags & NGX_POST_EVENTS) {
ngx_locked_post_event(rev, queue);
} else {
wev->handler(wev);
}
}
}
...
return NGX_OK;
}
instance标志位为什么可以判断事件是否过期?从上面的代码可以看出,instance标志位的使用其实很简单,它利用了指针的最后一位是0这个特性。既然最后一位始终为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究竟放在何处的问题也就解决了。
再回到开篇我们提出的过期事件的应用场景:假设第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* ngx_get_connection(ngx_socket_t s, ngx_log_t *log)
{
...
// 从连接池中获取一个连接
ngx_connection_t *c;
c = ngx_cycle->free_connections;
...
rev = c->read;
wev = c->write;
...
instance = rev->instance;
// 将instance标志位置为原来的相反位
rev->instance = !instance;
wev->instance = !instance;
...
return c;
}
这样,当这个ngx_connection_t连接重复使用时,它的instance标志位一定是不同的。因此,在ngx_epoll_process_events方法中一旦判断instance发生了变化,就认为这是过期事件而不处理。
这种设计方法非常值得我们学习,因为它几乎没有增加任何成本就很好地解决了服务器开发时一定会出现的过期事件问题。
参考资料:
陶辉.深入理解Nginx 模块开发与架构解析.北京:机械工业出版社,2013