Nginx 源碼分析:ngx_hash_t(上)

源文件路徑

版本:1.8.0node

csrc\core\Ngx_hash.h
src\core\Ngx_hash.c

關於hash表

Nginx實現的hash表和常見的hash表大致一致,細節有區別,因此,要了解ngx_hash_t最好對hash表的基礎概念進行一下梳理。算法

數組與hash表

從查詢的角度來看,數組根據索引值的查詢速度很快快。
緣由在於數組內元素的位置是基於數組起始位置的絕對位置,並且數組的存儲空間是連續的,能夠根據下標直接操做指針跳轉。segmentfault

雖然數組的查詢速度很快,可是數組的索引值必須是數值,這就很討厭了。
由於不少狀況下,索引值並非數字,而是字符串什麼的。好比用名字來索引一我的。數組

解決這個問題的一個很容易的辦法就是給每一個人安排一個學號(先不考慮重名的狀況),那麼,在實際存儲時,按照學號爲索引值的數組來存儲對應的信息;在查詢時,只須要知道名字,就能夠獲得名字對應的學號,根據學號能夠直接從數組中取出信息數據結構

這個解決方法中有兩個主要部分:框架

  1. 創建從名字到學號的對應關係;
  2. 創建以學號爲索引值的數組;

從名字到學號的對應關係能夠抽象成從字符串到數值的對應關係,這種對應關係,在數學上表示就是f(k)。其中k表示一個字符串(索引關鍵字),函數f表示從字符串到數值的對應關係,f(k)表示k通過f映射獲得的值。函數

只要有了f(k),那麼將f(k)做爲數組的下標便可獲取k所對應的信息。
性能

k------>f(k)------->info[f(k)]

其中,從kf(k)的映射函數稱爲哈希函數,數組info[]稱爲哈希(hash)表優化

hash表的問題及解決方法

理想是豐滿的,現實是骨感的。hash表在創建時最關鍵之處在於找到合適的哈希函數,使得:ui

  1. kf(k)之間是一一映射的。即,保證給定對於k存在惟一的f(k)與之對應,同時對於f(k)存在惟一的k與之對應。
  2. f(k)的集合是連續的。即,對於數組info[]而言,不存在數組項爲空的狀況,能夠更加充分利用資源。

惋惜,知足上述條件的哈希函數很是困難。
如今使用的各類哈希函數基本上只能保證較小几率出現兩個不一樣的kf(k)相同的狀況
基本不能保證f(k)的集合是連續的

由於f(k)的集合不是連續的,因此哈希表也被稱爲散列表,哈希函數也被稱爲散列函數。
而出現兩個k值對應的f(k)相同的狀況,稱爲哈希衝突。

解決哈希衝突常見的辦法
出現散列狀況表示可能浪費一點資源,這是能夠接受的。可是出現衝突表示會發生信息覆蓋,這是錯誤,不能接受。因此,必須解決哈希衝突。

解決哈希衝突的常見的方法有:
1) 開放地址法;2)再哈希法;3)鏈地址法;

具體內容請自行google,這裏就不去挖老墳了。

哈希表的創建

從上述的分析可知,創建哈希表有兩個主要環節:

1)創建哈希函數;
2)創建哈希表(都是窟窿的數組)

其中,爲了解決哈希衝突(假設採用鏈地址法),所創建的哈希表(數組)裏的元素多是一個鏈表或者一個數組。也就是說,哈希表是一個二維的結構。
同時,對於索引關鍵字,要求哈希函數得到的哈希值控制在必定範圍內。

所以,哈希表大概長成這個樣子:

ctypedef struct node_s{
     char    *key;
     char    *val;
     node_t  *next;
}node_t;

#define HASHSIZE 101
node_t* hashtable[HASHSIZE];

其中hashtable表示哈希表,key表示索引值,好比上述例子中某個學生的名字,node_t表示哈希表中存儲的信息,同時也能夠看到node_t是鏈表的一個節點,用於解決哈希衝突。

假設key的值是字符串"xiaoming",根據某個哈希函數,得出的值爲6,那麼"xiaoming"的信息就能夠從hashtable[6]鏈表中取得,這樣再去遍歷hashtable[6]這個鏈表,找到key等於"xiaoming"的鏈表節點,其val就是要查找的值。

從上述分析,可知,hash表是一種拿空間換取時間的數據結構。
關於hash表的各類實現方法及算法的算法複雜度,請自行google。


Nginx中的哈希表

須要指出的是,Nginx中自造的哈希表屬於內部使用的數據結構,所以,並非一個通用的哈希表。此外,爲了提升效率,做者作了至關多的優化,這些優化使得Nginx中的哈希表與常規的哈希表長得不同。

例如,Nginx的哈希表一經初始化就不可更改,既不能增長元素,也不能刪除元素。
這樣作主要是由於Nginx的哈希表用於解決相似於http模塊中域名匹配的問題,這些域名在配置文件中配置,一旦讀取配置文件,這些信息是不可修改的,所以,沒有增刪的需求。

另外,因爲Nginx哈希表的這種只讀特色,使得能夠在性能上有很大的可優化空間。
Nginx也確實在這上面做了不少文章。

數據結構

根據哈希表的概念可知:哈希表自己就是一個數組,所以,是一塊連續的內存空間
Nginx中,內存的管理都是經過ngx_pool_t來管理的(不清楚的請移步這裏),所以,須要一個用來管理這塊連續內存的結構體。

可是因爲哈希表爲了解決衝突問題,一般採用鏈地址法,因此,這個管理內存的結構體會使用指針的指針

另外,因爲Nginx的哈希表是只讀的,衝突的元素個數能夠在初始化是肯定,因此使用數組來代替鏈表解決衝突是更優的選擇。

這個用來代替鏈表的數組還有個名字叫hash桶,因此,會在Nginx源碼中看到buckets這樣的命名。

Nginx的哈希表在內存上大概是長這個樣子的:
圖片描述

假設理想狀況,全部的索引值key通過哈希函數映射後f(k)集合的大小爲4

爲了解決衝突,咱們將每一個f(k)對應的數組大小設定爲2。這樣,咱們的hash表在邏輯上就變成了一個4x2的數組。

固然,爲了更好的說明狀況,這裏假設哈希函數是理想的,所以,hash表不存在未使用的部分

因此,在內存上,Nginx哈希表的本尊,就是一段連續的內存空間,此外,還須要兩個用來管理這段內存空間的數據結構。

1)大小爲4的數組,類型爲ngx_hash_elt_t *,用來分別指向不一樣的內存段,表示每一個hash桶
2)類型爲ngx_hash_elt_t **的指針buckets,用來表示hash桶數組

因爲指針的指針能夠完整的表示二維數組,所以,ngx_hash_elt_t *數組並不須要定義。只需定義ngx_hash_elt_t來表示hash表中的每一個元素便可。

所以,Nginx哈希表的核心數據結構以下:

ngx_hash_elt_t用來表示hash表的元素。

ctypedef struct {
    void             *value;
    u_short           len;
    u_char            name[1];
} ngx_hash_elt_t;

ngx_hash_t用來表示整個hash表。

ctypedef struct {
    ngx_hash_elt_t  **buckets;
    ngx_uint_t        size;
} ngx_hash_t;

經過buckets這個指針的指針能夠完整的訪問二維數組。

Nginx中是如何使用這兩個數據結構的呢?或者簡化一下,Nginx是如何初始化這兩個數據結構的呢?

首先,做爲管理內存的結構體,ngx_hash_t既能夠做爲局部變量在棧上出現,也能夠做爲堆上的變量,使用ngx_pool_t管理。

以堆爲例,

ngx_hash_t  *hash;
// 向ngx_pool_t申請空間,用於存放管理結構體ngx_hash_t及4個 ngx_hash_elt_t指針
hash = ngx_pcalloc(pool, sizeof(ngx_hash_t) + 4*sizeof(ngx_hash_elt_t *));

u_char *elts;
// 向ngx_pool_t申請hash表自己使用的連續內存塊
elts = ngx_palloc(pool, 4 * 2 * sizeof(ngx_hash_elt_t));

ngx_hash_elt_t **buckets;
// 將管理結構體成員變量賦於正確的值。
for (i = 0; i < 4; i++) {
    buckets[i] = (ngx_hash_elt_t *) elts;  // 4個ngx_hash_elt_t指針指向正確地址;
    elts += 2 * sizeof(ngx_hash_elt_t);
}
hash->buckets = buckets;
hash->size = 4;

這段代碼,在內存池中申請了一段連續的內存,分別用於1ngx_hash_t4ngx_hash_elt_t *

這樣就把管理hash表那段連續內存塊使用的ngx_hash_elt_t** bucketsngx_hash_elt_t*數組一塊兒建立了。

而後依次給每一個ngx_hash_elt_t *賦值,使其指向正確的內存地址。


說明
以上代碼自Nginx源碼中簡化而來,去除了許多用於優化的代碼。

因爲ngx_hash_t內容較多,這裏只從設計角度分析了Nginx中的hash表。主要目的在於理清總體框架及思路。

細節部分,後續添加。先到這裏。

相關文章
相關標籤/搜索