Nginx 源碼分析:ngx_hash_t(下)

本篇的上篇爲 Nginx 源碼分析:ngx_hash_t(上)
建議先讀完上篇再來讀下篇。nginx


上篇回顧了hash表的基礎概念,分析了Nginx中hash表的內存模型及邏輯模型,從而引出了其核型數據結構ngx_hash_elt_tngx_hash_t,並從設計的角度解釋瞭如何初始化這兩個結構體。segmentfault

本篇主要分析,在Nginx源碼中是如何初始化這兩個結構體的。數組

主體思路

  1. 分析Nginx中使用的哈希函數,圍繞初始化時使用的ngx_hash_key_t結構體展開。
  2. 分析Nginx如何估算所需內存大小,如何分配內存。

相信仔細看過上篇後,對這兩個問題已經內心有底了。數據結構

ngx_hash_key_t結構體

上篇說過,Nginx中hash表是只讀的,所以,能夠提早對須要多大的內存進行預估。
Nginx中,提早將索引值、對應hash值,及內容計算出來,放入一個ngx_hash_key_t結構體中。函數

typedef struct {
    ngx_str_t         key;        // 索引值
    ngx_uint_t        key_hash;   // 對應hash值
    void             *value;      // 內容
} ngx_hash_key_t;

那麼,如何計算一個字符串key的哈希值呢?答案是哈希函數。
Nginx提供的哈希函數有兩個:源碼分析

#define ngx_hash(key, c)   ((ngx_uint_t) key * 31 + c)

ngx_uint_t
ngx_hash_key(u_char *data, size_t len)
{
    ngx_uint_t  i, key;

    key = 0;

    for (i = 0; i < len; i++) {
        key = ngx_hash(key, data[i]);
    }

    return key;
}

ngx_uint_t
ngx_hash_key_lc(u_char *data, size_t len)
{
    ngx_uint_t  i, key;

    key = 0;

    for (i = 0; i < len; i++) {
        key = ngx_hash(key, ngx_tolower(data[i]));
    }

    return key;
}

其中ngx_hash_key_lc是大小寫無關,ngx_hash_key是大小寫相關的。學習

通常狀況下,咱們會獲得一個ngx_hash_key_t的數組。
例如HTTP請求的通用首部:優化

Host:
Connection:
User-Agent:
...

每一條首部對應一個ngx_hash_key_t。這樣獲得一個關於HTTP請求的首部哈希數組。ui

有了這些信息,就能夠提早預估根據這個數組生成的哈希表究竟須要多大的內存。設計

ngx_hash_init_t結構體

關於Nginx的哈希表,上篇提到過兩點:

1)Nginx的哈希表自己是向ngx_pool_t申請的一塊連續的內存,所以初始化哈希表須要知道ngx_pool_t
2)Nginx的哈希表解決哈希衝突採用了hash桶的辦法,所以,在邏輯上,哈希表是一個二維數組。這個數組的大小受兩個因素影響:桶的大小和桶的個數。

通常來說,桶的個數越大,所須要桶的大小越小。

所以,這個在初始化時預先對內存進行估計的結構體ngx_hash_init_t是長成這個樣子的:

typedef ngx_uint_t (*ngx_hash_key_pt) (u_char *data, size_t len);

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用於指向建立的哈希表管理結構體ngx_hash_t,當這個值爲NULL時,在完成初始化時,回從內存池中獲取一塊內存。

  • max_size是限制生成的哈希表中桶的個數上限制。這個值越大,桶的大小越小,所以衝突項越少,查詢速度更快,可是浪費的內存會增多。
  • bucket_size用來限制每一個桶的大小上限值。

這兩個參數是給哈希表生成時提供一個上限參考,並非哈希表生成的最終大小。

Nginx哈希表的生成

鋪墊了這麼久,終於進入正題。-_-!!

Nginx的哈希表生成函數聲明以下:

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

其中,*hinit是初始化的限制條件,詳見上一小節的說明;
*namesNginx根據索引值,提早算出來的哈希值數組,詳見上上一小節;
nelts*names數組的大小。

整體來講:這個函數作了三件事情:

  1. 根據輸入的ngx_hash_key_t結構體數組及ngx_hash_init_t結構體,估算出hash表所需的hash桶的個數及每一個hash桶的大小;
  2. 根據算出的hash桶的個數及hash桶的大小,向內存池中申請空間;
  3. ngx_hash_key_t數組的內容裝入生成的hash表中。

由於ngx_hash_init的函數定義很長,爲了更好的說明問題,我按照次序撿取主要內容一段一段拆開來分析。感興趣的能夠看Ngx_hash.c的源碼完整版

校驗bucket_size值

首先,對傳入的hinitnames中的bucket_size大小進行校驗。
條件是,hinit中的bucket_size >= sizeof(ngx_hash_elt_t)

#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))

#define NGX_HASH_ELT_SIZE(name)                                               \
    (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))

for (n = 0; n < nelts; n++) {
        if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))
        {
            ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
                          "could not build the %s, you should "
                          "increase %s_bucket_size: %i",
                          hinit->name, hinit->name, hinit->bucket_size);
            return NGX_ERROR;
        }
    }

能夠看到,NGX_HASH_ELT_SIZE的最小值爲 sizeof(void*) + sizeof(void*),所以bucket_size的最小值是兩個指針的大小

這裏須要解釋一下ngx_align(d,a)這個宏的做用:將da的向上整數倍。例如,b=5a=4,那麼ngx_align(5,4)的結果就是8

在實現原理上,利用了二進制計算的技巧。這裏稍微講一下(已經懂的能夠跳過):

===========我是華麗的分割線===============
a2的某個冪的值時(例如a=2^2=4,或a=2^3=8),有如下特色:

a = 4:  二進制: 0000 0100       從右起,第三位爲1,剩下全爲0;
a = 8:  二進制: 0000 1000       從右起,第四位爲1, 剩下全爲0;
a = 16: 二進制:  0001 0000       從右起,第五位爲1,剩下全爲0;

a - 1 = 3:  二進制: 0000 0011   從右起,第三位以前,全是1;
a - 1 = 7:  二進制: 0000 0111   從右起,第四位以前,全是1;
a - 1 = 15: 二進制: 0000 1111   從右起,第五位以前,全是1;

~(a - 1) = ~3:  二進制: 1111 1100   從右起,第二位以後,全是1;
~(a - 1) = ~7:  二進制: 1111 1000   從右起,第三位以後,全是1;
~(a - 1) = ~15: 二進制: 1111 0000   從右起,第四位以後,全是1;

(理解的關鍵點)一個數,必定是這個數的二進制從右起第一個不爲零的位所表示的數的整數倍
好比:

a = 12:  二進制: 0000 1100 
從右起,第一個不爲零的位所表示的整數爲 0000 0100 即 4
那麼,a = 12 必定是 4 的整數倍

若是,咱們須要任意的一個數a4取整怎麼辦呢?很簡單,只須要把a從右起的若干位置0就能夠了。
好比:

a = 13: 二進制:0000 1101
向0000 0100 即 4 取整,只須要將 0000 1101從右起,前兩位置0,便可獲得,0000 1100 即12

這個置0的過程能夠表達爲0000 1101 &  1111 1100
而 1111 1100 = ~(4 - 1),所以,13 對 4 取整的二進制運算即爲:13 & ~(4 - 1)

能夠看到,這樣的二進制運算的結果是向下取整數倍
可是,在申請內存時,只能比需求的大,而不能比需求的小,所以須要向上取整數倍

對於一個任意的數d和一個2的任意次冪a:
d對a向下取整的二進制運算爲:d & ~(a -1)
d對a向上取整的二進制運算爲:(d + (a - 1)) & ~(a - 1)

相信到這裏,已經能夠很容易理解ngx_align這個宏的含義了

#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))

===========我是華麗的分割線===============

估算hash桶的個數及hash桶的大小

準備工做

如今咱們手頭上有全部索引值及其對應的hash值所組成的ngx_hash_key_t數組,這個數組的大小表示未來造成的hash表中有效元素的數目。

那麼,可知,hash桶數目 * 每一個hash桶能放置元素的個數 > ngx_hash_key_t 數組大小

從上一小節的分析可知:bucket_size的最小值爲2 * sizeof(void*),那麼,預設的ngx_hash_init_t中的bucket_size / 2 * sizeof(void*)就能獲得每一個桶所能放置的衝突項的個數最大值。

所以,ngx_hash_key_t的數組的大小值nelts / (bucket_size / 2 * sizeof(void*))就是預估的hash表所需hash桶數目的最小值。

所以,接下來Nginx有這麼一段:

bucket_size = hinit->bucket_size - sizeof(void *);
    // start 爲預估hash桶數目的最小值,由於會隨着計算不斷向上增長,因此命名爲start
    start = nelts / (bucket_size / (2 * sizeof(void *)));
    start = start ? start : 1;

    if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {
        start = hinit->max_size - 1000;
    }

這部分代碼能夠看做是在估算hash表中hash桶數目的最小值的合理值。

開始估算
ngx_hash_key_t數組中含有全部索引值及其對應的哈希值,爲了將hash表的hash桶數目控制在必定數量內,須要對ngx_hash_key_t內的哈希值進行取模運算

取模運算會將結果控制在必定整數範圍內。
好比:key_hash % size,表示運算獲得的結果必定在[0, size)區間內。

這樣,對於上述代碼獲得的start,咱們就能夠從start開始到hinit->max_size結束,挨個嘗試,看看每一個start所算出的bucket_size是否符合要求(<= hinit->bucket_size

代碼以下:

// 從start 開始,到hints->max_size爲止,挨個嘗試
for (size = start; size <= hinit->max_size; size++) {

        ngx_memzero(test, size * sizeof(u_short));

        for (n = 0; n < nelts; n++) {
            if (names[n].key.data == NULL) {
                continue;
            }
            // 取模運算,將hash值控制在當前size範圍內
            key = names[n].key_hash % size;
            // 累加計算每一個hash值對應的衝突項佔用的hash桶空間
            test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
            // 當某個hash值全部衝突項佔用的hash桶空間超過上限,
            // 表示當前hash桶個數設置過小,進入下一輪循環
            if (test[key] > (u_short) bucket_size) {
                goto next;
            }
        }
        // 找到合適的hash桶個數,接下來進入申請空間及初始化結構體階段
        goto found;

    next:

        continue;
    }

當代碼運行到goto found時,表示找到合適的hash桶數目,能夠進入下一階段:申請內存空間及初始化結構體了。

根據估算的hash表的大小參數,向內存池申請空間

根據上一篇文章對於ngx_hash_t內存的描述,可知須要申請的內存空間分紅兩部分:

1)用於管理hash表的ngx_hash_t結構體,及用於指向內存不一樣分組的ngx_hash_elt_t *數組。根據上一小節的估算,ngx_hash_elt_t *數組的大小爲size
2)用於存放hash表的連續內存塊,其大小爲全部hash桶佔用空間的總和。

這部分的源代碼以下:

// 清空上次爲了估算hash桶數目所存儲在test中的數據
    for (i = 0; i < size; i++) {
        test[i] = sizeof(void *);
    }
    // 從新計算各個hash值所佔用的內存空間
    for (n = 0; n < nelts; n++) {
        if (names[n].key.data == NULL) {
            continue;
        }

        key = names[n].key_hash % size;
        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    }

    len = 0;
    // 將全部hash值佔用的內存空間加和,獲得總共須要的內存空間len
    for (i = 0; i < size; i++) {
        if (test[i] == sizeof(void *)) {
            continue;
        }

        test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));

        len += test[i];
    }

對全部的ngx_hash_key_t從新計算了一遍,獲得了存放全部數據須要的內存空間len

而後,就能夠向ngx_pool_t內存池申請空間了。

if (hinit->hash == NULL) {
        // 在堆上建立hash管理結構體ngx_hash_t,及ngx_hash_elt_t*
        hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t)
                                             + size * sizeof(ngx_hash_elt_t *));
        if (hinit->hash == NULL) {
            ngx_free(test);
            return NGX_ERROR;
        }
        // 將ngx_hash_t** 指針指向hash桶管理結構體
        buckets = (ngx_hash_elt_t **)
                      ((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));

    } else {
        buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *));
        if (buckets == NULL) {
            ngx_free(test);
            return NGX_ERROR;
        }
    }
    // 向內存池申請hash表所使用的內存空間
    elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size);
    if (elts == NULL) {
        ngx_free(test);
        return NGX_ERROR;
    }

    elts = ngx_align_ptr(elts, ngx_cacheline_size);

這段代碼的邏輯分紅兩部分:

1)向內存池申請管理hash表的結構體所使用的內存空間,包括ngx_hash_t結構體及ngx_hash_elt_t*指針數組。
2)向內存池申請hash表所使用的內存空間,直接使用以前計算的len申請。固然,Nginx的源碼中爲了效率,進行了對齊操做。

正確初始化管理結構體的內容

OK, 有了管理結構體,有了hash表,最後的任務就是將管理結構體的各個指針指向正確的地址,各個變量賦於正確的值便可。

// 將ngx_hash_key_t* 數組指向各個內存段
    for (i = 0; i < size; i++) {
        if (test[i] == sizeof(void *)) {
            continue;
        }

        buckets[i] = (ngx_hash_elt_t *) elts;
        elts += test[i];

    }

    for (i = 0; i < size; i++) {
        test[i] = 0;
    }

    // 向hash表中填入正確的內容
    for (n = 0; n < nelts; n++) {
        if (names[n].key.data == NULL) {
            continue;
        }

        key = names[n].key_hash % size;
        elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);

        elt->value = names[n].value;
        elt->len = (u_short) names[n].key.len;

        ngx_strlow(elt->name, names[n].key.data, names[n].key.len);

        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    }

    for (i = 0; i < size; i++) {
        if (buckets[i] == NULL) {
            continue;
        }

        elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);

        elt->value = NULL;
    }

    ngx_free(test);

    // hash表管理結構體賦於正確的內容
    hinit->hash->buckets = buckets;
    hinit->hash->size = size;

這樣,就完成了初始化hash表的整個工做。

總結

我的經驗:理解ngx_hash_t的關鍵點以下:

1)Nginx的哈希表是隻讀的,因此採用了預先估算hash表所需佔用內存的大小的辦法。源碼中大多數的代碼是跟預估hash表大小相關的。
2)Nginx的哈希表的內存空間採用內存池管理,所以理解其內存模型是理解代碼邏輯的關鍵。內存模型請查看上一篇文章
3)Nginx的哈希表的核心是hash表的管理結構體ngx_hash_tngx_hash_elt_t*數組、及hash表內存空間分配。

我的以爲,Nginx的高效並非來自於Nginx有什麼屠龍大法,而是來自於針對性很強的優化,ngx_hash_t就是一個很好的例子。
ngx_hash_t中,隨處可見各類優化,不管是這種特殊的hash表結構設計,仍是內存分配上的各類對齊,都很值得學習。

相關文章
相關標籤/搜索