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

什麼是過期事件

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章