1. 程式人生 > >Nginx學習之路(八)Nginx中的事件驅動過程詳解-----以listenfd註冊過程為例

Nginx學習之路(八)Nginx中的事件驅動過程詳解-----以listenfd註冊過程為例

Nginx的高效得益於它的事件驅動機制,整個事件驅動機制基本框架就是linux下的select,poll,epoll這幾個IO多路複用模式,但是nginx絕不單單只是使用它們這麼簡單,今天以epoll模式為例,從nginx最開始的listenfd的監聽的過程來說明nginx是怎麼實現的事件驅動。

首先需要說明的是,整個事件模型(event)是一個模組(module),module在nginx中是一個很重要的概念,這也是nginx牛B的地方之一,模組帶來的好處就是非常非常棒的水平擴充套件能力,在nginx上做二次開發基本也是模組的編寫,下面我們就來分析下event module:

先來認識下ngx_module_t這個結構體

struct ngx_module_s {  
    ngx_uint_t            ctx_index;      
    /*分類的模組計數器 
    nginx模組可以分為四種:core、event、http和mail 
    每個模組都會各自計數,ctx_index就是每個模組在其所屬類組的計數*/  
      
    ngx_uint_t            index;          
    /*一個模組計數器,按照每個模組在ngx_modules[]陣列中的宣告順序,從0開始依次給每個模組賦值*/  
  
    ngx_uint_t            spare0;  
    ngx_uint_t            spare1;  
    ngx_uint_t            spare2;  
    ngx_uint_t            spare3;  
  
    ngx_uint_t            version;      //nginx模組版本  
  
    void                 *ctx;            
    /*模組的上下文,不同種類的模組有不同的上下文,因此實現了四種結構體*/  
      
    ngx_command_t        *commands;  
    /*命令定義地址 
    模組的指令集 
    每一個指令在原始碼中對應著一個ngx_command_t結構變數*/  
      
    ngx_uint_t            type;         //模組型別,用於區分core event http和mail  
  
    ngx_int_t           (*init_master)(ngx_log_t *log);         //初始化master時執行  
  
    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);     //初始化module時執行  
  
    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);    //初始化process時執行  
    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);     //初始化thread時執行  
    void                (*exit_thread)(ngx_cycle_t *cycle);     //退出thread時執行  
    void                (*exit_process)(ngx_cycle_t *cycle);    //退出process時執行  
  
    void                (*exit_master)(ngx_cycle_t *cycle);     //退出master時執行  
  
//空閒的鉤子函式 
    uintptr_t             spare_hook0;  
    uintptr_t             spare_hook1;  
    uintptr_t             spare_hook2;  
    uintptr_t             spare_hook3;  
    uintptr_t             spare_hook4;  
    uintptr_t             spare_hook5;  
    uintptr_t             spare_hook6;  
    uintptr_t             spare_hook7;  
};  
  
typedef struct ngx_module_s      ngx_module_t;  

這裡面最重要的就是那幾個回撥函式(init_master到exit_master之間),比如init_master任務,在ngx_module.c中有個全域性變數:

ngx_module_t *ngx_modules[] = {
    &ngx_core_module,
    &ngx_errlog_module,
    &ngx_conf_module,
    &ngx_dso_module,
    &ngx_conf_extend_module,
    &ngx_syslog_module,
...
}

在ngx_worker_process_init中就會從這裡面去呼叫各種module的init_master方法:

   for (i = 0; ngx_modules[i]; i++) {
        if (ngx_modules[i]->init_process) {
            if (ngx_modules[i]->init_process(cycle) == NGX_ERROR) {
                /* fatal */
                exit(2);
            }
        }
    }

那麼如何判定到底採用哪一種IO複用方式呢?

在ngx_event.c中有如下宣告:

extern ngx_module_t ngx_kqueue_module;
extern ngx_module_t ngx_eventport_module;
extern ngx_module_t ngx_devpoll_module;
extern ngx_module_t ngx_epoll_module;
extern ngx_module_t ngx_rtsig_module;
extern ngx_module_t ngx_select_module;

這些宣告以及配置檔案中的相應部分設定用來確定你用哪一個IO複用的模組,那麼事件是如何註冊的呢?

關於事件註冊的最重要的結構體如下:

typedef struct {
    ngx_str_t              *name;

    void                 *(*create_conf)(ngx_cycle_t *cycle);
    char                 *(*init_conf)(ngx_cycle_t *cycle, void *conf);

    ngx_event_actions_t     actions;
} ngx_event_module_t;

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;

我們以epoll為例,來看下epoll的ngx_module_t是怎麼實現的:

ngx_event_module_t  ngx_epoll_module_ctx = {
    &epoll_name,
    ngx_epoll_create_conf,               /* create configuration */
    ngx_epoll_init_conf,                 /* init configuration */

    {
        ngx_epoll_add_event,             /* add an event */
        ngx_epoll_del_event,             /* delete an event */
        ngx_epoll_add_event,             /* enable an event */
        ngx_epoll_del_event,             /* disable an event */
        ngx_epoll_add_connection,        /* add an connection */
        ngx_epoll_del_connection,        /* delete an connection */
        NULL,                            /* process the changes */
        ngx_epoll_process_events,        /* process the events */
        ngx_epoll_init,                  /* init the events */
        ngx_epoll_done,                  /* done the events */
    }
};

那麼,呼叫到這些註冊的方法的過程是怎樣的呢?我們以ngx_epoll_add_event為例,在ngx_worker_process_init的listenfd註冊到epoll的過程如下:

還記得剛剛說到的init_master那個回撥嗎,我們來看看epoll中的ngx_module_t

ngx_module_t  ngx_event_core_module = {
    NGX_MODULE_V1,
    &ngx_event_core_module_ctx,            /* module context */
    ngx_event_core_commands,               /* module directives */
    NGX_EVENT_MODULE,                      /* module type */
    NULL,                                  /* init master */
    ngx_event_module_init,                 /* init module */
    ngx_event_process_init,                /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    NULL,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};

可以看到在之前說明的過程中,會呼叫呼叫ngx_event_process_init函式,在這個函式中

if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
                return NGX_ERROR;
            }

至此就完成了listenfd註冊到epoll的過程,在這裡要說明下這個rev,說到rev就要說明一下nginx中連線(connection)的概念,nginx中註冊到epoll中的epoll_event裡的data部分是指向一個連線的,nginx中是提前分配好了一個連線池,每次需要連線的時候就從連線池裡拿一個出來,連線池的東西我們後面再細講,那麼rev是怎麼跟連線關聯起來的呢,我們來看下rev的結構體:

struct ngx_event_s {
    void            *data;//關注這個指標,這個指標通常都是指向一個連線
.
.
.
    ngx_event_handler_pt  handler;//最需要關注的是這個handler,這個handler就是這個event被呼叫是所呼叫的函式
.
.
.
};
這是個簡化的結構體,我只寫出了要關注的部分,再來看下connection的結構體

struct ngx_connection_s {  
    //連線未使用時候,data域充當連線連結串列中的next指標.  
    //當連線被使用時候,data域的意義由模組而定.  
    void               *data;  
    //連線對應的讀事件,這個read指標就指向了剛剛程式碼中的rev 
    ngx_event_t        *read;  
    //連線對應的寫事件  
    ngx_event_t        *write;  
  
    //套接字控制代碼  
    ngx_socket_t        fd;  
  
    //直接接收網路位元組流的方法  
    ngx_recv_pt         recv;  
    //直接放鬆網路位元組流的方法  
    ngx_send_pt         send;  
    //以ngx_chain連結串列為引數,接收網路位元組流的方法  
    ngx_recv_chain_pt   recv_chain;  
    //以ngx_chain連結串列為引數,傳送網路位元組流的方法  
    ngx_send_chain_pt   send_chain;  
  
    //這個連結對應的listening_t監聽物件.  
    //此連結由ngx_listening_t監聽的事件建立  
    ngx_listening_t    *listening;  
  
    //這個連線已經發送出去的位元組數  
    off_t               sent;  
  
    //記錄日誌  
    ngx_log_t          *log;  
  
    //在accept一個新連線的時候,會建立一個記憶體池,而這個連線結束時候,會銷燬一個記憶體池.  
    //這裡所說的連線是成功建立的tcp連線.記憶體池的大小由pool_size決定  
    //所有的ngx_connect_t結構體都是預分配的  
    ngx_pool_t         *pool;  
  
    //連線客戶端的結構體  
    struct sockaddr    *sockaddr;  
    //連線客戶端的結構體長度  
    socklen_t           socklen;  
    //連線客戶端的ip(字串形式)  
    ngx_str_t           addr_text;  
  
#if (NGX_SSL)  
    ngx_ssl_connection_t  *ssl;  
#endif  
  
    //本機中監聽埠對應的socketaddr結構體  
    //也就是listen監聽物件中的socketaddr成員  
    struct sockaddr    *local_sockaddr;  
  
    //用於接收和快取客戶端發來的字元流  
    ngx_buf_t          *buffer;  
  
    //該欄位表示將該連線以雙向連結串列形式新增到cycle結構體中的  
    //reusable_connections_queen雙向連結串列中,表示可以重用的連線.  
    ngx_queue_t         queue;  
  
    //連線使用次數,每次建立一條來自客戶端的連線,  
    //或者建立一條與後端伺服器的連線,number+1  
    ngx_atomic_uint_t   number;  
  
    //處理請求的次數  
    ngx_uint_t          requests;  
  
    //  
    unsigned            buffered:8;  
  
    //日誌級別  
    unsigned            log_error:3;     /* ngx_connection_log_error_e */  
  
    //不期待字元流結束  
    unsigned            unexpected_eof:1;  
    //連線超時  
    unsigned            timedout:1;  
    //連線處理過程中出現錯誤  
    unsigned            error:1;  
    //標識此連結已經銷燬,記憶體池,套接字等都不可用  
    unsigned            destroyed:1;  
  
    //連線處於空閒狀態  
    unsigned            idle:1;  
    //連線可以重用  
    unsigned            reusable:1;  
    //連線關閉  
    unsigned            close:1;  
  
    //正在將檔案中的資料法網另一端  
    unsigned            sendfile:1;  
    //連線中傳送緩衝區的資料高於低潮,才傳送資料.  
    //與ngx_handle_write_event方法中的lowat相對應  
    unsigned            sndlowat:1;  
    //使用tcp的nodely特性  
    unsigned            tcp_nodelay:2;   /* ngx_connection_tcp_nodelay_e */  
    //使用tcp的nopush特性  
    unsigned            tcp_nopush:2;    /* ngx_connection_tcp_nopush_e */
。
。
。
}

也就是說,connection中有read指標指向了read事件rev,同事rev也有指標指向了對應的connection,這樣就達到了資料相互關聯的作用,至此,整個事件的註冊流程就大致清晰了。