1. 程式人生 > >nginx原始碼閱讀(七).nginx的模組化設計

nginx原始碼閱讀(七).nginx的模組化設計

前言

高度模組化是nginx的一個特點,在正式進入到具體的模組之前,有必要從整體把握各模組之間聯絡與nginx對模組的控制。在前面的分析中其實已經使用了模組中提供的方法,比如worker程序的工作迴圈中呼叫的ngx_process_events_and_timers()函式,用於處理事件。

模組之間的聯絡

官方提供了5個型別的模組:核心模組、配置模組、事件模組、http模組、mail模組。

  • 配置模組主要負責解析nginx.conf檔案,是其他模組的基礎,該類模組中只有一個ngx_conf_module模組;
  • 核心模組主要負責定義除配置模組之外的其他模組,該類模組中有6個核心模組。
    1. ngx_mail_module
      負責定義mail模組;
    2. ngx_http_module負責定義http模組;
    3. ngx_events_module負責定義事件模組;
    4. ngx_core_module則是nginx啟動載入的第一個模組,它主要用來儲存全域性配置項。
    5. ngx_openssl_module只有當載入了之後,nginx才支援https請求
    6. ngx_errlog_module
  • 事件模組即負責事件的註冊、分發處理、銷燬等,該類模組中主要有這幾個模組:
    1. ngx_event_core_module負責載入其他事件模組,是其他事件模組的基礎
    2. ngx_epoll_module,該模組則是我們後面需要重點分析的模組,它負責事件的註冊、整合、處理等
    3. 其他的模組比如ngx_kqueue_module這些我們後面基本不會涉及,就不解釋了,畢竟是另外一種I/O多路複用機制,大致的思想是一樣的
  • http模組和mail模組也無需太多解釋,等到後面再介紹。

從中我們可以看出,核心模組中的部分模組與其他模組有一定的聯絡,比如核心模組中的ngx_event_module模組,它就作為事件模組的基礎,會幫事件模組解析配置項以及儲存事件模組中各模組的儲存配置項的結構體指標。

這樣的設計可以讓核心模組儘量的簡單,只做一些初始化還有退出的工作,而其他的事情交給相應型別的模組去細做即可。

除了核心模組與其他模組的聯絡之外,其實各型別模組中的子模組也有聯絡,同樣也以事件模組為例子,在不同的系平臺上,I/O多路複用機制可能也不同,比如linux2.6之後的epoll、FreeBSD的kqueue等,那麼選擇一種合適的機制就成了一個很重要的問題,事件模組中的ngx_event_core_module

就負責這個工作;又比如http模組中,ngx_http_core_module也負責決定對於不同的請求該選用哪一個http模組來處理。

統一的通用介面

為了將每個模組簡單的統一起來,nginx定義了ngx_module_t結構體作為每個模組的通用介面,同時考慮到靈活性的問題,ngx_module_t裡面只涉及到了模組的初始化還有退出等操作,並且其中的ctx成員是一個void *指標,它可以作為任意一種具體型別的模組中的通用性介面,比如在事件模組中,又可以自己再實現一個通用性介面用於事件模組的統一。

還記得ngx_modules陣列嗎,它的型別就是ngx_module_t,即儲存了所有模組的ngx_module_t介面,通過遍歷該陣列訪問統一的介面,就可以做到初始化以及退出,並且通過ctx可以訪問到具體型別模組的通用性介面(比如使用ctx呼叫核心模組中所有模組的create_conf方法)。

下面則是ngx_module_t的真面目:

typedef struct ngx_module_s       ngx_module_t;


struct ngx_module_s {
    //模組在該類模組中的序號
    ngx_uint_t            ctx_index;
    //所有模組在ngx_modules陣列中的序號
    ngx_uint_t            index;


    ngx_uint_t            spare0;
    ngx_uint_t            spare1;
    ngx_uint_t            spare2;
    ngx_uint_t            spare3;

    //模組版本
    ngx_uint_t            version;

    //該成員一般指向同一型別模組下的通用性介面
    //這樣的設計使得模組之間層次分明
    //並且支援多種不同型別模組擁有自己特點的介面
    //這樣的做法稱為具體化ngx_module_t介面
    void                 *ctx;
    //該陣列指定了模組處理配置項的方法
    ngx_command_t        *commands;
    //模組的型別
    //比如配置模組的則是NGX_CONF_MODULE
    //核心模組的則是NGX_CORE_MODULE
    ngx_uint_t            type;

    //master程序初始化時使用
    //不過我這個版本並沒有使用
    ngx_int_t           (*init_master)(ngx_log_t *log);

    //模組初始化時使用
    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);

    //工作程序初始化時使用
    //在nginx初始化的最後階段會呼叫
    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);

    //初始化/退出執行緒
    //同樣並沒有使用
    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);
    void                (*exit_thread)(ngx_cycle_t *cycle);

    //工作程序退出時呼叫
    void                (*exit_process)(ngx_cycle_t *cycle);

    //master程序退出時呼叫
    void                (*exit_master)(ngx_cycle_t *cycle);

    //預留成員,並沒有使用
    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;
};

為了加深對nginx中模組化的理解,我們再來回顧一下第二小節中分析的nginx的初始化。其實裡面就用到了ngx_core_module模組,只是當時為了方便講解以及不一來就以比較抽象的概念來描述初始化過程所以並沒有提起。

這裡可以以整體架構的角度來考慮nginx的做法,並且再複習一下前面的內容。

ngx_core_module

ngx_module_t只是整體框架提供給各模組的統一介面,但是每種型別的模組的需求並不一樣,比如事件模組中,肯定都需要新增/刪除事件等,而http模組中,則需要合併配置項等。因此,ctx成員的引入,使得不同型別之間的模組可以自己定義自己的介面(根據模組型別的不同具體化ngx_module_t),我們可以通過type的不同,使用ctx成員訪問特定的介面,可能有點抽象,舉個例子,比如核心模組,它的type成員為NGX_CORE_MODULE,我們可以遍歷ngx_modules陣列並只訪問型別為NGX_CORE_MODULE的模組,然後通過ctx呼叫核心模組特有的create_conf以及init_conf方法。

這樣設計的結果就是使得模組之前出現了多層次以及多型別化。配置模組很明顯是所有模組的基礎,而核心模組則是事件模組、http模組、mail模組的基礎,事件模組又是http模組、mail模組的基礎。

下面是ngx_init_cycle函式中涉及到核心模組的部分程式碼:

......
//遍歷所有模組
for (i = 0; ngx_modules[i]; i++) {
    //若非核心模組直接跳過
    if (ngx_modules[i]->type != NGX_CORE_MODULE) {
        continue;
    }

    //獲取到核心模組具體化ngx_module_t之後的結構體指標
    module = ngx_modules[i]->ctx;

    //呼叫核心模組內通用性介面的create_conf方法
    //用於建立儲存配置項的資料結構
    //並且在讀取nginx.conf配置檔案時,會根據ngx_command_t把解析出來的配置項存放在其中
    if (module->create_conf) {
        rv = module->create_conf(cycle);
        if (rv == NULL) {
            ngx_destroy_pool(pool);
            return NULL;
        }
        //conf_ctx是一個四級指標,用於存放指向儲存各模組配置項的結構體指標
        cycle->conf_ctx[ngx_modules[i]->index] = rv;
    }
}
......
//呼叫核心模組中各模組的init_conf
for (i = 0; ngx_modules[i]; i++) {
    //非核心模組直接跳過
    if (ngx_modules[i]->type != NGX_CORE_MODULE) {
        continue;
    }

    module = ngx_modules[i]->ctx;

    if (module->init_conf) {
        /* 通過ctx訪問核心模組中特有的介面
         * 之前已經呼叫過了create_conf方法
         * 因此這裡將使用解析過的配置項來初始化核心模組
         */
        if (module->init_conf(cycle, cycle->conf_ctx[ngx_modules[i]->index])
            == NGX_CONF_ERROR)
        {
            environ = senv;
            ngx_destroy_cycle_pools(&conf);
            return NULL;
        }
    }
}

核心模組的ngx_module_tctx成員指向的具體化的結構體是ngx_core_module_t,定義如下:

typedef struct {
  //核心模組的名稱
  ngx_str_t name;
  /* create_conf和init_conf方法的功能上面已經說過,這裡不做解釋了 */
  void *(*create_conf)(ngx_cycle_t *cycle);
  char *(*init_conf)(ngx_cycle_t *cycle, void *conf);
}

ngx_modules陣列中各模組的順序

綜合以上所講解的,可以輕而易舉的看出各模組在ngx_modules的順序是很重要的。比如ngx_event_module肯定需要在事件模組初始化前先初始化好,因為ngx_event_module的排在ngx_modules陣列中的順序肯定在事件模組中任何一個模組前面,而ngx_event_core_module模組又肯定在事件模組中的其他模組之前,畢竟它要選擇使用哪一種I/O多路複用機制。

如何獲取儲存模組的配置項結構體指標

nginx的核心結構體ngx_cycle_t中有一個四級指標成員conf_ctx,通過它可以獲取任意模組的配置項結構體指標。不過由於它是一個四級指標,可能第一次見理解起來不是那麼順利,這樣去理解可能要好一些:首先conf_ctx是一個數組(一個),它儲存的元素是指標型別(兩個),每個指標又指向了另外的陣列(三個),而每個陣列又儲存的是指標(四個)。這就是為什麼conf_ctx是四級指標。

conf_ctx的第一級指標(陣列),儲存的其實是所有核心模組的配置結構體指標,以事件模組為例,通過該指標,就可以得到任意事件模組的配置項結構體指標(ngx_event_module會將所有事件模組的配置項結構體指標形成一個數組,然後ngx_module_t中的ctx_index成員作為陣列下標)。

這樣我們就可以傳入想獲取配置項結構體指標的模組名,然後通過ngx_module_t中的index還有ctx_index編號來獲取配置項結構體指標。

小結

本小節中,主要從整體的角度來分析了模組之間的聯絡以及nginx對其的高度抽象。除了少量框架程式碼之外,其餘的全都是模組。

有了對模組的整體認識,有助於我們對事件模組進行分析。