菜鳥nginx源碼剖析數據結構篇(六) 哈希表 ngx_hash_t(上)nginx
Author:Echo Chen(陳斌)算法
Email:chenb19870707@gmail.com數組
Blog:Blog.csdn.net/chen19870707緩存
Date:October 31h, 2014數據結構
哈希表是一種典型的以空間換取時間的數據結構,在沒有衝突的狀況下,對任意元素的插入、索引、刪除的時間複雜度都是O(1)。這樣優秀的時間複雜度是經過將元素的key值以hash方法f映射到哈希表中的某一個位置來訪問記錄來實現的,即鍵值爲key的元素一定存儲在哈希表中的f(key)的位置。固然,不一樣的元素的hash值可能相同,這就是hash衝突,有兩種解決方法(分離鏈表發和開放地址發),ngx採用的是開放地址法.併發
分離鏈表法是經過將衝突的元素連接在一個哈希表外的一個鏈表中,這樣,找到hash表中的位置後,就能夠經過遍歷這個單鏈表來找到這個元素。
開放地址法是插入的時候發現 本身的位置f(key)已經被佔了,就向後遍歷,查看f(key)+1的位置是否被佔用,若是沒被佔用,就佔用它,不然繼續相後,查詢的時候,一樣也若是f(key)不是須要的值,也依次向後遍歷,一直找到須要的元素。
頭文件:http://trac.nginx.org/nginx/browser/nginx/src/core/ngx_hash.happ
源文件:http://trac.nginx.org/nginx/browser/nginx/src/core/ngx_hash.c函數
ngx_hash的內存佈局以下圖,它採用了三級管理結構,只要由如下幾個結構足證:高併發
ngx_hash_elt是哈希表的元素,它負責存儲key-value值,其中key爲name 、value爲value,這裏看到name僅爲一個字節的uchar數組,僅用於指出key的首地址,而key的長度是可變的,因此哈希表元素的大小並非由sizeof(ngx_hash_elt_t_t)決定的,而是在初始化時指定的。佈局
1: typedef struct {
2: void *value; //指向用戶自定義元素的指針,若是該位置沒有元素,即爲NULL
3: u_short len; //key的長度
4: u_char name[1]; //key的首地址
5: } ngx_hash_elt_t;
哈希表結構是一個ngx_hash_elt_t的數組,其中buckets指向哈希表的首地址,也是第一個槽的地址,size爲哈希表中槽的總個數
1: typedef struct {
2: ngx_hash_elt_t **buckets;
3: ngx_uint_t size;
4: } ngx_hash_t;
ngx_hash_wildcard_t專用於表示牽制或後置通配符的哈希表,如:前置*.test.com,後置:www.test.* ,它只是對ngx_hash_t的簡單封裝,是由一個基本哈希表hash和一個額外的value指針,當使用ngx_hash_wildcard_t通配符哈希表做爲容器元素時,可使用value指向用戶數據。
1: typedef struct {
2: ngx_hash_t hash;
3: void *value;
4: } ngx_hash_wildcard_t;
ngx_hash_combined_t是由3個哈希表組成,一個普通hash表hash,一個包含前向通配符的hash表wc_head和一個包含後向通配符的hash表 wc_tail。
1: typedef struct {
2: ngx_hash_t hash;
3: ngx_hash_wildcard_t *wc_head;
4: ngx_hash_wildcard_t *wc_tail;
5: } ngx_hash_combined_t;
·hash初始化結構是ngx_hash_init_t,ngx_hash_init用於初始化哈希表,初始化哈希表的槽的總數並非徹底由max_size成員決定的,而是由在作初始化時預先加入到哈希表的全部元素決定的,包括這些元素的老是、每一個元素的關鍵字長度等,還包括操做系統的頁面大小,這個算法比較複雜,能夠在ngx_hash_init函數中找到這個算法它的結構以下:
1: typedef struct {
2: ngx_hash_t *hash; //指向普通的徹底匹配哈希表
3: ngx_hash_key_pt key; //哈希方法
4:
5: ngx_uint_t max_size; //哈希表中槽的最大個數
6: ngx_uint_t bucket_size; //哈希表中一個槽的空間大小,不是sizeof(ngx_hash_elt_t)
7:
8: char *name; //哈希表的名稱
9: ngx_pool_t *pool; //內存池,它負責分配基本哈希列表、前置通配哈希列表、後置哈希列表中全部槽
10: ngx_pool_t *temp_pool; //臨時內存池,它僅存在初始化哈希表以前。用於分配一些臨時的動態數組,帶通配符的元素初始化時須要用到臨時動態數組
11: } ngx_hash_init_t;
ngx_hash_key_t用於表示即將添加到哈希表中的元素,其結構以下:
1: typedef struct {
2: ngx_str_t key; //元素關鍵字
3: ngx_uint_t key_hash; //由哈希方法算出來的哈希值
4: void *value; //指向用戶自定義數據
5: } ngx_hash_key_t;
能夠看到,這裏設計了3個簡易的哈希列表( keys_hash、dns_wc_head_hash、dns_wc_tail_hash),即採用分離鏈表法來解決衝突,這樣作的好處是若是沒有這三個次啊用分離鏈表法來解決衝突的建議哈希列表,那麼每添加一個關鍵字元素都要遍歷數組(數組採用開放地址法解決衝突,衝突就必須遍歷)。
1: typedef struct {
2: ngx_uint_t hsize; //散列中槽總數
3:
4: ngx_pool_t *pool; //內存池,用於分配永久性的內存
5: ngx_pool_t *temp_pool; //臨時內存池,下面的臨時動態數組都是由臨時內存池分配
6:
7: ngx_array_t keys; //存放全部非通配符key的數組。
8: ngx_array_t *keys_hash; //這是個二維數組,第一個維度表明的是bucket的編號,那麼keys_hash[i]中存放的是全部的key算出來的hash值對hsize取模之後的值爲i的key。假設有3個key,分別是key1,key2和key3假設hash值算出來之後對hsize取模的值都是i,那麼這三個key的值就順序存放在keys_hash[i][0],keys_hash[i][1], keys_hash[i][2]。該值在調用的過程當中用來保存和檢測是否有衝突的key值,也就是是否有重複。
9:
10: ngx_array_t dns_wc_head; //存放前向通配符key被處理完成之後的值。好比:「*.abc.com」被處理完成之後,變成「com.abc.」被存放在此數組中。
11: ngx_array_t *dns_wc_head_hash; //該值在調用的過程當中用來保存和檢測是否有衝突的前向通配符的key值,也就是是否有重複。
12:
13: ngx_array_t dns_wc_tail; //存放後向通配符key被處理完成之後的值。好比:「mail.xxx.*」被處理完成之後,變成「mail.xxx.」被存放在此數組中。
14: ngx_array_t *dns_wc_tail_hash; //該值在調用的過程當中用來保存和檢測是否有衝突的後向通配符的key值,也就是是否有重複。
15: } ngx_hash_keys_arrays_t;
初始化設計操做設計仍是很巧妙的,巧妙的結構設計在這裏都獲得體現,主要有:
- 桶大小估算,這裏一開始 按照 ngx_hash_elt_t估算最小須要的桶的數目,而後再從這個數目開始搜索,大大提升了效率,值得學習。
- ngx_hash_elt_t中uchar name[1]的設計,若是在name很短的狀況下,name和 ushort 的字節對齊可能只用佔到一個字節,這樣就比放一個uchar* 的指針少佔用一個字節,能夠看到ngx是真的在內存上考慮,節省每一份內存來提升併發。
先看一下求ngx_hash_elt_t的佔用內存大小的方法,前面提到不是用sizeof(ngx_hash_elt_t),緣由是由於name的特殊設計,正確的求法以下,能夠看到是一個sizeof(void*) 即用戶自定義指針(value),一個長度len(sizeof(unsigned short)) 和 name 的真實長度len 對void*字節對齊。
1: typedef struct {
2: void *value;
3: u_short len;
4: u_char name[1];
5: } ngx_hash_elt_t;
6:
7: #define NGX_HASH_ELT_SIZE(name) \
8: (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
以下是註釋版源代碼,比較長,總的流程即爲:預估須要的桶數量 –> 搜索須要的桶數量->分配桶內存->初始化每個ngx_hash_elt_t
1: //hinit是哈希表初始化結構指針,names是預添加到哈希表結構的數組,nelts爲names元素個數
2: ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
3: {
4: u_char *elts;
5: size_t len;
6: u_short *test;
7: ngx_uint_t i, n, key, size, start, bucket_size;
8: ngx_hash_elt_t *elt, **buckets;
9:
10: //遍歷預添加數組names,數組的每個元素,判斷槽的大小bucket_size是否夠分配
11: for (n = 0; n < nelts; n++)
12: {
13: if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))
14: {
15: //有任何一個元素,槽的大小不夠爲該元素分配空間,則退出
16: ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
17: "could not build the %s, you should "
18: "increase %s_bucket_size: %i",
19: hinit->name, hinit->name, hinit->bucket_size);
20: return NGX_ERROR;
21: }
22: }
23:
24: //test 是short數組,用於臨時保存每一個桶的當前大小
25: test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
26: if (test == NULL) {
27: return NGX_ERROR;
28: }
29:
30: // 爲何會多一個指針大小呢?這裏主要仍是爲了後面將每一個元素對齊到指針
31: bucket_size = hinit->bucket_size - sizeof(void *);
32:
33: /* 計算須要桶數目的下界
34: 每一個元素最少須要 NGX_HASH_ELT_SIZE(&name[n]) > (2*sizeof(void*)) 的空間
35: 所以 bucket_size 大小的桶最多能容下 bucket_size/(2*sizeof(void*)) 個元素
36: 所以 nelts 個元素就最少須要start個桶。
37: */
38: start = nelts / (bucket_size / (2 * sizeof(void *)));
39: start = start ? start : 1;
40:
41: if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {
42: start = hinit->max_size - 1000;
43: }
44:
45: /* 從最小桶數目開始試,計算容下 nelts 個元素須要多少個桶 */
46: for (size = start; size <= hinit->max_size; size++) {
47: ngx_memzero(test, size * sizeof(u_short));
48:
49: for (n = 0; n < nelts; n++) {
50: if (names[n].key.data == NULL) {
51: continue;
52: }
53:
54: //根據哈希值計算計算要放在哪一個桶
55: key = names[n].key_hash % size;
56: //將桶大小增長一個ngx_hash_elt_t
57: test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
58:
59: #if 0
60: ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
61: "%ui: %ui %ui \"%V\"",
62: size, key, test[key], &names[n].key);
63: #endif
64:
65: //發現放在size 個桶中,仍是有放不下的狀況,因此須要的桶+1,再循環
66: if (test[key] > (u_short) bucket_size) {
67: goto next;
68: }
69: }
70:
71: //names中全部元素均可以放入size個桶中,找到正確的size大小了
72: goto found;
73:
74: next:
75:
76: continue;
77: }
78:
79: ngx_log_error(NGX_LOG_WARN, hinit->pool->log, 0,
80: "could not build optimal %s, you should increase "
81: "either %s_max_size: %i or %s_bucket_size: %i; "
82: "ignoring %s_bucket_size",
83: hinit->name, hinit->name, hinit->max_size,
84: hinit->name, hinit->bucket_size, hinit->name);
85:
86: found:
87: /* 執行到這裏就獲得了 容下 nelts 個元素須要 size 個桶 ,初始化每一個桶大小*/
88: for (i = 0; i < size; i++) {
89: test[i] = sizeof(void *);
90: }
91:
92: //計算實際上每一個桶的大小
93: for (n = 0; n < nelts; n++) {
94: if (names[n].key.data == NULL) {
95: continue;
96: }
97:
98: //根據哈希值,應該放在第key個桶中,大小增長一個ngx_hash_elt_t
99: key = names[n].key_hash % size;
100: test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
101: }
102:
103: len = 0;
104:
105: for (i = 0; i < size; i++) {
106: //桶中沒有元素
107: if (test[i] == sizeof(void *)) {
108: continue;
109: }
110:
111: //行緩存對其,CPU讀取內存不是一個一個字節,而是以cacheline_size爲單位,以行緩存對其,提升CPU讀取效率
112: test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));
113:
114: len += test[i];
115: }
116:
117: // 這裏彷佛看起來很奇怪,既然是hash,爲何分配空間的大小又跟hash結構體一點關聯都沒有呢
118: // 這裏頗有意思,由於ngx_hash_wildchard_t包含hash這個結構體,因此就一塊兒分配了
119: // 而且把每一個桶的指針也分配在一塊兒了,這種思考跟之前學的面向對象思想很不同,但這樣會很高效
120: if (hinit->hash == NULL) {
121: hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t)
122: + size * sizeof(ngx_hash_elt_t *));
123: if (hinit->hash == NULL) {
124: ngx_free(test);
125: return NGX_ERROR;
126: }
127:
128: buckets = (ngx_hash_elt_t **)
129: ((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));
130:
131: } else {
132: //分配桶
133: buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *));
134: if (buckets == NULL) {
135: ngx_free(test);
136: return NGX_ERROR;
137: }
138: }
139:
140: //將內存池對其到行緩存,提升CPU讀取效率
141: elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size);
142: if (elts == NULL) {
143: ngx_free(test);
144: return NGX_ERROR;
145: }
146:
147: //對指針地址的對齊操做
148: elts = ngx_align_ptr(elts, ngx_cacheline_size);
149:
150: for (i = 0; i < size; i++) {
151: if (test[i] == sizeof(void *)) {
152: continue;
153: }
154: // 給bucket每一個桶地址賦值
155: buckets[i] = (ngx_hash_elt_t *) elts;
156: elts += test[i];
157:
158: }
159: // 清空從新計算
160: for (i = 0; i < size; i++) {
161: test[i] = 0;
162: }
163:
164: for (n = 0; n < nelts; n++) {
165: if (names[n].key.data == NULL) {
166: continue;
167: }
168:
169: //根據哈希值找到桶
170: key = names[n].key_hash % size;
171:
172: //找到每一個元素的地址
173: elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);
174:
175: //給value和len賦值
176: elt->value = names[n].value;
177: elt->len = (u_short) names[n].key.len;
178:
179: //拷貝name,name長度在前面計算size時已經算好了
180: ngx_strlow(elt->name, names[n].key.data, names[n].key.len);
181:
182: //增長這個桶的索引
183: test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
184: }
185:
186: // 設置每一個桶的結束元素爲NULL
187: for (i = 0; i < size; i++) {
188: if (buckets[i] == NULL) {
189: continue;
190: }
191:
192: elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);
193:
194: elt->value = NULL;
195: }
196:
197: //釋放臨時動態數組
198: ngx_free(test);
199:
200: //給哈希表賦值
201: hinit->hash->buckets = buckets;
202: hinit->hash->size = size;
203: return NGX_OK;
204: }
首先看一下ngx_hash_wildcard_init 的內存結構,當構造此類型的hash表的時候,其實是構造了一個hash表的一個「鏈表」,是經過hash表中的key「連接」起來的。好比:對於「*.abc.com」將會構造出2個hash表,第一個hash表中有一個key爲com的表項,該表項的value包含有指向第二個hash表的指針,而第二個hash表中有一個表項abc,該表項的value包含有指向*.abc.com對應的value的指針。那麼查詢的時候,好比查詢www.abc.com的時候,先查com,經過查com能夠找到第二級的hash表,在第二級hash表中,再查找abc,依次類推,直到在某一級的hash表中查到的表項對應的value對應一個真正的值而非一個指向下一級hash表的指針的時候,查詢過程結束。
理解了這個,咱們就能夠看源代碼了,ngx_hash_wildcard是一個遞歸函數,遞歸建立如上圖的hash鏈表,以下爲註釋版源代碼。
精彩的讀點有:
1: /*hinit爲初始化結構體指針,names爲預加入哈希表數組,elts爲預加入數組大小
2: 特別要注意的是這裏的key已經都是被預處理過的。例如:「*.abc.com」或者「.abc.com」被預處理完成之後,
3: 變成了「com.abc.」。而「mail.xxx.*」則被預處理爲「mail.xxx.」*/
4: ngx_int_t ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,ngx_uint_t nelts)
5: {
6: size_t len, dot_len;
7: ngx_uint_t i, n, dot;
8: ngx_array_t curr_names, next_names;
9: ngx_hash_key_t *name, *next_name;
10: ngx_hash_init_t h;
11: ngx_hash_wildcard_t *wdc;
12:
13: //初始化臨時動態數組curr_names,curr_names是存放當前關鍵字的數組
14: if (ngx_array_init(&curr_names, hinit->temp_pool, nelts,
15: sizeof(ngx_hash_key_t))
16: != NGX_OK)
17: {
18: return NGX_ERROR;
19: }
20:
21: //初始化臨時動態數組next_names,next_names是存放關鍵字去掉後剩餘關鍵字
22: if (ngx_array_init(&next_names, hinit->temp_pool, nelts, sizeof(ngx_hash_key_t)) != NGX_OK)
23: {
24: return NGX_ERROR;
25: }
26:
27: //遍歷 names 數組
28: for (n = 0; n < nelts; n = i)
29: {
30: dot = 0;
31:
32: //查找 dot
33: for (len = 0; len < names[n].key.len; len++)
34: {
35: if (names[n].key.data[len] == '.')
36: {
37: dot = 1;
38: break;
39: }
40: }
41:
42: //將關鍵字dot之前的關鍵字放入curr_names
43: name = ngx_array_push(&curr_names);
44: if (name == NULL) {
45: return NGX_ERROR;
46: }
47:
48: name->key.len = len;
49: name->key.data = names[n].key.data;
50: name->key_hash = hinit->key(name->key.data, name->key.len);
51: name->value = names[n].value;
52:
53: dot_len = len + 1;
54:
55: //len指向dot後剩餘關鍵字
56: if (dot)
57: {
58: len++;
59: }
60:
61: next_names.nelts = 0;
62:
63: //若是names[n] dot後還有剩餘關鍵字,將剩餘關鍵字放入next_names中
64: if (names[n].key.len != len)
65: {
66: next_name = ngx_array_push(&next_names);
67: if (next_name == NULL) {
68: return NGX_ERROR;
69: }
70:
71: next_name->key.len = names[n].key.len - len;
72: next_name->key.data = names[n].key.data + len;
73: next_name->key_hash = 0;
74: next_name->value = names[n].value;
75:
76: }
77:
78: //若是上面搜索到的關鍵字沒有dot,從n+1遍歷names,將關鍵字比它長的所有放入next_name
79: for (i = n + 1; i < nelts; i++)
80: {
81: //前len個關鍵字相同
82: if (ngx_strncmp(names[n].key.data, names[i].key.data, len) != 0) {
83: break;
84: }
85:
86:
87: if (!dot
88: && names[i].key.len > len
89: && names[i].key.data[len] != '.')
90: {
91: break;
92: }
93:
94: next_name = ngx_array_push(&next_names);
95: if (next_name == NULL) {
96: return NGX_ERROR;
97: }
98:
99: next_name->key.len = names[i].key.len - dot_len;
100: next_name->key.data = names[i].key.data + dot_len;
101: next_name->key_hash = 0;
102: next_name->value = names[i].value;
103:
104: }
105:
106: //若是next_name非空
107: if (next_names.nelts)
108: {
109: h = *hinit;
110: h.hash = NULL;
111:
112: //遞歸,建立一個新的哈西表
113: if (ngx_hash_wildcard_init(&h, (ngx_hash_key_t *) next_names.elts,next_names.nelts) != NGX_OK)
114: {
115: return NGX_ERROR;
116: }
117:
118: wdc = (ngx_hash_wildcard_t *) h.hash;
119:
120: //如上圖,將用戶value值放入新的hash表
121: if (names[n].key.len == len)
122: {
123: wdc->value = names[n].value;
124: }
125:
126: //並將當前value值指向新的hash表
127: name->value = (void *) ((uintptr_t) wdc | (dot ? 3 : 2));
128:
129: } else if (dot)
130: {
131: name->value = (void *) ((uintptr_t) name->value | 1);
132: }
133: }
134:
135: //將最外層hash初始化
136: if (ngx_hash_init(hinit, (ngx_hash_key_t *) curr_names.elts,curr_names.nelts) != NGX_OK)
137: {
138: return NGX_ERROR;
139: }
140:
141: return NGX_OK;
142: }
因爲ngx_hash的內容比較多,不少設計都值得學習和研究,將在《菜鳥nginx源碼剖析數據結構篇(六) 哈希表 ngx_hash_t(下)》 探討剩餘內容。