hash 一個常用的數據結構,一般來說 hash主要是主要關注key的散列算法和衝突處理的方法。nginx的hash對沖突處理使用的是開鏈法。並且ngx_hash是一次初始化,沒有刪除和添加方法。來看下ngx_hash的實現吧。首先還是數據結構。
typedef struct {
ngx_str_t key; //name
ngx_uint_t key_hash;//計算出來的hash值
void *value;//保存的值
} ngx_hash_key_t;
typedef struct {
void *value; //存儲值
u_short len; //長度
u_char name[1]; //名字 1的原因是不知道具體的長度,到時候當成指針使用。配合上面的len就可以完美的找出key
} ngx_hash_elt_t;//節點
每一個hash節點的內容。typedef struct {
ngx_hash_elt_t **buckets; //保存的桶
ngx_uint_t size; //大小
} ngx_hash_t;
typedef struct {
ngx_hash_t *hash; //使用哪個hash表來進行存儲。
ngx_hash_key_pt key; // 計算key的hash值散列算法指針
ngx_uint_t max_size; //最大的大小
ngx_uint_t bucket_size;//桶的大小
char *name; //名字
ngx_pool_t *pool; //分配內存使用的pool
ngx_pool_t *temp_pool;//臨時pool
} ngx_hash_init_t;
基礎的數據結構清楚了,我們來看具體的初始化操作。#define NGX_HASH_ELT_SIZE(name) \
(sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
計算每一個節點的大小
對應到我們的數據結構ngx_hash_elt_t 第一個sizeof(void*) 就是value的大小。 key.len +2 等於 name + len的大小 ,然後與對齊。
來看hash的初始化操作,直接從源碼來看:
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++) {//循環判斷下,這個桶裝得下不
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;
}
}
test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);//分配max_size的空間
if (test == NULL) {
return NGX_ERROR;
}
bucket_size = hinit->bucket_size - sizeof(void *); //預留一個szieof(void*)大小
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;
}
for (size = start; size <= hinit->max_size; size++) { //找粗來一個差不多合適的size
ngx_memzero(test, size * sizeof(u_short));
for (n = 0; n < nelts; n++) {
if (names[n].key.data == NULL) {//一般來說 感覺會蠻少的。有個key就是空,空也是值嘛
continue;
}
key = names[n].key_hash % size; //散列後對應的位置咯。
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));//累計每個桶大小情況
if (test[key] > (u_short) bucket_size) { //裝不下了
goto next;
}
}
goto found; //每一個元素都能有個桶來放了。
next:
continue;//這個size玩玩了不行。
}
size = hinit->max_size;
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);//很明顯是找不到合適的size來跟你玩
found:
for (i = 0; i < size; i++) {//找到一個差不多size
test[i] = sizeof(void *); //預留一個sizeof(void*)
}
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;
for (i = 0; i < size; i++) {
if (test[i] == sizeof(void *)) {
continue;
}
test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));//和ngx_cacheline_size對齊
/*
我們知道,CACHE與內存交換的最小單位爲CACHE LINE
假如我們的CACHE LINE爲64,如果我們的CACHE LINE全是對齊的,讀取一個小於等於CACHE LINE大小的cache需要一個cache,
但是如果是沒有對齊的話,一個小於等於CACHE LINE的內存放入cache的時候有可能會佔用兩個,讀的時候有可能是讀取兩個,讀取一個和兩個的區別就是類是於內存的缺頁中斷,再來一次。、
關於對齊,到時候重新寫個文章吧
*/
len += test[i];//需要的大小累加
}
if (hinit->hash == NULL) {//米有hash表給你用
hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t)
+ size * sizeof(ngx_hash_elt_t *));//把hash的內存一起分配了
if (hinit->hash == NULL) {
ngx_free(test);
return NGX_ERROR;
}
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 *));//只分配buckets的
if (buckets == NULL) {
ngx_free(test);
return NGX_ERROR;
}
}
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);//這裏爲了對齊
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;
}
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++) {//每個buckets多了sizeof(void *)
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;
return NGX_OK;
}
他的處理流程很簡單:
首先判斷下bucket_size是否足夠支持一個元素的存儲。
然後根據散列存儲的原則,找出一個桶個數來做了存儲
計算出每一個桶的大小。
計算出總共桶的大小。
給桶分配空間。
然後往桶裏放元素。
具體的直接參考完整的註釋。
接着 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;
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]) {//內容不一樣
goto next;
}
}
return elt->value;//一樣的
next:
elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
sizeof(void *));//偏移到下一個位置
continue;
}
return NULL;//沒有合適的
}
總結下:其實可以發現,預先計算出每個桶的大小,然後統一分配了內存空間,這樣可以不用使用鏈表那些來連接,節約了空間。
其實實現還是蠻簡單的,沒啥特別的地方。