nginx學習筆記(4):通過instance標誌位處理過期事件

li27z發表於2017-01-04

什麼是過期事件

舉個例子,假設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

相關文章