本篇的上篇为 Nginx 源码分析:ngx_hash_t(上)。
建议先读完上篇再来读下篇。nginx
上篇回顾了hash表的基础概念,分析了Nginx
中hash表的内存模型及逻辑模型,从而引出了其核型数据结构ngx_hash_elt_t
和ngx_hash_t
,并从设计的角度解释了如何初始化这两个结构体。segmentfault
本篇主要分析,在Nginx
源码中是如何初始化这两个结构体的。数组
Nginx
中使用的哈希函数,围绕初始化时使用的ngx_hash_key_t
结构体展开。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
是初始化的限制条件,详见上一小节的说明;*names
是Nginx
根据索引值,提早算出来的哈希值数组,详见上上一小节;nelts
是*names
数组的大小。
整体来讲:这个函数作了三件事情:
ngx_hash_key_t
结构体数组及ngx_hash_init_t
结构体,估算出hash表所需的hash桶的个数及每一个hash桶的大小;ngx_hash_key_t
数组的内容装入生成的hash表中。由于ngx_hash_init
的函数定义很长,为了更好的说明问题,我按照次序捡取主要内容一段一段拆开来分析。感兴趣的能够看Ngx_hash.c的源码完整版
首先,对传入的hinit
及names
中的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)
这个宏的做用:将d
取a
的向上整数倍。例如,b=5
,a=4
,那么ngx_align(5,4)
的结果就是8
。
在实现原理上,利用了二进制计算的技巧。这里稍微讲一下(已经懂的能够跳过):
===========我是华丽的分割线===============
当a
为2
的某个幂的值时(例如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 的整数倍
若是,咱们须要任意的一个数a
对4
取整怎么办呢?很简单,只须要把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值所组成的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桶数目,能够进入下一阶段:申请内存空间及初始化结构体了。
根据上一篇文章对于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_t
、ngx_hash_elt_t*
数组、及hash表内存空间分配。
我的以为,Nginx
的高效并非来自于Nginx
有什么屠龙大法,而是来自于针对性很强的优化,ngx_hash_t
就是一个很好的例子。
在ngx_hash_t
中,随处可见各类优化,不管是这种特殊的hash表结构设计,仍是内存分配上的各类对齐,都很值得学习。