nginx事件機制解讀

nginx事件模塊機制

事件模塊主要包含以下文件:
ngx_event.c/h 事件核心模塊,以及定義所有事件模塊的統一接口
ngx_event_accept.c 事件連接處理
ngx_event_posted.c/h 隊列事件相關,主要隊列事件的添加,刪除,處理
ngx_event_timer.c/h 定時器事件相關,定時器事件相關的執行,添加,刪除
其他就是不同系統對應模塊的事件epoll,poll,select,iocp,kqueue等

事件類型

nginx事件類型主要分爲三類:網絡io事件,定時器事件,隊列事件

事件模塊接口

nginx定義了不同的模塊,有http,mail,event等模
ngx_event_module_t 結構體是nginx事件模塊的接口
    /* 事件驅動模型通用接口ngx_event_module_t結構體 */
typedef struct {
    /* 事件模塊名稱 */
    ngx_str_t              *name;

    /* 解析配置項前調用,創建存儲配置項參數的結構體 */
    void                 *(*create_conf)(ngx_cycle_t *cycle);
    /* 完成配置項解析後調用,處理當前事件模塊感興趣的全部配置 */
    char                 *(*init_conf)(ngx_cycle_t *cycle, void *conf);

    /* 每個事件模塊具體實現的方法,有10個方法,即IO多路複用模型的統一接口 */
    ngx_event_actions_t     actions;
} ngx_event_module_t;

nginx事件模塊支持了多種不同操作系統的事件機制,例如:devpoll, epoll, iocp, kqueue, poll, select, win32_select等。其中ngx_event_actions_t 結構體,該成員結構實現了不同事件驅動模塊的具體方法。封裝了IO多路複用模型的統一接口 該結構體定義在文件src/event/ngx_event.h 中:

typedef struct {
    ngx_int_t  (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
    /* 刪除事件,將某個描述符的某個事件從事件驅動機制監控描述符集中刪除 */
    ngx_int_t  (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);

    /* 啓動對某個指定事件的監控 */
    ngx_int_t  (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
    /* 禁用對某個指定事件的監控 */
    ngx_int_t  (*disable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);

    /* 將指定連接所關聯的描述符添加到事件驅動機制監控中 */
    ngx_int_t  (*add_conn)(ngx_connection_t *c);
    /* 將指定連接所關聯的描述符從事件驅動機制監控中刪除 */
    ngx_int_t  (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);

    /* 監控事件是否發生變化,僅用在多線程環境中 */
    ngx_int_t  (*process_changes)(ngx_cycle_t *cycle, ngx_uint_t nowait);
    /* 等待事件的發生,並對事件進行處理 */
    ngx_int_t  (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer,
                   ngx_uint_t flags);

    /* 初始化事件驅動模塊 */
    ngx_int_t  (*init)(ngx_cycle_t *cycle, ngx_msec_t timer);
    /* 在退出事件驅動模塊前調用該函數回收資源 */
    void       (*done)(ngx_cycle_t *cycle);
} ngx_event_actions_t;

事件機制的啓動

事件啓動初始化主要在ngx_event.c文件中,在ngx_event_core_module模塊定義中,實現了兩種方法分別爲 ngx_event_module_init 和ngx_event_process_init 方法。

ngx_event_module_init是在ngx_init_cycle初始化各種參數的時候調用的,在Nginx 啓動過程中沒有使用 fork 出 worker 子進程之前。主要是做一些事件模塊的初始化,共享內存創建和初始化,全局變量的初始化。以上這些初始化全局變量和共享內存都是多worker進程共用的

ngx_event_process_init是在fork出worker進程之後調用的,每個worker進程都會調用,函數主要是初始化當前進程用的post事件隊列,負載均衡鎖(ngx_use_accept_mutex),定時器事件初始化,網絡事件在各個事件模塊進行的初始化,連接池和連接池對應事件的初始化。

事件執行整體流程

ngx_process_events_and_timers是nginx所有事件執行的主體函數,整體邏輯主要是調用三種事件的處理函數以及多進程驚羣現象的處理。
alt 事件執行總體流程
ngx_process_events_and_timers主要在以下四個地方調用:
在這裏插入圖片描述
分別是在ngx_single_process_cycle,ngx_worker_process_cycle,ngx_cache_manager_process_handler,ngx_privileged_agent_process_cycle
單進程、worker進程、cache進程、agent進程,這些進程其實就是在一直循環執行當前這個事件處理函數。

不同的事件對應不同的事件處理函數以下是三類事件的處理函數:

事件類型 事件執行主體函數
定時器事件 ngx_event_expire_timers
網絡 事件 ngx_process_events
post事件 ngx_event_process_posted

其中ngx_process_events對應各自事件模塊的執行函數,ngx_select_process_events, ngx_epoll_process_events等

void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ngx_uint_t  flags;
    ngx_msec_t  timer, delta;

    /* ngx_timer_resolution 主要用來減少事件觸發的頻率 */
    if (ngx_timer_resolution) {
        timer = NGX_TIMER_INFINITE;
        flags = 0;

    } else {
        timer = ngx_event_find_timer();
        flags = NGX_UPDATE_TIME;

#if (NGX_WIN32)

        /* handle signals from master in case of network inactivity */

        if (timer == NGX_TIMER_INFINITE || timer > 500) {
            timer = 500;
        }

#endif
    }
   
   /* 
    * ngx_use_accept_mutex表示是否需要通過對accept加鎖來解決驚羣問題。
    * 當nginx worker進程數>1時且配置文件中打開accept_mutex時,這個標誌置爲1
    */
    if (ngx_use_accept_mutex) {
         /*
          * ngx_accept_disabled 變量是負載均衡閾值,表示進程是否超載;
          * 設置負載均衡閾值爲每個進程最大連接數的八分之一減去空閒連接數; 
          * 即當每個進程accept到的活動連接數超過最大連接數的7/8時,
          * ngx_accept_disabled 大於0,表示該進程處於負載過重;
          *  ngx_accept_disabled = ngx_cycle->connection_n / 8
          *                         - ngx_cycle->free_connection_n;
          * free_connection_n 初始值和connection_n相等,每次新來一個請求free的會減少,斷開釋放會++。
          * 等於0說明當前進程還剩下八分之一的空閒連接,大於0說明已經小於八分之一再大就要超負載了
          */
        if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;

        } else {
            /* 獲取accept鎖,多個worker只有一個能獲取到,非阻塞的過程 ,拿到了就繼續處理當前連接,沒拿到就不處理當前連接
             * 獲取成功的話ngx_accept_mutex_held被置爲1。拿到鎖,意味着監聽句柄被放到本進程的epoll中了,如果沒有拿到鎖,則監聽句柄會被從epoll中取出。
             * /
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }
            /* 拿到鎖的話,置flag爲NGX_POST_EVENTS,這意味着ngx_process_events函數中,任何事件都將延後處理,
             * 會把accept事件都放到ngx_posted_accept_events鏈表中,epollin|epollout事件都放到ngx_posted_events鏈表中
             */
            if (ngx_accept_mutex_held) {
                flags |= NGX_POST_EVENTS;

            } else {
                /* timer 主要是epoll_wait 的等超時時間 */
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    /* accept_mutex_delay配置可以配,在ngx_event_process_init會初始化 */
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }

    delta = ngx_current_msec;

    /* 處理網絡事件 對應各個事件模塊的事件處理函數 */
    (void) ngx_process_events(cycle, timer, flags);

    delta = ngx_current_msec - delta;

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "timer delta: %M", delta);
    /* 處理 accept延遲事件 */
    ngx_event_process_posted(cycle, &ngx_posted_accept_events);

    if (ngx_accept_mutex_held) {
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }

    /* 處理定時器事件,循環遍歷找紅黑樹最左邊的節點時間和當前相比判斷,大於0說明已經超時 */
    if (delta) {
        ngx_event_expire_timers();
    }
    /* 處理普通的事件 */
    ngx_event_process_posted(cycle, &ngx_posted_events);
}

epoll處理事件的整體流程

Level Triggered (LT) 水平觸發
socket接收緩衝區不爲空 有數據可讀 讀事件一直觸發
socket發送緩衝區不滿 可以繼續寫入數據 寫事件一直觸發
符合思維習慣,epoll_wait返回的事件就是socket的狀態

Edge Triggered (ET) 邊沿觸發
socket的接收緩衝區狀態變化時觸發讀事件,即空的接收緩衝區剛接收到數據時觸發讀事件 (讀事件只有開始來的時候,會觸發一次,所以一次需要)
socket的發送緩衝區狀態變化時觸發寫事件,即滿的緩衝區剛空出空間時觸發讀事件

ET的讀事件是 “讀緩衝區從空到非空” 狀態改變的時候 觸發,提示你有數據可以讀
寫事件是 "寫緩衝區從非空到空"狀態改變的時候觸發, 提示你 緩衝區可以寫數據了

LT的讀事件是 “只要讀緩衝區有數據, 就一直觸發讀事件"
寫事件是 ”只要寫緩衝區沒有滿, 就一直觸發寫事件

所以一般採用epoll ET模式情況下,讀事件的時候要一次性讀完緩衝區裏面的數據。
nginx監聽採用的是LT模式,其他採用的是ET模式。水平模式相比邊緣模式來說,頻繁調用觸發會產生更多的系統調用,開銷相比自然就高了

/* 處理已準備就緒的事件 */
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_uint_t         level;
    ngx_err_t          err;
    ngx_event_t       *rev, *wev, **queue;
    ngx_connection_t  *c;

    /* NGX_TIMER_INFINITE == INFTIM */

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "epoll timer: %M", timer);

    /* 調用epoll_wait在規定的timer時間內等待監控的事件準備就緒 */
    events = epoll_wait(ep, event_list, (int) nevents, timer);

    /* 若出錯,設置錯誤編碼 */
    err = (events == -1) ? ngx_errno : 0;

    /*
     * 若沒有設置timer_resolution配置項時,
     * NGX_UPDATE_TIME 標誌表示每次調用epoll_wait函數返回後需要更新時間;
     * 若設置timer_resolution配置項,
     * 則每隔timer_resolution配置項參數會設置ngx_event_timer_alarm爲1,表示需要更新時間;
     */
    if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
        /* 更新時間,將時間緩存到一組全局變量中,方便程序高效獲取事件 */
        ngx_time_update();
    }

    /* 處理epoll_wait的錯誤 */
    if (err) {
        if (err == NGX_EINTR) {

            if (ngx_event_timer_alarm) {
                ngx_event_timer_alarm = 0;
                return NGX_OK;
            }

            level = NGX_LOG_INFO;

        } else {
            level = NGX_LOG_ALERT;
        }

        ngx_log_error(level, cycle->log, err, "epoll_wait() failed");
        return NGX_ERROR;
    }

    /*
     * 若epoll_wait返回的事件數events爲0,則有兩種可能:
     * 1、超時返回,即時間超過timer;
     * 2、在限定的timer時間內返回,此時表示出錯error返回;
     */
    if (events == 0) {
        if (timer != NGX_TIMER_INFINITE) {
            return NGX_OK;
        }

        ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
                      "epoll_wait() returned no events without timeout");
        return NGX_ERROR;
    }

    /* 僅在多線程環境下有效 */
    ngx_mutex_lock(ngx_posted_events_mutex);

    /* 遍歷由epoll_wait返回的所有已準備就緒的事件,並處理這些事件 */
    for (i = 0; i < events; i++) {
        /*
         * 獲取與事件關聯的連接對象;
         * 連接對象地址的最低位保存的是添加事件時設置的事件過期標誌位;
         */
        c = event_list[i].data.ptr;

        /* 獲取事件過期標誌位,即連接對象地址的最低位 */
        instance = (uintptr_t) c & 1;
        /* 屏蔽連接對象的最低位,即獲取連接對象的真正地址 */
        c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);

        /* 獲取讀事件 */
        rev = c->read;

        /*
         * 同一連接的讀寫事件的instance標誌位是相同的;
         * 若fd描述符爲-1,或連接對象讀事件的instance標誌位不相同,則判爲過期事件;
         */
        if (c->fd == -1 || rev->instance != instance) {

            /*
             * the stale event from a file descriptor
             * that was just closed in this iteration
             */

            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                           "epoll: stale event %p", c);
            continue;
        }

        /* 獲取連接對象中已準備就緒的事件類型 */
        revents = event_list[i].events;

        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "epoll: fd:%d ev:%04XD d:%p",
                       c->fd, revents, event_list[i].data.ptr);
        /* 記錄epoll_wait的錯誤返回狀態 */
        /*
         * EPOLLERR表示連接出錯;EPOLLHUP表示收到RST報文;
         * 檢測到上面這兩種錯誤時,TCP連接中可能存在未讀取的數據;
         */
        if (revents & (EPOLLERR|EPOLLHUP)) {
            ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                           "epoll_wait() error on fd:%d ev:%04XD",
                           c->fd, revents);
        }

#if 0
        if (revents & ~(EPOLLIN|EPOLLOUT|EPOLLERR|EPOLLHUP)) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
                          "strange epoll_wait() events fd:%d ev:%04XD",
                          c->fd, revents);
        }
#endif

        /*
         * 若連接發生錯誤且未設置EPOLLIN、EPOLLOUT,
         * 則將EPOLLIN、EPOLLOUT添加到revents中;
         * 即在調用讀寫事件時能夠處理連接的錯誤;
         */
        if ((revents & (EPOLLERR|EPOLLHUP))
             && (revents & (EPOLLIN|EPOLLOUT)) == 0)
        {
            /*
             * if the error events were returned without EPOLLIN or EPOLLOUT,
             * then add these flags to handle the events at least in one
             * active handler
             */

            revents |= EPOLLIN|EPOLLOUT;
        }

        /* 連接有可讀事件,且該讀事件是active活躍的 */
        if ((revents & EPOLLIN) && rev->active) {

#if (NGX_HAVE_EPOLLRDHUP)
            /* EPOLLRDHUP表示連接對端關閉了讀取端 */
            if (revents & EPOLLRDHUP) {
                rev->pending_eof = 1;
            }
#endif

            if ((flags & NGX_POST_THREAD_EVENTS) && !rev->accept) {
                rev->posted_ready = 1;

            } else {
                /* 讀事件已準備就緒 */
                /*
                 * 這裏要區分active與ready:
                 * active是指事件被添加到epoll對象的監控中,
                 * 而ready表示被監控的事件已經準備就緒,即可以對其進程IO處理;
                 */
                rev->ready = 1;
            }

            /*
             * NGX_POST_EVENTS表示已準備就緒的事件需要延遲處理,
             * 根據accept標誌位將事件加入到相應的隊列中;
             */
            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;

        /* 連接有可寫事件,且該寫事件是active活躍的 */
        if ((revents & EPOLLOUT) && wev->active) {

            /* 檢查寫事件是否過期 */
            if (c->fd == -1 || wev->instance != instance) {

                /*
                 * the stale event from a file descriptor
                 * that was just closed in this iteration
                 */

                ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                               "epoll: stale event %p", c);
                continue;
            }

            if (flags & NGX_POST_THREAD_EVENTS) {
                wev->posted_ready = 1;

            } else {

                /* 寫事件已準備就緒 */
                wev->ready = 1;
            }

            /*
             * NGX_POST_EVENTS表示已準備就緒的事件需要延遲處理,
             * 根據accept標誌位將事件加入到相應的隊列中;
             */
            if (flags & NGX_POST_EVENTS) {
                ngx_locked_post_event(wev, &ngx_posted_events);

            } else {
                /* 若不延遲處理,則直接調用事件的處理函數 */
                wev->handler(wev);
            }
        }
    }

    ngx_mutex_unlock(ngx_posted_events_mutex);

    return NGX_OK;
}

定時器事件處理函數

函數整體邏輯主要是從紅黑樹中找出超時的事件進行處理,循環從樹中找出最左邊的節點和當前時間的差,比當前小說明已經超時開始處理

void
ngx_event_expire_timers(void)
{
    ngx_event_t        *ev;
    ngx_rbtree_node_t  *node, *root, *sentinel;

    sentinel = ngx_event_timer_rbtree.sentinel;

    /* 循環檢查 */
    for ( ;; ) {

        ngx_mutex_lock(ngx_event_timer_mutex);

        root = ngx_event_timer_rbtree.root;

        /* 若定時器紅黑樹爲空,則直接返回,不做任何處理 */
        if (root == sentinel) {
            return;
        }

        /* 找出定時器紅黑樹最左邊的節點,即最小的節點,同時也是最有可能超時的事件對象 */
        node = ngx_rbtree_min(root, sentinel);

        /* node->key <= ngx_current_time */

        /* 若檢查到的當前事件已超時 */
        if ((ngx_msec_int_t) (node->key - ngx_current_msec) <= 0) {
            /* 獲取超時的具體事件 */
            ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer));

            /* 下面是針對多線程 */
#if (NGX_THREADS)

            if (ngx_threaded && ngx_trylock(ev->lock) == 0) {
                ......
                break;
            }
#endif

            /* 將已超時事件對象從現有定時器紅黑樹中移除 */
            ngx_rbtree_delete(&ngx_event_timer_rbtree, &ev->timer);

            ngx_mutex_unlock(ngx_event_timer_mutex);

            /* 設置事件的在定時器紅黑樹中的監控標誌位 */
            ev->timer_set = 0;/* 0表示不受監控 */

            /* 多線程環境 */
#if (NGX_THREADS)
            if (ngx_threaded) {
                ev->posted_timedout = 1;

                ngx_post_event(ev, &ngx_posted_events);

                ngx_unlock(ev->lock);

                continue;
            }
#endif

            /* 設置事件的超時標誌位 */
            ev->timedout = 1;/* 1表示已經超時 */

            /* 調用已超時事件的處理函數對該事件進行處理 */
            ev->handler(ev);

            continue;
        }

        break;
    }

    ngx_mutex_unlock(ngx_event_timer_mutex);
}

隊列事件

隊列事件主要是accept事件隊列ngx_posted_accept_events 和 普通事件隊列ngx_posted_events

/* 函數主要邏輯是從隊列中*/
void
ngx_event_process_posted(ngx_cycle_t *cycle, ngx_queue_t *posted)
{
    ngx_queue_t  *q;
    ngx_event_t  *ev;

    /* 判斷隊列是否爲null */
    while (!ngx_queue_empty(posted)) {

        q = ngx_queue_head(posted);
        ev = ngx_queue_data(q, ngx_event_t, queue);

        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                      "posted event %p", ev);

        /* 將對當前事件從隊列中刪除 */
        ngx_delete_posted_event(ev);

        /* 處理當前事件的handler邏輯 */
        ev->handler(ev);
    }
}

事件連接處理模塊可以參考着篇文章:https://www.kancloud.cn/digest/understandingnginx/202603

參考資料:
https://blog.csdn.net/russell_tao/article/details/7204260
http://yikun.github.io/2014/03/26/nginxevent/
https://www.kancloud.cn/digest/understandingnginx/202602

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