Nginx 基本數(shù)據(jù)結(jié)構(gòu)

2022-03-23 15:38 更新

Nginx 的作者為追求極致的高效,自己實(shí)現(xiàn)了很多頗具特色的 Nginx 風(fēng)格的數(shù)據(jù)結(jié)構(gòu)以及公共函數(shù)。比如,Nginx 提供了帶長(zhǎng)度的字符串,根據(jù)編譯器選項(xiàng)優(yōu)化過(guò)的字符串拷貝函數(shù) ngx_copy 等。所以,在我們寫 Nginx 模塊時(shí),應(yīng)該盡量調(diào)用 Nginx 提供的 api,盡管有些 api 只是對(duì) glibc 的宏定義。本節(jié),我們介紹 string、list、buffer、chain 等一系列最基本的數(shù)據(jù)結(jié)構(gòu)及相關(guān)api的使用技巧以及注意事項(xiàng)。

ngx_str_t

在 Nginx 源碼目錄的 src/core 下面的 ngx_string.h|c 里面,包含了字符串的封裝以及字符串相關(guān)操作的 api。Nginx 提供了一個(gè)帶長(zhǎng)度的字符串結(jié)構(gòu) ngx_str_t,它的原型如下:

    typedef struct {
        size_t      len;
        u_char     *data;
    } ngx_str_t;

在結(jié)構(gòu)體當(dāng)中,data 指向字符串?dāng)?shù)據(jù)的第一個(gè)字符,字符串的結(jié)束用長(zhǎng)度來(lái)表示,而不是由'\\0'來(lái)表示結(jié)束。所以,在寫 Nginx 代碼時(shí),處理字符串的方法跟我們平時(shí)使用有很大的不一樣,但要時(shí)刻記住,字符串不以'\\0'結(jié)束,盡量使用 Nginx 提供的字符串操作的 api 來(lái)操作字符串。

那么,Nginx 這樣做有什么好處呢?首先,通過(guò)長(zhǎng)度來(lái)表示字符串長(zhǎng)度,減少計(jì)算字符串長(zhǎng)度的次數(shù)。其次,Nginx 可以重復(fù)引用一段字符串內(nèi)存,data 可以指向任意內(nèi)存,長(zhǎng)度表示結(jié)束,而不用去 copy 一份自己的字符串(因?yàn)槿绻?code>'\\0'結(jié)束,而不能更改原字符串,所以勢(shì)必要 copy 一段字符串)。我們?cè)?ngx_http_request_t 結(jié)構(gòu)體的成員中,可以找到很多字符串引用一段內(nèi)存的例子,比如 request_line、uri、args 等等,這些字符串的 data 部分,都是指向在接收數(shù)據(jù)時(shí)創(chuàng)建 buffer 所指向的內(nèi)存中,uri,args 就沒(méi)有必要 copy 一份出來(lái)。這樣的話,減少了很多不必要的內(nèi)存分配與拷貝。

正是基于此特性,在 Nginx 中,必須謹(jǐn)慎的去修改一個(gè)字符串。在修改字符串時(shí)需要認(rèn)真的去考慮:是否可以修改該字符串;字符串修改后,是否會(huì)對(duì)其它的引用造成影響。在后面介紹 ngx_unescape_uri 函數(shù)的時(shí)候,就會(huì)看到這一點(diǎn)。但是,使用 Nginx 的字符串會(huì)產(chǎn)生一些問(wèn)題,glibc 提供的很多系統(tǒng) api 函數(shù)大多是通過(guò)'\\0'來(lái)表示字符串的結(jié)束,所以我們?cè)谡{(diào)用系統(tǒng) api 時(shí),就不能直接傳入 str->data 了。此時(shí),通常的做法是創(chuàng)建一段 str->len + 1 大小的內(nèi)存,然后 copy 字符串,最后一個(gè)字節(jié)置為'\\0'。比較 hack 的做法是,將字符串最后一個(gè)字符的后一個(gè)字符 backup 一個(gè),然后設(shè)置為'\\0',在做完調(diào)用后,再由 backup 改回來(lái),但前提條件是,你得確定這個(gè)字符是可以修改的,而且是有內(nèi)存分配,不會(huì)越界,但一般不建議這么做。接下來(lái),看看 Nginx 提供的操作字符串相關(guān)的 api。

    #define ngx_string(str)     { sizeof(str) - 1, (u_char *) str }

ngx_string(str) 是一個(gè)宏,它通過(guò)一個(gè)以'\\0'結(jié)尾的普通字符串 str 構(gòu)造一個(gè) Nginx 的字符串,鑒于其中采用 sizeof 操作符計(jì)算字符串長(zhǎng)度,因此參數(shù)必須是一個(gè)常量字符串。

    #define ngx_null_string     { 0, NULL }

定義變量時(shí),使用 ngx_null_string 初始化字符串為空字符串,符串的長(zhǎng)度為 0,data 為 NULL。

    #define ngx_str_set(str, text)                                               \
        (str)->len = sizeof(text) - 1; (str)->data = (u_char *) text

ngx_str_set 用于設(shè)置字符串 str 為 text,由于使用 sizeof 計(jì)算長(zhǎng)度,故 text 必須為常量字符串。

    #define ngx_str_null(str)   (str)->len = 0; (str)->data = NULL

ngx_str_null 用于設(shè)置字符串 str 為空串,長(zhǎng)度為 0,data 為 NULL。

上面這四個(gè)函數(shù),使用時(shí)一定要小心,ngx_string 與 ngx_null_string 是“{,}”格式的,故只能用于賦值時(shí)初始化,如:

    ngx_str_t str = ngx_string("hello world");
    ngx_str_t str1 = ngx_null_string;

如果向下面這樣使用,就會(huì)有問(wèn)題,這里涉及到c語(yǔ)言中對(duì)結(jié)構(gòu)體變量賦值操作的語(yǔ)法規(guī)則,在此不做介紹。

    ngx_str_t str, str1;
    str = ngx_string("hello world");    // 編譯出錯(cuò)
    str1 = ngx_null_string;                // 編譯出錯(cuò)

這種情況,可以調(diào)用 ngx_str_set 與 ngx_str_null 這兩個(gè)函數(shù)來(lái)做:

    ngx_str_t str, str1;
    ngx_str_set(&str, "hello world");    
    ngx_str_null(&str1);

按照 C99 標(biāo)準(zhǔn),您也可以這么做:

    ngx_str_t str, str1;
    str  = (ngx_str_t) ngx_string("hello world");
    str1 = (ngx_str_t) ngx_null_string;

另外要注意的是,ngx_string 與 ngx_str_set 在調(diào)用時(shí),傳進(jìn)去的字符串一定是常量字符串,否則會(huì)得到意想不到的錯(cuò)誤(因?yàn)?ngx_str_set 內(nèi)部使用了 sizeof(),如果傳入的是 u_char*,那么計(jì)算的是這個(gè)指針的長(zhǎng)度,而不是字符串的長(zhǎng)度)。如:

   ngx_str_t str;
   u_char *a = "hello world";
   ngx_str_set(&str, a);    // 問(wèn)題產(chǎn)生

此外,值得注意的是,由于 ngx_str_set 與 ngx_str_null 實(shí)際上是兩行語(yǔ)句,故在 if/for/while 等語(yǔ)句中單獨(dú)使用需要用花括號(hào)括起來(lái),例如:

   ngx_str_t str;
   if (cond)
      ngx_str_set(&str, "true");     // 問(wèn)題產(chǎn)生
   else
      ngx_str_set(&str, "false");    // 問(wèn)題產(chǎn)生
   void ngx_strlow(u_char *dst, u_char *src, size_t n);

將 src 的前 n 個(gè)字符轉(zhuǎn)換成小寫存放在 dst 字符串當(dāng)中,調(diào)用者需要保證 dst 指向的空間大于等于n,且指向的空間必須可寫。操作不會(huì)對(duì)原字符串產(chǎn)生變動(dòng)。如要更改原字符串,可以:

    ngx_strlow(str->data, str->data, str->len);
    ngx_strncmp(s1, s2, n)

區(qū)分大小寫的字符串比較,只比較前n個(gè)字符。

    ngx_strcmp(s1, s2)

區(qū)分大小寫的不帶長(zhǎng)度的字符串比較。

    ngx_int_t ngx_strcasecmp(u_char *s1, u_char *s2);

不區(qū)分大小寫的不帶長(zhǎng)度的字符串比較。

    ngx_int_t ngx_strncasecmp(u_char *s1, u_char *s2, size_t n);

不區(qū)分大小寫的帶長(zhǎng)度的字符串比較,只比較前 n 個(gè)字符。

u_char * ngx_cdecl ngx_sprintf(u_char *buf, const char *fmt, ...);
u_char * ngx_cdecl ngx_snprintf(u_char *buf, size_t max, const char *fmt, ...);
u_char * ngx_cdecl ngx_slprintf(u_char *buf, u_char *last, const char *fmt, ...);

上面這三個(gè)函數(shù)用于字符串格式化,ngx_snprintf 的第二個(gè)參數(shù) max 指明 buf 的空間大小,ngx_slprintf 則通過(guò) last 來(lái)指明 buf 空間的大小。推薦使用第二個(gè)或第三個(gè)函數(shù)來(lái)格式化字符串,ngx_sprintf 函數(shù)還是比較危險(xiǎn)的,容易產(chǎn)生緩沖區(qū)溢出漏洞。在這一系列函數(shù)中,Nginx 在兼容 glibc 中格式化字符串的形式之外,還添加了一些方便格式化 Nginx 類型的一些轉(zhuǎn)義字符,比如%V用于格式化 ngx_str_t 結(jié)構(gòu)。在 Nginx 源文件的 ngx_string.c 中有說(shuō)明:

    /*
     * supported formats:
     *    %[0][width][x][X]O        off_t
     *    %[0][width]T              time_t
     *    %[0][width][u][x|X]z      ssize_t/size_t
     *    %[0][width][u][x|X]d      int/u_int
     *    %[0][width][u][x|X]l      long
     *    %[0][width|m][u][x|X]i    ngx_int_t/ngx_uint_t
     *    %[0][width][u][x|X]D      int32_t/uint32_t
     *    %[0][width][u][x|X]L      int64_t/uint64_t
     *    %[0][width|m][u][x|X]A    ngx_atomic_int_t/ngx_atomic_uint_t
     *    %[0][width][.width]f      double, max valid number fits to %18.15f
     *    %P                        ngx_pid_t
     *    %M                        ngx_msec_t
     *    %r                        rlim_t
     *    %p                        void *
     *    %V                        ngx_str_t *
     *    %v                        ngx_variable_value_t *
     *    %s                        null-terminated string
     *    %*s                       length and string
     *    %Z                        '\0'
     *    %N                        '\n'
     *    %c                        char
     *    %%                        %
     *
     *  reserved:
     *    %t                        ptrdiff_t
     *    %S                        null-terminated wchar string
     *    %C                        wchar
     */

這里特別要提醒的是,我們最常用于格式化 ngx_str_t 結(jié)構(gòu),其對(duì)應(yīng)的轉(zhuǎn)義符是%V,傳給函數(shù)的一定要是指針類型,否則程序就會(huì) coredump 掉。這也是我們最容易犯的錯(cuò)。比如:

    ngx_str_t str = ngx_string("hello world");
    u_char buffer[1024];
    ngx_snprintf(buffer, 1024, "%V", &str);    // 注意,str取地址
    void ngx_encode_base64(ngx_str_t *dst, ngx_str_t *src);
    ngx_int_t ngx_decode_base64(ngx_str_t *dst, ngx_str_t *src);

這兩個(gè)函數(shù)用于對(duì) str 進(jìn)行 base64 編碼與解碼,調(diào)用前,需要保證 dst 中有足夠的空間來(lái)存放結(jié)果,如果不知道具體大小,可先調(diào)用 ngx_base64_encoded_length 與 ngx_base64_decoded_length 來(lái)預(yù)估最大占用空間。

    uintptr_t ngx_escape_uri(u_char *dst, u_char *src, size_t size,
        ngx_uint_t type);

對(duì) src 進(jìn)行編碼,根據(jù) type 來(lái)按不同的方式進(jìn)行編碼,如果 dst 為 NULL,則返回需要轉(zhuǎn)義的字符的數(shù)量,由此可得到需要的空間大小。type 的類型可以是:

    #define NGX_ESCAPE_URI         0
    #define NGX_ESCAPE_ARGS        1
    #define NGX_ESCAPE_HTML        2
    #define NGX_ESCAPE_REFRESH     3
    #define NGX_ESCAPE_MEMCACHED   4
    #define NGX_ESCAPE_MAIL_AUTH   5
    void ngx_unescape_uri(u_char **dst, u_char **src, size_t size, ngx_uint_t type);

對(duì) src 進(jìn)行反編碼,type 可以是 0、NGX_UNESCAPE_URI、NGX_UNESCAPE_REDIRECT 這三個(gè)值。如果是 0,則表示 src 中的所有字符都要進(jìn)行轉(zhuǎn)碼。如果是 NGX_UNESCAPE_URI 與 NGX_UNESCAPE_REDIRECT,則遇到'?'后就結(jié)束了,后面的字符就不管了。而 NGX_UNESCAPE_URI 與 NGX_UNESCAPE_REDIRECT 之間的區(qū)別是 NGX_UNESCAPE_URI 對(duì)于遇到的需要轉(zhuǎn)碼的字符,都會(huì)轉(zhuǎn)碼,而 NGX_UNESCAPE_REDIRECT 則只會(huì)對(duì)非可見(jiàn)字符進(jìn)行轉(zhuǎn)碼。

    uintptr_t ngx_escape_html(u_char *dst, u_char *src, size_t size);

對(duì) html 標(biāo)簽進(jìn)行編碼。

當(dāng)然,我這里只介紹了一些常用的 api 的使用,大家可以先熟悉一下,在實(shí)際使用過(guò)程中,遇到不明白的,最快最直接的方法就是去看源碼,看 api 的實(shí)現(xiàn)或看 Nginx 自身調(diào)用 api 的地方是怎么做的,代碼就是最好的文檔。

ngx_pool_t

ngx_pool_t是一個(gè)非常重要的數(shù)據(jù)結(jié)構(gòu),在很多重要的場(chǎng)合都有使用,很多重要的數(shù)據(jù)結(jié)構(gòu)也都在使用它。那么它究竟是一個(gè)什么東西呢?簡(jiǎn)單的說(shuō),它提供了一種機(jī)制,幫助管理一系列的資源(如內(nèi)存,文件等),使得對(duì)這些資源的使用和釋放統(tǒng)一進(jìn)行,免除了使用過(guò)程中考慮到對(duì)各種各樣資源的什么時(shí)候釋放,是否遺漏了釋放的擔(dān)心。

例如對(duì)于內(nèi)存的管理,如果我們需要使用內(nèi)存,那么總是從一個(gè) ngx_pool_t 的對(duì)象中獲取內(nèi)存,在最終的某個(gè)時(shí)刻,我們銷毀這個(gè) ngx_pool_t 對(duì)象,所有這些內(nèi)存都被釋放了。這樣我們就不必要對(duì)對(duì)這些內(nèi)存進(jìn)行 malloc 和 free 的操作,不用擔(dān)心是否某塊被malloc出來(lái)的內(nèi)存沒(méi)有被釋放。因?yàn)楫?dāng) ngx_pool_t 對(duì)象被銷毀的時(shí)候,所有從這個(gè)對(duì)象中分配出來(lái)的內(nèi)存都會(huì)被統(tǒng)一釋放掉。

再比如我們要使用一系列的文件,但是我們打開(kāi)以后,最終需要都關(guān)閉,那么我們就把這些文件統(tǒng)一登記到一個(gè) ngx_pool_t 對(duì)象中,當(dāng)這個(gè) ngx_pool_t 對(duì)象被銷毀的時(shí)候,所有這些文件都將會(huì)被關(guān)閉。

從上面舉的兩個(gè)例子中我們可以看出,使用 ngx_pool_t 這個(gè)數(shù)據(jù)結(jié)構(gòu)的時(shí)候,所有的資源的釋放都在這個(gè)對(duì)象被銷毀的時(shí)刻,統(tǒng)一進(jìn)行了釋放,那么就會(huì)帶來(lái)一個(gè)問(wèn)題,就是這些資源的生存周期(或者說(shuō)被占用的時(shí)間)是跟 ngx_pool_t 的生存周期基本一致(ngx_pool_t 也提供了少量操作可以提前釋放資源)。從最高效的角度來(lái)說(shuō),這并不是最好的。比如,我們需要依次使用 A,B,C 三個(gè)資源,且使用完 B 的時(shí)候,A 就不會(huì)再被使用了,使用C的時(shí)候 A 和 B 都不會(huì)被使用到。如果不使用 ngx_pool_t 來(lái)管理這三個(gè)資源,那我們可能從系統(tǒng)里面申請(qǐng) A,使用 A,然后在釋放 A。接著申請(qǐng) B,使用 B,再釋放 B。最后申請(qǐng) C,使用 C,然后釋放 C。但是當(dāng)我們使用一個(gè) ngx_pool_t 對(duì)象來(lái)管理這三個(gè)資源的時(shí)候,A,B 和 C 的釋放是在最后一起發(fā)生的,也就是在使用完 C 以后。誠(chéng)然,這在客觀上增加了程序在一段時(shí)間的資源使用量。但是這也減輕了程序員分別管理三個(gè)資源的生命周期的工作。這也就是有所得,必有所失的道理。實(shí)際上是一個(gè)取舍的問(wèn)題,要看在具體的情況下,你更在乎的是哪個(gè)。

可以看一下在 Nginx 里面一個(gè)典型的使用 ngx_pool_t 的場(chǎng)景,對(duì)于 Nginx 處理的每個(gè) http request, Nginx 會(huì)生成一個(gè) ngx_pool_t 對(duì)象與這個(gè) http request 關(guān)聯(lián),所有處理過(guò)程中需要申請(qǐng)的資源都從這個(gè) ngx_pool_t 對(duì)象中獲取,當(dāng)這個(gè) http request 處理完成以后,所有在處理過(guò)程中申請(qǐng)的資源,都將隨著這個(gè)關(guān)聯(lián)的 ngx_pool_t 對(duì)象的銷毀而釋放。

ngx_pool_t 相關(guān)結(jié)構(gòu)及操作被定義在文件src/core/ngx_palloc.h|c中。

    typedef struct ngx_pool_s        ngx_pool_t; 

    struct ngx_pool_s {
        ngx_pool_data_t       d;
        size_t                max;
        ngx_pool_t           *current;
        ngx_chain_t          *chain;
        ngx_pool_large_t     *large;
        ngx_pool_cleanup_t   *cleanup;
        ngx_log_t            *log;
    };

從 ngx_pool_t 的一般使用者的角度來(lái)說(shuō),可不用關(guān)注 ngx_pool_t 結(jié)構(gòu)中各字段作用。所以這里也不會(huì)進(jìn)行詳細(xì)的解釋,當(dāng)然在說(shuō)明某些操作函數(shù)的使用的時(shí)候,如有必要,會(huì)進(jìn)行說(shuō)明。

下面我們來(lái)分別解釋下 ngx_pool_t 的相關(guān)操作。

    ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);

創(chuàng)建一個(gè)初始節(jié)點(diǎn)大小為 size 的 pool,log 為后續(xù)在該 pool 上進(jìn)行操作時(shí)輸出日志的對(duì)象。 需要說(shuō)明的是 size 的選擇,size 的大小必須小于等于 NGX_MAX_ALLOC_FROM_POOL,且必須大于 sizeof(ngx_pool_t)。

選擇大于 NGX_MAX_ALLOC_FROM_POOL 的值會(huì)造成浪費(fèi),因?yàn)榇笥谠撓拗频目臻g不會(huì)被用到(只是說(shuō)在第一個(gè)由 ngx_pool_t 對(duì)象管理的內(nèi)存塊上的內(nèi)存,后續(xù)的分配如果第一個(gè)內(nèi)存塊上的空閑部分已用完,會(huì)再分配的)。

選擇小于 sizeof(ngx_pool_t)的值會(huì)造成程序崩潰。由于初始大小的內(nèi)存塊中要用一部分來(lái)存儲(chǔ) ngx_pool_t 這個(gè)信息本身。

當(dāng)一個(gè) ngx_pool_t 對(duì)象被創(chuàng)建以后,該對(duì)象的 max 字段被賦值為 size-sizeof(ngx_pool_t)和 NGX_MAX_ALLOC_FROM_POOL 這兩者中比較小的。后續(xù)的從這個(gè) pool 中分配的內(nèi)存塊,在第一塊內(nèi)存使用完成以后,如果要繼續(xù)分配的話,就需要繼續(xù)從操作系統(tǒng)申請(qǐng)內(nèi)存。當(dāng)內(nèi)存的大小小于等于 max 字段的時(shí)候,則分配新的內(nèi)存塊,鏈接在 d 這個(gè)字段(實(shí)際上是 d.next 字段)管理的一條鏈表上。當(dāng)要分配的內(nèi)存塊是比 max 大的,那么從系統(tǒng)中申請(qǐng)的內(nèi)存是被掛接在 large 字段管理的一條鏈表上。我們暫且把這個(gè)稱之為大塊內(nèi)存鏈和小塊內(nèi)存鏈。

    void *ngx_palloc(ngx_pool_t *pool, size_t size); 

從這個(gè) pool 中分配一塊為 size 大小的內(nèi)存。注意,此函數(shù)分配的內(nèi)存的起始地址按照 NGX_ALIGNMENT 進(jìn)行了對(duì)齊。對(duì)齊操作會(huì)提高系統(tǒng)處理的速度,但會(huì)造成少量?jī)?nèi)存的浪費(fèi)。


    void *ngx_pnalloc(ngx_pool_t *pool, size_t size); 

從這個(gè) pool 中分配一塊為 size 大小的內(nèi)存。但是此函數(shù)分配的內(nèi)存并沒(méi)有像上面的函數(shù)那樣進(jìn)行過(guò)對(duì)齊。

.. code:: c

void *ngx_pcalloc(ngx_pool_t *pool, size_t size);

該函數(shù)也是分配size大小的內(nèi)存,并且對(duì)分配的內(nèi)存塊進(jìn)行了清零。內(nèi)部實(shí)際上是轉(zhuǎn)調(diào)用ngx_palloc實(shí)現(xiàn)的。


    void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);

按照指定對(duì)齊大小 alignment 來(lái)申請(qǐng)一塊大小為 size 的內(nèi)存。此處獲取的內(nèi)存不管大小都將被置于大內(nèi)存塊鏈中管理。

    ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);

對(duì)于被置于大塊內(nèi)存鏈,也就是被 large 字段管理的一列內(nèi)存中的某塊進(jìn)行釋放。該函數(shù)的實(shí)現(xiàn)是順序遍歷 large 管理的大塊內(nèi)存鏈表。所以效率比較低下。如果在這個(gè)鏈表中找到了這塊內(nèi)存,則釋放,并返回 NGX_OK。否則返回 NGX_DECLINED。

由于這個(gè)操作效率比較低下,除非必要,也就是說(shuō)這塊內(nèi)存非常大,確應(yīng)及時(shí)釋放,否則一般不需要調(diào)用。反正內(nèi)存在這個(gè) pool 被銷毀的時(shí)候,總歸會(huì)都釋放掉的嘛!

    ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size); 

ngx_pool_t 中的 cleanup 字段管理著一個(gè)特殊的鏈表,該鏈表的每一項(xiàng)都記錄著一個(gè)特殊的需要釋放的資源。對(duì)于這個(gè)鏈表中每個(gè)節(jié)點(diǎn)所包含的資源如何去釋放,是自說(shuō)明的。這也就提供了非常大的靈活性。意味著,ngx_pool_t 不僅僅可以管理內(nèi)存,通過(guò)這個(gè)機(jī)制,也可以管理任何需要釋放的資源,例如,關(guān)閉文件,或者刪除文件等等。下面我們看一下這個(gè)鏈表每個(gè)節(jié)點(diǎn)的類型:

    typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;
    typedef void (*ngx_pool_cleanup_pt)(void *data);

    struct ngx_pool_cleanup_s {
        ngx_pool_cleanup_pt   handler;
        void                 *data;
        ngx_pool_cleanup_t   *next;
    };
  • data: 指明了該節(jié)點(diǎn)所對(duì)應(yīng)的資源。

  • handler: 是一個(gè)函數(shù)指針,指向一個(gè)可以釋放 data 所對(duì)應(yīng)資源的函數(shù)。該函數(shù)只有一個(gè)參數(shù),就是 data。

  • next: 指向該鏈表中下一個(gè)元素。

看到這里,ngx_pool_cleanup_add 這個(gè)函數(shù)的用法,我相信大家都應(yīng)該有一些明白了。但是這個(gè)參數(shù) size 是起什么作用的呢?這個(gè) size 就是要存儲(chǔ)這個(gè) data 字段所指向的資源的大小,該函數(shù)會(huì)為 data 分配 size 大小的空間。

比如我們需要最后刪除一個(gè)文件。那我們?cè)谡{(diào)用這個(gè)函數(shù)的時(shí)候,把 size 指定為存儲(chǔ)文件名的字符串的大小,然后調(diào)用這個(gè)函數(shù)給 cleanup 鏈表中增加一項(xiàng)。該函數(shù)會(huì)返回新添加的這個(gè)節(jié)點(diǎn)。我們?nèi)缓蟀堰@個(gè)節(jié)點(diǎn)中的 data 字段拷貝為文件名。把 hander 字段賦值為一個(gè)刪除文件的函數(shù)(當(dāng)然該函數(shù)的原型要按照 void (\*ngx_pool_cleanup_pt)(void \*data))。

    void ngx_destroy_pool(ngx_pool_t *pool);

該函數(shù)就是釋放 pool 中持有的所有內(nèi)存,以及依次調(diào)用 cleanup 字段所管理的鏈表中每個(gè)元素的 handler 字段所指向的函數(shù),來(lái)釋放掉所有該 pool 管理的資源。并且把 pool 指向的 ngx_pool_t 也釋放掉了,完全不可用了。

    void ngx_reset_pool(ngx_pool_t *pool);

該函數(shù)釋放 pool 中所有大塊內(nèi)存鏈表上的內(nèi)存,小塊內(nèi)存鏈上的內(nèi)存塊都修改為可用。但是不會(huì)去處理 cleanup鏈表上的項(xiàng)目。

ngx_array_t

ngx_array_t 是 Nginx 內(nèi)部使用的數(shù)組結(jié)構(gòu)。Nginx 的數(shù)組結(jié)構(gòu)在存儲(chǔ)上與大家認(rèn)知的 C 語(yǔ)言內(nèi)置的數(shù)組有相似性,比如實(shí)際上存儲(chǔ)數(shù)據(jù)的區(qū)域也是一大塊連續(xù)的內(nèi)存。但是數(shù)組除了存儲(chǔ)數(shù)據(jù)的內(nèi)存以外還包含一些元信息來(lái)描述相關(guān)的一些信息。下面我們從數(shù)組的定義上來(lái)詳細(xì)的了解一下。ngx_array_t 的定義位于src/core/ngx_array.c|h里面。

    typedef struct ngx_array_s       ngx_array_t;
    struct ngx_array_s {
        void        *elts;
        ngx_uint_t   nelts;
        size_t       size;
        ngx_uint_t   nalloc;
        ngx_pool_t  *pool;
    };
  • elts: 指向?qū)嶋H的數(shù)據(jù)存儲(chǔ)區(qū)域。

  • nelts: 數(shù)組實(shí)際元素個(gè)數(shù)。

  • size: 數(shù)組單個(gè)元素的大小,單位是字節(jié)。

  • nalloc: 數(shù)組的容量。表示該數(shù)組在不引發(fā)擴(kuò)容的前提下,可以最多存儲(chǔ)的元素的個(gè)數(shù)。當(dāng) nelts 增長(zhǎng)到達(dá) nalloc 時(shí),如果再往此數(shù)組中存儲(chǔ)元素,則會(huì)引發(fā)數(shù)組的擴(kuò)容。數(shù)組的容量將會(huì)擴(kuò)展到原有容量的 2 倍大小。實(shí)際上是分配新的一塊內(nèi)存,新的一塊內(nèi)存的大小是原有內(nèi)存大小的 2 倍。原有的數(shù)據(jù)會(huì)被拷貝到新的一塊內(nèi)存中。

  • pool: 該數(shù)組用來(lái)分配內(nèi)存的內(nèi)存池。

下面介紹 ngx_array_t 相關(guān)操作函數(shù)。

    ngx_array_t *ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size);

創(chuàng)建一個(gè)新的數(shù)組對(duì)象,并返回這個(gè)對(duì)象。

  • p: 數(shù)組分配內(nèi)存使用的內(nèi)存池;
  • n: 數(shù)組的初始容量大小,即在不擴(kuò)容的情況下最多可以容納的元素個(gè)數(shù)。
  • size: 單個(gè)元素的大小,單位是字節(jié)。
    void ngx_array_destroy(ngx_array_t *a);

銷毀該數(shù)組對(duì)象,并釋放其分配的內(nèi)存回內(nèi)存池。

    void *ngx_array_push(ngx_array_t *a);

在數(shù)組 a 上新追加一個(gè)元素,并返回指向新元素的指針。需要把返回的指針使用類型轉(zhuǎn)換,轉(zhuǎn)換為具體的類型,然后再給新元素本身或者是各字段(如果數(shù)組的元素是復(fù)雜類型)賦值。

    void *ngx_array_push_n(ngx_array_t *a, ngx_uint_t n);

在數(shù)組 a 上追加 n 個(gè)元素,并返回指向這些追加元素的首個(gè)元素的位置的指針。

    static ngx_inline ngx_int_t ngx_array_init(ngx_array_t *array, ngx_pool_t *pool, ngx_uint_t n, size_t size);

如果一個(gè)數(shù)組對(duì)象是被分配在堆上的,那么當(dāng)調(diào)用 ngx_array_destroy 銷毀以后,如果想再次使用,就可以調(diào)用此函數(shù)。

如果一個(gè)數(shù)組對(duì)象是被分配在棧上的,那么就需要調(diào)用此函數(shù),進(jìn)行初始化的工作以后,才可以使用。

注意事項(xiàng)由于使用 ngx_palloc 分配內(nèi)存,數(shù)組在擴(kuò)容時(shí),舊的內(nèi)存不會(huì)被釋放,會(huì)造成內(nèi)存的浪費(fèi)。因此,最好能提前規(guī)劃好數(shù)組的容量,在創(chuàng)建或者初始化的時(shí)候一次搞定,避免多次擴(kuò)容,造成內(nèi)存浪費(fèi)。

ngx_hash_t

ngx_hash_t 是 Nginx 自己的 hash 表的實(shí)現(xiàn)。定義和實(shí)現(xiàn)位于src/core/ngx_hash.h|c中。ngx_hash_t 的實(shí)現(xiàn)也與數(shù)據(jù)結(jié)構(gòu)教科書(shū)上所描述的 hash 表的實(shí)現(xiàn)是大同小異。對(duì)于常用的解決沖突的方法有線性探測(cè),二次探測(cè)和開(kāi)鏈法等。ngx_hash_t 使用的是最常用的一種,也就是開(kāi)鏈法,這也是 STL 中的 hash 表使用的方法。

但是 ngx_hash_t 的實(shí)現(xiàn)又有其幾個(gè)顯著的特點(diǎn):

  1. ngx_hash_t 不像其他的 hash 表的實(shí)現(xiàn),可以插入刪除元素,它只能一次初始化,就構(gòu)建起整個(gè) hash 表以后,既不能再刪除,也不能在插入元素了。
  2. ngx_hash_t 的開(kāi)鏈并不是真的開(kāi)了一個(gè)鏈表,實(shí)際上是開(kāi)了一段連續(xù)的存儲(chǔ)空間,幾乎可以看做是一個(gè)數(shù)組。這是因?yàn)?ngx_hash_t 在初始化的時(shí)候,會(huì)經(jīng)歷一次預(yù)計(jì)算的過(guò)程,提前把每個(gè)桶里面會(huì)有多少元素放進(jìn)去給計(jì)算出來(lái),這樣就提前知道每個(gè)桶的大小了。那么就不需要使用鏈表,一段連續(xù)的存儲(chǔ)空間就足夠了。這也從一定程度上節(jié)省了內(nèi)存的使用。

從上面的描述,我們可以看出來(lái),這個(gè)值越大,越造成內(nèi)存的浪費(fèi)。就兩步,首先是初始化,然后就可以在里面進(jìn)行查找了。下面我們?cè)敿?xì)來(lái)看一下。

ngx_hash_t 的初始化。

 ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
 ngx_uint_t nelts);

首先我們來(lái)看一下初始化函數(shù)。該函數(shù)的第一個(gè)參數(shù) hinit 是初始化的一些參數(shù)的一個(gè)集合。 names 是初始化一個(gè) ngx_hash_t 所需要的所有 key 的一個(gè)數(shù)組。而 nelts 就是 key 的個(gè)數(shù)。下面先看一下 ngx_hash_init_t 類型,該類型提供了初始化一個(gè) hash 表所需要的一些基本信息。

    typedef struct {
        ngx_hash_t       *hash;
        ngx_hash_key_pt   key;

        ngx_uint_t        max_size;
        ngx_uint_t        bucket_size;

        char             *name;
        ngx_pool_t       *pool;
        ngx_pool_t       *temp_pool;
    } ngx_hash_init_t;
  • hash: 該字段如果為 NULL,那么調(diào)用完初始化函數(shù)后,該字段指向新創(chuàng)建出來(lái)的 hash 表。如果該字段不為 NULL,那么在初始的時(shí)候,所有的數(shù)據(jù)被插入了這個(gè)字段所指的 hash 表中。

  • key: 指向從字符串生成 hash 值的 hash 函數(shù)。Nginx 的源代碼中提供了默認(rèn)的實(shí)現(xiàn)函數(shù) ngx_hash_key_lc。

  • max_size: hash 表中的桶的個(gè)數(shù)。該字段越大,元素存儲(chǔ)時(shí)沖突的可能性越小,每個(gè)桶中存儲(chǔ)的元素會(huì)更少,則查詢起來(lái)的速度更快。當(dāng)然,這個(gè)值越大,越造成內(nèi)存的浪費(fèi)也越大,(實(shí)際上也浪費(fèi)不了多少)。
:bucket_size: 每個(gè)桶的最大限制大小,單位是字節(jié)。如果在初始化一個(gè) hash 表的時(shí)候,發(fā)現(xiàn)某個(gè)桶里面無(wú)法存的下所有屬于該桶的元素,則 hash 表初始化失敗。

name: 該 hash 表的名字。

pool: 該 hash 表分配內(nèi)存使用的 pool。

temp_pool: 該 hash 表使用的臨時(shí) pool,在初始化完成以后,該 pool 可以被釋放和銷毀掉。

下面來(lái)看一下存儲(chǔ) hash 表 key 的數(shù)組的結(jié)構(gòu)。

    typedef struct {
        ngx_str_t         key;
        ngx_uint_t        key_hash;
        void             *value;
    } ngx_hash_key_t;

key 和 value 的含義顯而易見(jiàn),就不用解釋了。key_hash 是對(duì) key 使用 hash 函數(shù)計(jì)算出來(lái)的值。

對(duì)這兩個(gè)結(jié)構(gòu)分析完成以后,我想大家應(yīng)該都已經(jīng)明白這個(gè)函數(shù)應(yīng)該是如何使用了吧。該函數(shù)成功初始化一個(gè) hash 表以后,返回 NGX_OK,否則返回 NGX_ERROR。

    void *ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len);

在 hash 里面查找 key 對(duì)應(yīng)的 value。實(shí)際上這里的 key 是對(duì)真正的 key(也就是 name)計(jì)算出的 hash 值。len 是 name 的長(zhǎng)度。

如果查找成功,則返回指向 value 的指針,否則返回 NULL。

ngx_hash_wildcard_t

Nginx 為了處理帶有通配符的域名的匹配問(wèn)題,實(shí)現(xiàn)了 ngx_hash_wildcard_t 這樣的 hash 表。他可以支持兩種類型的帶有通配符的域名。一種是通配符在前的,例如:\*.abc.com,也可以省略掉星號(hào),直接寫成.abc.com。這樣的 key,可以匹配 www.abc.com,qqq.www.abc.com 之類的。另外一種是通配符在末尾的,例如:mail.xxx.\*,請(qǐng)?zhí)貏e注意通配符在末尾的不像位于開(kāi)始的通配符可以被省略掉。這樣的通配符,可以匹配 mail.xxx.com、mail.xxx.com.cn、mail.xxx.net 之類的域名。

有一點(diǎn)必須說(shuō)明,就是一個(gè) ngx_hash_wildcard_t 類型的 hash 表只能包含通配符在前的key或者是通配符在后的key。不能同時(shí)包含兩種類型的通配符的 key。ngx_hash_wildcard_t 類型變量的構(gòu)建是通過(guò)函數(shù) ngx_hash_wildcard_init 完成的,而查詢是通過(guò)函數(shù) ngx_hash_find_wc_head 或者 ngx_hash_find_wc_tail 來(lái)做的。ngx_hash_find_wc_head 查詢包含通配符在前的 key 的 hash 表的,而 ngx_hash_find_wc_tail 是查詢包含通配符在后的 key 的 hash 表的。

下面詳細(xì)說(shuō)明這幾個(gè)函數(shù)的用法。

    ngx_int_t ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
        ngx_uint_t nelts);

該函數(shù)用來(lái)構(gòu)建一個(gè)可以包含通配符 key 的 hash 表。

  • hinit: 構(gòu)造一個(gè)通配符 hash 表的一些參數(shù)的一個(gè)集合。關(guān)于該參數(shù)對(duì)應(yīng)的類型的說(shuō)明,請(qǐng)參見(jiàn) ngx_hash_t 類型中 ngx_hash_init 函數(shù)的說(shuō)明。

  • names: 構(gòu)造此 hash 表的所有的通配符 key 的數(shù)組。特別要注意的是這里的 key 已經(jīng)都是被預(yù)處理過(guò)的。例如:\*.abc.com或者.abc.com被預(yù)處理完成以后,變成了com.abc.。而mail.xxx.\*則被預(yù)處理為mail.xxx.。為什么會(huì)被處理這樣?這里不得不簡(jiǎn)單地描述一下通配符 hash 表的實(shí)現(xiàn)原理。當(dāng)構(gòu)造此類型的 hash 表的時(shí)候,實(shí)際上是構(gòu)造了一個(gè) hash 表的一個(gè)“鏈表”,是通過(guò) hash 表中的 key “鏈接”起來(lái)的。比如:對(duì)于\*.abc.com將會(huì)構(gòu)造出 2 個(gè) hash 表,第一個(gè) hash 表中有一個(gè) key 為 com 的表項(xiàng),該表項(xiàng)的 value 包含有指向第二個(gè) hash 表的指針,而第二個(gè) hash 表中有一個(gè)表項(xiàng) abc,該表項(xiàng)的 value 包含有指向\*.abc.com對(duì)應(yīng)的 value 的指針。那么查詢的時(shí)候,比如查詢 www.abc.com 的時(shí)候,先查 com,通過(guò)查 com 可以找到第二級(jí)的 hash 表,在第二級(jí) hash 表中,再查找 abc,依次類推,直到在某一級(jí)的 hash 表中查到的表項(xiàng)對(duì)應(yīng)的 value 對(duì)應(yīng)一個(gè)真正的值而非一個(gè)指向下一級(jí) hash 表的指針的時(shí)候,查詢過(guò)程結(jié)束。這里有一點(diǎn)需要特別注意的,就是 names 數(shù)組中元素的 value 值低兩位 bit 必須為 0(有特殊用途)。如果不滿足這個(gè)條件,這個(gè) hash 表查詢不出正確結(jié)果。

  • nelts: names 數(shù)組元素的個(gè)數(shù)。

該函數(shù)執(zhí)行成功返回 NGX_OK,否則 NGX_ERROR。

    void *ngx_hash_find_wc_head(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);

該函數(shù)查詢包含通配符在前的 key 的 hash 表的。

  • hwc: hash 表對(duì)象的指針。
  • name: 需要查詢的域名,例如: www.abc.com。
  • len: name 的長(zhǎng)度。

該函數(shù)返回匹配的通配符對(duì)應(yīng) value。如果沒(méi)有查到,返回 NULL。

    void *ngx_hash_find_wc_tail(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);

該函數(shù)查詢包含通配符在末尾的 key 的 hash 表的。

參數(shù)及返回值請(qǐng)參加上個(gè)函數(shù)的說(shuō)明。

ngx_hash_combined_t

組合類型 hash 表,該 hash 表的定義如下:

    typedef struct {
        ngx_hash_t            hash;
        ngx_hash_wildcard_t  *wc_head;
        ngx_hash_wildcard_t  *wc_tail;
    } ngx_hash_combined_t;

從其定義顯見(jiàn),該類型實(shí)際上包含了三個(gè) hash 表,一個(gè)普通 hash 表,一個(gè)包含前向通配符的 hash 表和一個(gè)包含后向通配符的 hash 表。

Nginx 提供該類型的作用,在于提供一個(gè)方便的容器包含三個(gè)類型的 hash 表,當(dāng)有包含通配符的和不包含通配符的一組 key 構(gòu)建 hash 表以后,以一種方便的方式來(lái)查詢,你不需要再考慮一個(gè) key 到底是應(yīng)該到哪個(gè)類型的 hash 表里去查了。

構(gòu)造這樣一組合 hash 表的時(shí)候,首先定義一個(gè)該類型的變量,再分別構(gòu)造其包含的三個(gè)子 hash 表即可。

對(duì)于該類型 hash 表的查詢,Nginx 提供了一個(gè)方便的函數(shù) ngx_hash_find_combined。

    void *ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key,
    u_char *name, size_t len);

該函數(shù)在此組合 hash 表中,依次查詢其三個(gè)子 hash 表,看是否匹配,一旦找到,立即返回查找結(jié)果,也就是說(shuō)如果有多個(gè)可能匹配,則只返回第一個(gè)匹配的結(jié)果。

  • hash: 此組合 hash 表對(duì)象。
  • key: 根據(jù) name 計(jì)算出的 hash 值。
  • name: key 的具體內(nèi)容。
  • len: name 的長(zhǎng)度。

返回查詢的結(jié)果,未查到則返回 NULL。

ngx_hash_keys_arrays_t

大家看到在構(gòu)建一個(gè) ngx_hash_wildcard_t 的時(shí)候,需要對(duì)通配符的哪些 key 進(jìn)行預(yù)處理。這個(gè)處理起來(lái)比較麻煩。而當(dāng)有一組 key,這些里面既有無(wú)通配符的 key,也有包含通配符的 key 的時(shí)候。我們就需要構(gòu)建三個(gè) hash 表,一個(gè)包含普通的 key 的 hash 表,一個(gè)包含前向通配符的 hash 表,一個(gè)包含后向通配符的 hash 表(或者也可以把這三個(gè) hash 表組合成一個(gè) ngx_hash_combined_t)。在這種情況下,為了讓大家方便的構(gòu)造這些 hash 表,Nginx 提供給了此輔助類型。

該類型以及相關(guān)的操作函數(shù)也定義在src/core/ngx_hash.h|c里。我們先來(lái)看一下該類型的定義。

    typedef struct {
        ngx_uint_t        hsize;

        ngx_pool_t       *pool;
        ngx_pool_t       *temp_pool;

        ngx_array_t       keys;
        ngx_array_t      *keys_hash;

        ngx_array_t       dns_wc_head;
        ngx_array_t      *dns_wc_head_hash;

        ngx_array_t       dns_wc_tail;
        ngx_array_t      *dns_wc_tail_hash;
    } ngx_hash_keys_arrays_t;
  • hsize: 將要構(gòu)建的 hash 表的桶的個(gè)數(shù)。對(duì)于使用這個(gè)結(jié)構(gòu)中包含的信息構(gòu)建的三種類型的 hash 表都會(huì)使用此參數(shù)。

  • pool: 構(gòu)建這些 hash 表使用的 pool。

  • temp_pool: 在構(gòu)建這個(gè)類型以及最終的三個(gè) hash 表過(guò)程中可能用到臨時(shí) pool。該 temp_pool 可以在構(gòu)建完成以后,被銷毀掉。這里只是存放臨時(shí)的一些內(nèi)存消耗。

  • keys: 存放所有非通配符 key 的數(shù)組。

  • keys_hash: 這是個(gè)二維數(shù)組,第一個(gè)維度代表的是 bucket 的編號(hào),那么 keys_hash[i] 中存放的是所有的 key 算出來(lái)的 hash 值對(duì) hsize 取模以后的值為 i 的 key。假設(shè)有 3 個(gè) key,分別是 key1,key2 和 key3 假設(shè) hash 值算出來(lái)以后對(duì) hsize 取模的值都是 i,那么這三個(gè) key 的值就順序存放在keys_hash[i][0],keys_hash[i][1], keys_hash[i][2]。該值在調(diào)用的過(guò)程中用來(lái)保存和檢測(cè)是否有沖突的 key 值,也就是是否有重復(fù)。

  • dns_wc_head: 放前向通配符 key 被處理完成以后的值。比如:\*.abc.com 被處理完成以后,變成 “com.abc.” 被存放在此數(shù)組中。

  • dns_wc_tail: 存放后向通配符 key 被處理完成以后的值。比如:mail.xxx.\* 被處理完成以后,變成 “mail.xxx.” 被存放在此數(shù)組中。

  • dns_wc_head_hash: 該值在調(diào)用的過(guò)程中用來(lái)保存和檢測(cè)是否有沖突的前向通配符的 key 值,也就是是否有重復(fù)。

  • dns_wc_tail_hash: 該值在調(diào)用的過(guò)程中用來(lái)保存和檢測(cè)是否有沖突的后向通配符的 key 值,也就是是否有重復(fù)。

在定義一個(gè)這個(gè)類型的變量,并對(duì)字段 pool 和 temp_pool 賦值以后,就可以調(diào)用函數(shù) ngx_hash_add_key 把所有的 key 加入到這個(gè)結(jié)構(gòu)中了,該函數(shù)會(huì)自動(dòng)實(shí)現(xiàn)普通 key,帶前向通配符的 key 和帶后向通配符的 key 的分類和檢查,并將這個(gè)些值存放到對(duì)應(yīng)的字段中去,然后就可以通過(guò)檢查這個(gè)結(jié)構(gòu)體中的 keys、dns_wc_head、dns_wc_tail 三個(gè)數(shù)組是否為空,來(lái)決定是否構(gòu)建普通 hash 表,前向通配符 hash 表和后向通配符 hash 表了(在構(gòu)建這三個(gè)類型的 hash 表的時(shí)候,可以分別使用 keys、dns_wc_head、dns_wc_tail三個(gè)數(shù)組)。

構(gòu)建出這三個(gè) hash 表以后,可以組合在一個(gè) ngx_hash_combined_t 對(duì)象中,使用 ngx_hash_find_combined 進(jìn)行查找?;蛘呤侨匀槐3秩齻€(gè)獨(dú)立的變量對(duì)應(yīng)這三個(gè) hash 表,自己決定何時(shí)以及在哪個(gè) hash 表中進(jìn)行查詢。

    ngx_int_t ngx_hash_keys_array_init(ngx_hash_keys_arrays_t *ha, ngx_uint_t type);

初始化這個(gè)結(jié)構(gòu),主要是對(duì)這個(gè)結(jié)構(gòu)中的 ngx_array_t 類型的字段進(jìn)行初始化,成功返回 NGX_OK。

  • ha: 該結(jié)構(gòu)的對(duì)象指針。

  • type: 該字段有 2 個(gè)值可選擇,即 NGX_HASH_SMALL 和 NGX_HASH_LARGE。用來(lái)指明將要建立的 hash 表的類型,如果是 NGX_HASH_SMALL,則有比較小的桶的個(gè)數(shù)和數(shù)組元素大小。NGX_HASH_LARGE 則相反。
    ngx_int_t ngx_hash_add_key(ngx_hash_keys_arrays_t *ha, ngx_str_t *key,
    void *value, ngx_uint_t flags);

一般是循環(huán)調(diào)用這個(gè)函數(shù),把一組鍵值對(duì)加入到這個(gè)結(jié)構(gòu)體中。返回 NGX_OK 是加入成功。返回 NGX_BUSY 意味著key值重復(fù)。

  • ha: 該結(jié)構(gòu)的對(duì)象指針。

  • key: 參數(shù)名自解釋了。

  • value: 參數(shù)名自解釋了。

  • flags: 有兩個(gè)標(biāo)志位可以設(shè)置,NGX_HASH_WILDCARD_KEY 和 NGX_HASH_READONLY_KEY。同時(shí)要設(shè)置的使用邏輯與操作符就可以了。NGX_HASH_READONLY_KEY 被設(shè)置的時(shí)候,在計(jì)算 hash 值的時(shí)候,key 的值不會(huì)被轉(zhuǎn)成小寫字符,否則會(huì)。NGX_HASH_WILDCARD_KEY 被設(shè)置的時(shí)候,說(shuō)明 key 里面可能含有通配符,會(huì)進(jìn)行相應(yīng)的處理。如果兩個(gè)標(biāo)志位都不設(shè)置,傳 0。

有關(guān)于這個(gè)數(shù)據(jù)結(jié)構(gòu)的使用,可以參考src/http/ngx_http.c中的 ngx_http_server_names 函數(shù)。

ngx_chain_t

Nginx 的 filter 模塊在處理從別的 filter 模塊或者是 handler 模塊傳遞過(guò)來(lái)的數(shù)據(jù)(實(shí)際上就是需要發(fā)送給客戶端的 http response)。這個(gè)傳遞過(guò)來(lái)的數(shù)據(jù)是以一個(gè)鏈表的形式(ngx_chain_t)。而且數(shù)據(jù)可能被分多次傳遞過(guò)來(lái)。也就是多次調(diào)用 filter 的處理函數(shù),以不同的 ngx_chain_t。

該結(jié)構(gòu)被定義在src/core/ngx_buf.h|c。下面我們來(lái)看一下 ngx_chain_t 的定義。

    typedef struct ngx_chain_s       ngx_chain_t;

    struct ngx_chain_s {
        ngx_buf_t    *buf;
        ngx_chain_t  *next;
    };

就 2 個(gè)字段,next 指向這個(gè)鏈表的下個(gè)節(jié)點(diǎn)。buf 指向?qū)嶋H的數(shù)據(jù)。所以在這個(gè)鏈表上追加節(jié)點(diǎn)也是非常容易,只要把末尾元素的 next 指針指向新的節(jié)點(diǎn),把新節(jié)點(diǎn)的 next 賦值為 NULL 即可。

    ngx_chain_t *ngx_alloc_chain_link(ngx_pool_t *pool);

該函數(shù)創(chuàng)建一個(gè) ngx_chain_t 的對(duì)象,并返回指向?qū)ο蟮闹羔槪》祷?NULL。

    #define ngx_free_chain(pool, cl)                                             \
        cl->next = pool->chain;                                                  \
    pool->chain = cl

該宏釋放一個(gè) ngx_chain_t 類型的對(duì)象。如果要釋放整個(gè) chain,則迭代此鏈表,對(duì)每個(gè)節(jié)點(diǎn)使用此宏即可。

注意: 對(duì) ngx_chaint_t 類型的釋放,并不是真的釋放了內(nèi)存,而僅僅是把這個(gè)對(duì)象掛在了這個(gè) pool 對(duì)象的一個(gè)叫做 chain 的字段對(duì)應(yīng)的 chain 上,以供下次從這個(gè) pool 上分配 ngx_chain_t 類型對(duì)象的時(shí)候,快速的從這個(gè) pool->chain上 取下鏈?zhǔn)自鼐头祷亓?,?dāng)然,如果這個(gè)鏈?zhǔn)强盏模艜?huì)真的在這個(gè) pool 上使用 ngx_palloc 函數(shù)進(jìn)行分配。

ngx_buf_t

這個(gè) ngx_buf_t 就是這個(gè) ngx_chain_t 鏈表的每個(gè)節(jié)點(diǎn)的實(shí)際數(shù)據(jù)。該結(jié)構(gòu)實(shí)際上是一種抽象的數(shù)據(jù)結(jié)構(gòu),它代表某種具體的數(shù)據(jù)。這個(gè)數(shù)據(jù)可能是指向內(nèi)存中的某個(gè)緩沖區(qū),也可能指向一個(gè)文件的某一部分,也可能是一些純?cè)獢?shù)據(jù)(元數(shù)據(jù)的作用在于指示這個(gè)鏈表的讀取者對(duì)讀取的數(shù)據(jù)進(jìn)行不同的處理)。

該數(shù)據(jù)結(jié)構(gòu)位于src/core/ngx_buf.h|c文件中。我們來(lái)看一下它的定義。

    struct ngx_buf_s {
        u_char          *pos;
        u_char          *last;
        off_t            file_pos;
        off_t            file_last;

        u_char          *start;         /* start of buffer */
        u_char          *end;           /* end of buffer */
        ngx_buf_tag_t    tag;
        ngx_file_t      *file;
        ngx_buf_t       *shadow;

        /* the buf's content could be changed */
        unsigned         temporary:1;

        /*
         * the buf's content is in a memory cache or in a read only memory
         * and must not be changed
         */
        unsigned         memory:1;

        /* the buf's content is mmap()ed and must not be changed */
        unsigned         mmap:1;

        unsigned         recycled:1;
        unsigned         in_file:1;
        unsigned         flush:1;
        unsigned         sync:1;
        unsigned         last_buf:1;
        unsigned         last_in_chain:1;

        unsigned         last_shadow:1;
        unsigned         temp_file:1;

        /* STUB */ int   num;
    };
  • pos:當(dāng) buf 所指向的數(shù)據(jù)在內(nèi)存里的時(shí)候,pos 指向的是這段數(shù)據(jù)開(kāi)始的位置。

  • last:當(dāng) buf 所指向的數(shù)據(jù)在內(nèi)存里的時(shí)候,last 指向的是這段數(shù)據(jù)結(jié)束的位置。

  • file_pos:當(dāng) buf 所指向的數(shù)據(jù)是在文件里的時(shí)候,file_pos 指向的是這段數(shù)據(jù)的開(kāi)始位置在文件中的偏移量。

  • file_last:當(dāng) buf 所指向的數(shù)據(jù)是在文件里的時(shí)候,file_last 指向的是這段數(shù)據(jù)的結(jié)束位置在文件中的偏移量。

  • start:當(dāng) buf 所指向的數(shù)據(jù)在內(nèi)存里的時(shí)候,這一整塊內(nèi)存包含的內(nèi)容可能被包含在多個(gè) buf 中(比如在某段數(shù)據(jù)中間插入了其他的數(shù)據(jù),這一塊數(shù)據(jù)就需要被拆分開(kāi))。那么這些 buf 中的 start 和 end 都指向這一塊內(nèi)存的開(kāi)始地址和結(jié)束地址。而 pos 和 last 指向本 buf 所實(shí)際包含的數(shù)據(jù)的開(kāi)始和結(jié)尾。

  • end:解釋參見(jiàn) start。

  • tag:實(shí)際上是一個(gè)void *類型的指針,使用者可以關(guān)聯(lián)任意的對(duì)象上去,只要對(duì)使用者有意義。

  • file:當(dāng) buf 所包含的內(nèi)容在文件中時(shí),file字段指向?qū)?yīng)的文件對(duì)象。

  • shadow:當(dāng)這個(gè) buf 完整 copy 了另外一個(gè) buf 的所有字段的時(shí)候,那么這兩個(gè) buf 指向的實(shí)際上是同一塊內(nèi)存,或者是同一個(gè)文件的同一部分,此時(shí)這兩個(gè) buf 的 shadow 字段都是指向?qū)Ψ降?。那么?duì)于這樣的兩個(gè) buf,在釋放的時(shí)候,就需要使用者特別小心,具體是由哪里釋放,要提前考慮好,如果造成資源的多次釋放,可能會(huì)造成程序崩潰!

  • temporary:為 1 時(shí)表示該 buf 所包含的內(nèi)容是在一個(gè)用戶創(chuàng)建的內(nèi)存塊中,并且可以被在 filter 處理的過(guò)程中進(jìn)行變更,而不會(huì)造成問(wèn)題。

  • memory:為 1 時(shí)表示該 buf 所包含的內(nèi)容是在內(nèi)存中,但是這些內(nèi)容卻不能被進(jìn)行處理的 filter 進(jìn)行變更。

  • mmap:為 1 時(shí)表示該 buf 所包含的內(nèi)容是在內(nèi)存中, 是通過(guò) mmap 使用內(nèi)存映射從文件中映射到內(nèi)存中的,這些內(nèi)容卻不能被進(jìn)行處理的 filter 進(jìn)行變更。

  • recycled:可以回收的。也就是這個(gè) buf 是可以被釋放的。這個(gè)字段通常是配合 shadow 字段一起使用的,對(duì)于使用 ngx_create_temp_buf 函數(shù)創(chuàng)建的 buf,并且是另外一個(gè) buf 的 shadow,那么可以使用這個(gè)字段來(lái)標(biāo)示這個(gè)buf是可以被釋放的。

  • in_file:為 1 時(shí)表示該 buf 所包含的內(nèi)容是在文件中。

  • flush:遇到有 flush 字段被設(shè)置為 1 的 buf 的 chain,則該 chain 的數(shù)據(jù)即便不是最后結(jié)束的數(shù)據(jù)(last_buf被設(shè)置,標(biāo)志所有要輸出的內(nèi)容都完了),也會(huì)進(jìn)行輸出,不會(huì)受 postpone_output 配置的限制,但是會(huì)受到發(fā)送速率等其他條件的限制。

  • last_buf:數(shù)據(jù)被以多個(gè) chain 傳遞給了過(guò)濾器,此字段為 1 表明這是最后一個(gè) buf。

  • last_in_chain:在當(dāng)前的 chain 里面,此 buf 是最后一個(gè)。特別要注意的是 last_in_chain 的 buf 不一定是last_buf,但是 last_buf 的 buf 一定是 last_in_chain 的。這是因?yàn)閿?shù)據(jù)會(huì)被以多個(gè) chain 傳遞給某 個(gè)filter 模塊。

  • last_shadow:在創(chuàng)建一個(gè) buf 的 shadow 的時(shí)候,通常將新創(chuàng)建的一個(gè) buf 的 last_shadow 置為 1。

  • temp_file:由于受到內(nèi)存使用的限制,有時(shí)候一些 buf 的內(nèi)容需要被寫到磁盤上的臨時(shí)文件中去,那么這時(shí),就設(shè)置此標(biāo)志。

對(duì)于此對(duì)象的創(chuàng)建,可以直接在某個(gè) ngx_pool_t 上分配,然后根據(jù)需要,給對(duì)應(yīng)的字段賦值。也可以使用定義好的 2 個(gè)宏:

    #define ngx_alloc_buf(pool)  ngx_palloc(pool, sizeof(ngx_buf_t))
    #define ngx_calloc_buf(pool) ngx_pcalloc(pool, sizeof(ngx_buf_t))

這兩個(gè)宏使用類似函數(shù),也是不說(shuō)自明的。

對(duì)于創(chuàng)建 temporary 字段為 1 的 buf(就是其內(nèi)容可以被后續(xù)的 filter 模塊進(jìn)行修改),可以直接使用函數(shù) ngx_create_temp_buf 進(jìn)行創(chuàng)建。

    ngx_buf_t *ngx_create_temp_buf(ngx_pool_t *pool, size_t size);

該函數(shù)創(chuàng)建一個(gè) ngx_buf_t 類型的對(duì)象,并返回指向這個(gè)對(duì)象的指針,創(chuàng)建失敗返回 NULL。

對(duì)于創(chuàng)建的這個(gè)對(duì)象,它的 start 和 end 指向新分配內(nèi)存開(kāi)始和結(jié)束的地方。pos 和 last 都指向這塊新分配內(nèi)存的開(kāi)始處,這樣,后續(xù)的操作可以在這塊新分配的內(nèi)存上存入數(shù)據(jù)。

  • pool: 分配該 buf 和 buf 使用的內(nèi)存所使用的 pool。
  • size: 該 buf 使用的內(nèi)存的大小。

為了配合對(duì) ngx_buf_t 的使用,Nginx 定義了以下的宏方便操作。

    #define ngx_buf_in_memory(b)        (b->temporary || b->memory || b->mmap)

返回這個(gè) buf 里面的內(nèi)容是否在內(nèi)存里。

    #define ngx_buf_in_memory_only(b)   (ngx_buf_in_memory(b) && !b->in_file)

返回這個(gè) buf 里面的內(nèi)容是否僅僅在內(nèi)存里,并且沒(méi)有在文件里。

    #define ngx_buf_special(b)                                                   \
        ((b->flush || b->last_buf || b->sync)                                    \
         && !ngx_buf_in_memory(b) && !b->in_file)

返回該 buf 是否是一個(gè)特殊的 buf,只含有特殊的標(biāo)志和沒(méi)有包含真正的數(shù)據(jù)。

    #define ngx_buf_sync_only(b)                                                 \
        (b->sync                                                                 \
         && !ngx_buf_in_memory(b) && !b->in_file && !b->flush && !b->last_buf)

返回該 buf 是否是一個(gè)只包含 sync 標(biāo)志而不包含真正數(shù)據(jù)的特殊 buf。

    #define ngx_buf_size(b)                                                      \
        (ngx_buf_in_memory(b) ? (off_t) (b->last - b->pos):                      \
                                (b->file_last - b->file_pos))

返回該 buf 所含數(shù)據(jù)的大小,不管這個(gè)數(shù)據(jù)是在文件里還是在內(nèi)存里。

ngx_list_t

ngx_list_t 顧名思義,看起來(lái)好像是一個(gè) list 的數(shù)據(jù)結(jié)構(gòu)。這樣的說(shuō)法,算對(duì)也不算對(duì)。因?yàn)樗?list 類型數(shù)據(jù)結(jié)構(gòu)的一些特點(diǎn),比如可以添加元素,實(shí)現(xiàn)自增長(zhǎng),不會(huì)像數(shù)組類型的數(shù)據(jù)結(jié)構(gòu),受到初始設(shè)定的數(shù)組容量的限制,并且它跟我們常見(jiàn)的 list 型數(shù)據(jù)結(jié)構(gòu)也是一樣的,內(nèi)部實(shí)現(xiàn)使用了一個(gè)鏈表。

那么它跟我們常見(jiàn)的鏈表實(shí)現(xiàn)的 list 有什么不同呢?不同點(diǎn)就在于它的節(jié)點(diǎn),它的節(jié)點(diǎn)不像我們常見(jiàn)的 list 的節(jié)點(diǎn),只能存放一個(gè)元素,ngx_list_t 的節(jié)點(diǎn)實(shí)際上是一個(gè)固定大小的數(shù)組。

在初始化的時(shí)候,我們需要設(shè)定元素需要占用的空間大小,每個(gè)節(jié)點(diǎn)數(shù)組的容量大小。在添加元素到這個(gè) list 里面的時(shí)候,會(huì)在最尾部的節(jié)點(diǎn)里的數(shù)組上添加元素,如果這個(gè)節(jié)點(diǎn)的數(shù)組存滿了,就再增加一個(gè)新的節(jié)點(diǎn)到這個(gè) list 里面去。

好了,看到這里,大家應(yīng)該基本上明白這個(gè) list 結(jié)構(gòu)了吧?還不明白也沒(méi)有關(guān)系,下面我們來(lái)具體看一下它的定義,這些定義和相關(guān)的操作函數(shù)定義在src/core/ngx_list.h|c文件中。

    typedef struct {
        ngx_list_part_t  *last;
        ngx_list_part_t   part;
        size_t            size;
        ngx_uint_t        nalloc;
        ngx_pool_t       *pool;
    } ngx_list_t;
  • last: 指向該鏈表的最后一個(gè)節(jié)點(diǎn)。
  • part: 該鏈表的首個(gè)存放具體元素的節(jié)點(diǎn)。
  • size: 鏈表中存放的具體元素所需內(nèi)存大小。
  • nalloc: 每個(gè)節(jié)點(diǎn)所含的固定大小的數(shù)組的容量。
  • pool: 該 list 使用的分配內(nèi)存的 pool。

好,我們?cè)诳匆幌旅總€(gè)節(jié)點(diǎn)的定義。

    typedef struct ngx_list_part_s  ngx_list_part_t;
    struct ngx_list_part_s {
        void             *elts;
        ngx_uint_t        nelts;
        ngx_list_part_t  *next;
    };
  • elts: 節(jié)點(diǎn)中存放具體元素的內(nèi)存的開(kāi)始地址。

  • nelts: 節(jié)點(diǎn)中已有元素個(gè)數(shù)。這個(gè)值是不能大于鏈表頭節(jié)點(diǎn) ngx_list_t 類型中的 nalloc 字段的。

  • next: 指向下一個(gè)節(jié)點(diǎn)。

我們來(lái)看一下提供的一個(gè)操作的函數(shù)。

    ngx_list_t *ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size);

該函數(shù)創(chuàng)建一個(gè) ngx_list_t 類型的對(duì)象,并對(duì)該 list 的第一個(gè)節(jié)點(diǎn)分配存放元素的內(nèi)存空間。

  • pool: 分配內(nèi)存使用的 pool。

  • n: 每個(gè)節(jié)點(diǎn)(ngx_list_part_t)固定長(zhǎng)度的數(shù)組的長(zhǎng)度,即最多可以存放的元素個(gè)數(shù)。

  • size: 每個(gè)元素所占用的內(nèi)存大小。

  • 返回值: 成功返回指向創(chuàng)建的 ngx_list_t 對(duì)象的指針,失敗返回 NULL。
    void *ngx_list_push(ngx_list_t *list);

該函數(shù)在給定的 list 的尾部追加一個(gè)元素,并返回指向新元素存放空間的指針。如果追加失敗,則返回 NULL。

    static ngx_inline ngx_int_t
    ngx_list_init(ngx_list_t *list, ngx_pool_t *pool, ngx_uint_t n, size_t size);

該函數(shù)是用于 ngx_list_t 類型的對(duì)象已經(jīng)存在,但是其第一個(gè)節(jié)點(diǎn)存放元素的內(nèi)存空間還未分配的情況下,可以調(diào)用此函數(shù)來(lái)給這個(gè) list 的首節(jié)點(diǎn)來(lái)分配存放元素的內(nèi)存空間。

那么什么時(shí)候會(huì)出現(xiàn)已經(jīng)有了 ngx_list_t 類型的對(duì)象,而其首節(jié)點(diǎn)存放元素的內(nèi)存尚未分配的情況呢?那就是這個(gè) ngx_list_t 類型的變量并不是通過(guò)調(diào)用 ngx_list_create 函數(shù)創(chuàng)建的。例如:如果某個(gè)結(jié)構(gòu)體的一個(gè)成員變量是 ngx_list_t 類型的,那么當(dāng)這個(gè)結(jié)構(gòu)體類型的對(duì)象被創(chuàng)建出來(lái)的時(shí)候,這個(gè)成員變量也被創(chuàng)建出來(lái)了,但是它的首節(jié)點(diǎn)的存放元素的內(nèi)存并未被分配。

總之,如果這個(gè) ngx_list_t 類型的變量,如果不是你通過(guò)調(diào)用函數(shù) ngx_list_create 創(chuàng)建的,那么就必須調(diào)用此函數(shù)去初始化,否則,你往這個(gè) list 里追加元素就可能引發(fā)不可預(yù)知的行為,亦或程序會(huì)崩潰!

ngx_queue_t

ngx_queue_t 是 Nginx 中的雙向鏈表,在 Nginx 源碼目錄src/core下面的ngx_queue.h|c里面。它的原型如下:

    typedef struct ngx_queue_s ngx_queue_t;

    struct ngx_queue_s {
        ngx_queue_t  *prev;
        ngx_queue_t  *next;
    };

不同于教科書(shū)中將鏈表節(jié)點(diǎn)的數(shù)據(jù)成員聲明在鏈表節(jié)點(diǎn)的結(jié)構(gòu)體中,ngx_queue_t 只是聲明了前向和后向指針。在使用的時(shí)候,我們首先需要定義一個(gè)哨兵節(jié)點(diǎn)(對(duì)于后續(xù)具體存放數(shù)據(jù)的節(jié)點(diǎn),我們稱之為數(shù)據(jù)節(jié)點(diǎn)),比如:

    ngx_queue_t free;

接下來(lái)需要進(jìn)行初始化,通過(guò)宏 ngx_queue_init()來(lái)實(shí)現(xiàn):

    ngx_queue_init(&free);

ngx_queue_init()的宏定義如下:

    #define ngx_queue_init(q)     \
        (q)->prev = q;            \
        (q)->next = q

可見(jiàn)初始的時(shí)候哨兵節(jié)點(diǎn)的 prev 和 next 都指向自己,因此其實(shí)是一個(gè)空鏈表。ngx_queue_empty()可以用來(lái)判斷一個(gè)鏈表是否為空,其實(shí)現(xiàn)也很簡(jiǎn)單,就是:

    #define ngx_queue_empty(h)    \
        (h == (h)->prev)

那么如何聲明一個(gè)具有數(shù)據(jù)元素的鏈表節(jié)點(diǎn)呢?只要在相應(yīng)的結(jié)構(gòu)體中加上一個(gè) ngx_queue_t 的成員就行了。比如 ngx_http_upstream_keepalive_module 中的 ngx_http_upstream_keepalive_cache_t:

    typedef struct {
        ngx_http_upstream_keepalive_srv_conf_t  *conf;

        ngx_queue_t                        queue;
        ngx_connection_t                  *connection;

        socklen_t                          socklen;
        u_char                             sockaddr[NGX_SOCKADDRLEN];
    } ngx_http_upstream_keepalive_cache_t;

對(duì)于每一個(gè)這樣的數(shù)據(jù)節(jié)點(diǎn),可以通過(guò) ngx_queue_insert_head()來(lái)添加到鏈表中,第一個(gè)參數(shù)是哨兵節(jié)點(diǎn),第二個(gè)參數(shù)是數(shù)據(jù)節(jié)點(diǎn),比如:

    ngx_http_upstream_keepalive_cache_t cache;
    ngx_queue_insert_head(&free, &cache.queue);

相應(yīng)的幾個(gè)宏定義如下:

    #define ngx_queue_insert_head(h, x)                         \
        (x)->next = (h)->next;                                  \
        (x)->next->prev = x;                                    \
        (x)->prev = h;                                          \
        (h)->next = x

    #define ngx_queue_insert_after   ngx_queue_insert_head

    #define ngx_queue_insert_tail(h, x)                          \
        (x)->prev = (h)->prev;                                   \
        (x)->prev->next = x;                                     \
        (x)->next = h;                                           \
        (h)->prev = x

ngx_queue_insert_head() 和 ngx_queue_insert_after() 都是往頭部添加節(jié)點(diǎn),ngx_queue_insert_tail() 是往尾部添加節(jié)點(diǎn)。從代碼可以看出哨兵節(jié)點(diǎn)的 prev 指向鏈表的尾數(shù)據(jù)節(jié)點(diǎn),next 指向鏈表的頭數(shù)據(jù)節(jié)點(diǎn)。另外 ngx_queue_head() 和 ngx_queue_last() 這兩個(gè)宏分別可以得到頭節(jié)點(diǎn)和尾節(jié)點(diǎn)。

那假如現(xiàn)在有一個(gè) ngx_queue_t *q 指向的是鏈表中的數(shù)據(jù)節(jié)點(diǎn)的 queue 成員,如何得到ngx_http_upstream_keepalive_cache_t 的數(shù)據(jù)呢? Nginx 提供了 ngx_queue_data() 宏來(lái)得到ngx_http_upstream_keepalive_cache_t 的指針,例如:

 ngx_http_upstream_keepalive_cache_t *cache = ngx_queue_data(q,
                              ngx_http_upstream_keepalive_cache_t,
                                                     queue);

也許您已經(jīng)可以猜到 ngx_queue_data 是通過(guò)地址相減來(lái)得到的:

    #define ngx_queue_data(q, type, link)                        \
        (type *) ((u_char *) q - offsetof(type, link))

另外 Nginx 也提供了 ngx_queue_remove()宏來(lái)從鏈表中刪除一個(gè)數(shù)據(jù)節(jié)點(diǎn),以及 ngx_queue_add() 用來(lái)將一個(gè)鏈表添加到另一個(gè)鏈表。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)