Nginx 哈希表結構 ngx_hash_t

概述

        關於哈希表的基本知識在前面的文章《數據結構-哈希表》已作介紹。哈希表結合了數組和鏈表的特點,使其尋址、插入以及刪除操作更加方便。哈希表的過程是將關鍵字通過某種哈希函數映射到相應的哈希表位置,即對應的哈希值所在哈希表的位置。但是會出現多個關鍵字映射相同位置的情況導致衝突問題,爲了解決這種情況,哈希表使用兩個可選擇的方法:拉鍊法 和 開放尋址法

        Nginx 的哈希表中使用開放尋址來解決衝突問題,爲了處理字符串,Nginx 還實現了支持通配符操作的相關函數,下面對 Nginx 中哈希表的源碼進行分析。源碼文件:src/core/ngx_hash.h/.c。

哈希表結構

ngx_hash_elt_t 結構 

哈希表中關鍵字元素的結構 ngx_hash_elt_t,哈希表元素結構採用 鍵-值 形式,即<key,value> 。其定義如下:

/* hash散列表中元素的結構,採用鍵值及其所以應的值<key,value>*/
typedef struct {
    void             *value;    /* 指向用戶自定義的數據 */
    u_short           len;      /* 鍵值key的長度 */
    u_char            name[1];  /* 鍵值key的第一個字符,數組名name表示指向鍵值key首地址 */
} ngx_hash_elt_t;
ngx_hash_t 結構 

哈希表基本結構 ngx_hash_t,其結構定義如下:

/* 基本hash散列表結構 */
typedef struct {
    ngx_hash_elt_t  **buckets;  /* 指向hash散列表第一個存儲元素的桶 */
    ngx_uint_t        size;     /* hash散列表的桶個數 */
} ngx_hash_t;

元素結構圖以及基本哈希結構圖如下所示:

                                        


ngx_hash_init_t 初始化結構 

        哈希初始化結構 ngx_hash_init_t,Nginx 的 hash 初始化結構是 ngx_hash_init_t,用來將其相關數據封裝起來作爲參數傳遞給ngx_hash_init(),其定義如下:

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

/* 初始化hash結構 */
typedef struct {
    ngx_hash_t       *hash;         /* 指向待初始化的基本hash結構 */
    ngx_hash_key_pt   key;          /* hash 函數指針 */

    ngx_uint_t        max_size;     /* hash表中桶bucket的最大個數 */
    ngx_uint_t        bucket_size;  /* 每個桶bucket的存儲空間 */

    char             *name;         /* hash結構的名稱(僅在錯誤日誌中使用) */
    ngx_pool_t       *pool;         /* 分配hash結構的內存池 */
    /* 分配臨時數據空間的內存池,僅在初始化hash表前,用於分配一些臨時數組 */
    ngx_pool_t       *temp_pool;
} ngx_hash_init_t;

        哈希元素數據 ngx_hash_key_t,該結構也主要用來保存要 hash 的數據,即鍵-值對<key,value>,在實際使用中,一般將多個鍵-值對保存在 ngx_hash_key_t 結構的數組中,作爲參數傳給ngx_hash_init()。其定義如下:

/* 計算待添加元素的hash元素結構 */
typedef struct {
    ngx_str_t         key;      /* 元素關鍵字 */
    ngx_uint_t        key_hash; /* 元素關鍵字key計算出的hash值 */
    void             *value;    /* 指向關鍵字key對應的值,組成hash表元素:鍵-值<key,value> */
} ngx_hash_key_t;

哈希操作

        哈希操作包括初始化函數、查找函數;其中初始化函數是 Nginx 中哈希表比較重要的函數,由於 Nginx 的 hash 表是靜態只讀的,即不能在運行時動態添加新元素的,一切的結構和數據都在配置初始化的時候就已經規劃完畢。

哈希函數

        哈希表中使用哈希函數把用戶數據映射到哈希表對應的位置中,下面是 Nginx 哈希函數的定義:

/* hash函數 */
#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 ngx_hash_key_lc(u_char *data, size_t len);
ngx_uint_t ngx_hash_strlow(u_char *dst, u_char *src, size_t n);
#define ngx_hash(key, c)   ((ngx_uint_t) key * 31 + c)
/* hash函數 */
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++) {
        /* 調用宏定義的hash 函數 */
        key = ngx_hash(key, data[i]);
    }

    return key;
}


/* 這裏只是將字符串data中的所有字符轉換爲小寫字母再進行hash值計算 */
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++) {
        /* 把字符串轉換爲小寫字符,並計算每個字符的hash值 */
        key = ngx_hash(key, ngx_tolower(data[i]));
    }

    return key;
}


/* 把原始關鍵字符串的前n個字符轉換爲小寫字母再計算hash值
 * 注意:這裏只計算前n個字符的hash值
 */
ngx_uint_t
ngx_hash_strlow(u_char *dst, u_char *src, size_t n)
{
    ngx_uint_t  key;

    key = 0;

    while (n--) {/* 把src字符串的前n個字符轉換爲小寫字母 */
        *dst = ngx_tolower(*src);
        key = ngx_hash(key, *dst);/* 計算所轉換小寫字符的hash值 */
        dst++;
        src++;
    }

    return key;/* 返回整型的hash值 */
}

哈希初始化函數

        hash 初始化由 ngx_hash_init() 函數完成,其 names 參數是 ngx_hash_key_t 結構的數組,即鍵-值對 <key,value> 數組,nelts 表示該數組元素的個數。該函數初始化的結果就是將 names 數組保存的鍵-值對<key,value>,通過 hash 的方式將其存入相應的一個或多個 hash 桶(即代碼中的 buckets )中。hash 桶裏面存放的是 ngx_hash_elt_t 結構的指針(hash元素指針),該指針指向一個基本連續的數據區。該數據區中存放的是經 hash 之後的鍵-值對<key',value'>,即 ngx_hash_elt_t 結構中的字段 <name,value>。每一個這樣的數據區存放的鍵-值對<key',value'>可以是一個或多個。其定義如下:

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

/* 初始化hash結構函數 */
/* 參數hinit是hash表初始化結構指針;
 * name是指向待添加在hash表結構的元素數組;
 * nelts是待添加元素數組中元素的個數;
 */
ngx_int_t
ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
{
    u_char          *elts;
    size_t           len;
    u_short         *test;
    ngx_uint_t       i, n, key, size, start, bucket_size;
    ngx_hash_elt_t  *elt, **buckets;

    for (n = 0; n < nelts; n++) {
        /* 若每個桶bucket的內存空間不足以存儲一個關鍵字元素,則出錯返回
         * 這裏考慮到了每個bucket桶最後的null指針所需的空間,即該語句中的sizeof(void *),
         * 該指針可作爲查找過程中的結束標記
         */
        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;
        }
    }

    /* 臨時分配sizeof(u_short)*max_size的test空間,即test數組總共有max_size個元素,即最大bucket的數量,
     * 每個元素會累計落到相應hash表位置的關鍵字長度,
     * 當大於256字節,即u_short所表示的字節大小,
     * 則表示bucket較少
     */
    test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
    if (test == NULL) {
        return NGX_ERROR;
    }

    /* 每個bucket桶實際容納的數據大小,
     * 由於每個bucket的末尾結束標誌是null,
     * 所以bucket實際容納的數據大小必須減去一個指針所佔的內存大小
     */
    bucket_size = hinit->bucket_size - sizeof(void *);

    /* 估計hash表最少bucket數量;
     * 每個關鍵字元素需要的內存空間是 NGX_HASH_ELT_SIZE(&name[n]),至少需要佔用兩個指針的大小即2*sizeof(void *)
     * 這樣來估計hash表所需的最小bucket數量
     * 因爲關鍵字元素內存越小,則每個bucket所容納的關鍵字元素就越多
     * 那麼hash表的bucket所需的數量就越少,但至少需要一個bucket
     */
    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;
    }

    /* 以前面估算的最小bucket數量start,通過測試數組test估算hash表容納 nelts個關鍵字元素所需的bucket數量
     * 根據需求適當擴充bucket的數量
     */
    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值計算存在到測試數組test對應的位置中,即計算bucket在hash表中的編號key,key取值爲0~size-1 */
            key = names[n].key_hash % size;
            test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));

#if 0
            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "%ui: %ui %ui \"%V\"",
                          size, key, test[key], &names[n].key);
#endif

            /* test數組中對應的內存大於每個桶bucket最大內存,則需擴充bucket的數量
             * 即在start的基礎上繼續增加size的值
             */
            if (test[key] > (u_short) bucket_size) {
                goto next;
            }
        }

        /* 若size個bucket桶可以容納name數組的所有關鍵字元素,則表示找到合適的bucket數量大小即爲size */
        goto found;

    next:

        continue;
    }

    ngx_log_error(NGX_LOG_WARN, hinit->pool->log, 0,
                  "could not build optimal %s, you should increase "
                  "either %s_max_size: %i or %s_bucket_size: %i; "
                  "ignoring %s_bucket_size",
                  hinit->name, hinit->name, hinit->max_size,
                  hinit->name, hinit->bucket_size, hinit->name);

found:

    /* 到此已經找到合適的bucket數量,即爲size
     * 重新初始化test數組元素,初始值爲一個指針大小
     */
    for (i = 0; i < size; i++) {
        test[i] = sizeof(void *);
    }

    /* 計算每個bucket中關鍵字所佔的空間,即每個bucket實際所容納數據的大小,
     * 必須注意的是:test[i]中還有一個指針大小
     */
    for (n = 0; n < nelts; n++) {
        if (names[n].key.data == NULL) {
            continue;
        }

        /* 根據hash值計算出關鍵字放在對應的test[key]中,即test[key]的大小增加一個關鍵字元素的大小 */
        key = names[n].key_hash % size;
        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    }

    len = 0;

    /* 調整成對齊到cacheline的大小,並記錄所有元素的總長度 */
    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];
    }

    /*
     * 向內存池申請bucket元素所佔的內存空間,
     * 注意:若前面沒有申請hash表頭結構,則在這裏將和ngx_hash_wildcard_t一起申請
     */
    if (hinit->hash == NULL) {
        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;
        }

        /* 計算buckets的起始位置 */
        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;
        }
    }

    /* 分配elts,對齊到cacheline大小 */
    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);

    /* 將buckets數組與相應的elts對應起來,即設置每個bucket對應實際數據的地址 */
    for (i = 0; i < size; i++) {
        if (test[i] == sizeof(void *)) {
            continue;
        }

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

    }

    /* 清空test數組,以便用來累計實際數據的長度,這裏不計算結尾指針的長度 */
    for (i = 0; i < size; i++) {
        test[i] = 0;
    }

    /* 依次向各個bucket中填充實際數據 */
    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]記錄當前bucket內容的填充位置,即下一次填充的起始位置 */
        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    }

    /* 設置bucket結束位置的null指針 */
    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);

    hinit->hash->buckets = buckets;
    hinit->hash->size = size;

#if 0

    for (i = 0; i < size; i++) {
        ngx_str_t   val;
        ngx_uint_t  key;

        elt = buckets[i];

        if (elt == NULL) {
            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "%ui: NULL", i);
            continue;
        }

        while (elt->value) {
            val.len = elt->len;
            val.data = &elt->name[0];

            key = hinit->key(val.data, val.len);

            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "%ui: %p \"%V\" %ui", i, elt, &val, key);

            elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
                                                   sizeof(void *));
        }
    }

#endif

    return NGX_OK;
}

哈希查找函數

        hash 查找操作由 ngx_hash_find() 函數完成,查找時由 key 直接計算所在的 bucket,該 bucket 中保存其所在 ngx_hash_elt_t 數據區的起始地址;然後根據長度判斷並用 name 內容匹配,匹配成功,其 ngx_hash_elt_t 結構的 value 字段即是所求。其定義如下:

/* 查找hash元素 */
void *
ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len)
{
    ngx_uint_t       i;
    ngx_hash_elt_t  *elt;

#if 0
    ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, "hf:\"%*s\"", len, name);
#endif

    /* 由key找到元素在hash表中所在bucket的位置 */
    elt = hash->buckets[key % hash->size];

    if (elt == NULL) {
        return NULL;
    }

    while (elt->value) {
        if (len != (size_t) elt->len) {/* 判斷長度是否相等 */
            goto next;
        }

        for (i = 0; i < len; i++) {
            if (name[i] != elt->name[i]) {/* 若長度相等,則比較name的內容 */
                goto next;
            }
        }

        /* 匹配成功,則返回value字段 */
        return elt->value;

    next:

        elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
                                               sizeof(void *));
        continue;
    }

    return NULL;
}

測試程序:

#include <stdio.h>
#include "ngx_config.h"
#include "ngx_conf_file.h"
#include "nginx.h"
#include "ngx_core.h"
#include "ngx_string.h"
#include "ngx_palloc.h"
#include "ngx_array.h"
#include "ngx_hash.h"
volatile ngx_cycle_t  *ngx_cycle;
void ngx_log_error_core(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, const char *fmt, ...) { }


static ngx_str_t names[] = {ngx_string("www.baidu.com"),
                            ngx_string("www.google.com.hk"),
                            ngx_string("www.github.com")};
static char* descs[] = {"baidu: 1","google: 2", "github: 3"};

// hash table的一些基本操作
int main()
{
    ngx_uint_t          k;
    ngx_pool_t*         pool;
    ngx_hash_init_t     hash_init;
    ngx_hash_t*         hash;
    ngx_array_t*        elements;
    ngx_hash_key_t*     arr_node;
    char*               find;
    int                 i;
    ngx_cacheline_size = 32;

    pool = ngx_create_pool(1024*10, NULL);

    /* 分配hash表基本結構內存 */
    hash = (ngx_hash_t*) ngx_pcalloc(pool, sizeof(hash));
    /* 初始化hash結構 */
    hash_init.hash      = hash;                      // hash結構
    hash_init.key       = &ngx_hash_key_lc;          // hash函數
    hash_init.max_size  = 1024*10;                   // max_size
    hash_init.bucket_size = 64;
    hash_init.name      = "test_hash_error";
    hash_init.pool           = pool;
    hash_init.temp_pool      = NULL;

    /* 創建數組,把關鍵字壓入到數組中 */

    elements = ngx_array_create(pool, 32, sizeof(ngx_hash_key_t));
    for(i = 0; i < 3; i++) {
        arr_node            = (ngx_hash_key_t*) ngx_array_push(elements);
        arr_node->key       = (names[i]);
        arr_node->key_hash  = ngx_hash_key_lc(arr_node->key.data, arr_node->key.len);
        arr_node->value     = (void*) descs[i];

        printf("key: %s , key_hash: %u\n", arr_node->key.data, arr_node->key_hash);
    }

    /* hash初始化函數 */
    if (ngx_hash_init(&hash_init, (ngx_hash_key_t*) elements->elts, elements->nelts) != NGX_OK){
        return 1;
    }

    /* 查找hash 元素 */
    k    = ngx_hash_key_lc(names[0].data, names[0].len);
    printf("%s key is %d\n", names[0].data, k);
    find = (char*)
        ngx_hash_find(hash, k, (u_char*) names[0].data, names[0].len);

    if (find) {
        printf("get desc: %s\n",(char*) find);
    }

    ngx_array_destroy(elements);
    ngx_destroy_pool(pool);

    return 0;
}

輸出結果:

 ./hash_test 
key: www.baidu.com , key_hash: 270263191
key: www.google.com.hk , key_hash: 2472785358
key: www.github.com , key_hash: 2818415021
www.baidu.com key is 270263191
get desc: baidu: 1


參考資料:

《深入理解 Nginx 》

nginx中hash表的設計與實現

Nginx 代碼研究

發佈了214 篇原創文章 · 獲贊 44 · 訪問量 42萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章