Nginx使用instance标记处理kqueue/epoll中的stale事件
下面以epoll为例:
每个fd往往会关联一个自定义的结构体来表示逻辑上的连接。比如在nginx中每个ngx_connection_t都抽象成一个TCP连接并且与fd关联。
当我们accept到一个连接的时候,我们会申请这样一个ngx_connection_t,随后将这个fd与ngx_connection_t关联并加入到epoll循环。
代码(添加读事件)
1 2 3 4 5 6 7 8 9 10 |
ngx_connection_t *c; struct epoll_event ee; fd = accept(…); c = ngx_get_connection(fd,…); ee.events = EPOLLIN; ee.data.ptr = (void *) ((uintptr_t) c | c->read->instance); epoll_ctl(ep, EPOLL_CTL_ADD, c->fd, &ee) |
然后我们使用epoll_wait来检查发生的事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
struct epoll_event event_list[1024]; /*轮循*/ n = epoll_wait(ep, event_list, 1024, NULL); /*检查发生事件*/ for (i = 0;i < n; i++) { c = event_list[i].data.ptr; instance = (uintptr_t) c & 1; c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1); /*以读事件为例*/ r = c->read; if (c->fd == -1 || r->instance != instance) { continue;/*NOTE!*/ } /*other code*/ } |
每个epoll_event的data.ptr是由ngx_connection_t*指针与instance相或而来,也就是data.ptr = c | instance;这样做是有前提条件的,那就是c必须是偶数,因为instance是bool类型,data.ptr最末位存储了instance的值。
假设当某一次epoll_wait后返回了一组epoll_event。
随后进行了如下操作:如图1所示:
(1)fd=47的文件描述有事件发生。
(2)处理fd=47时,我们关闭了fd=12的连接,回收了ngx_connection_t内存。
(3)接着处理fd=12时,此时data_ptr指向的ngx_connection_t已经被销毁,访问data_ptr将出现致命错误。
解决方法(如图2所示):暂不回收ngx_connection_t内存而是将ngx_connection_t的fd设置为-1,那么处理fd=12时,发现ngx_connection_t中的fd=-1,那么足以证明这个fd在之前已经被释放掉了,从面阻止使用data_ptr继续使用,这就是stale事件。
当我们将fd=12的ngx_connection_t回收后将fd设成了-1,防止处理到fd=12时使用了被释放的data_ptr。
试想一下,此时将fd设置成了-1,但在处理fd=12前,我将刚才释放的ngx_connection_t重新分配给了新连接fd=48(调用了accept()),那么在处理fd=12时,其data_ptr仍然不能使用,因这个ngx_connection_t已不属于fd=12而是属于fd=48,也就是说fd=12的ngx_connection_t已被释放并被fd=48使用。
我们使用instance来区分这个ngx_connection_t到底属于谁。因为本身data_ptr中存储了instance值,并且ngx_connection_t中也存储了instance值,在申请ngx_connection_t时,我们要将ngx_connection_t中的instance值置反。当ngx_connection_t的instance与data_ptr不一致时,说明当前fd已销毁。(如图3所示)
注意:
另外,有一点要说明的是,如果在处理fd=12之前,fd=48也被销毁了,然后该ngx_connection_t又被fd=49复用,那么这种情况仍然会有问题。
这会导致data_ptr中的instance值与ngx_connection_t一致(两次置反)并且fd=49。event_list原本指示的fd=12有事件,但被nginx错误地认为了fd=49有事件发生,目前我还没想到好的解决办法。