本节我们讲述一下nginx中hash。nginx中hash有三种类型:
-
不带通配符的hash
-
带前向通配符的hash
-
带后向通配符的hash
注意: 不支持同时带有前向通配符与后向通配符的hash
1. 函数ngx_hash_find()
这里函数实现较为简单,我们主要看一下ngx_align_ptr宏,该宏的core/ngx_config.h头文件中:
#define ngx_align_ptr(p, a) \
(u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
主要是实现a
字节对齐,这里是进行sizeof(void *)
字节对齐。
2. 函数ngx_hash_find_wc_head()
在介绍本函数之前,我们先来看一下对于前向通配符字符串
加入hash表时其是如何处理的:
现在如果要检索www.example.com
是否匹配*.example.com
,直接使用nginx的ngx_hash_find_wc_head()方法查询。该方法会把要查询的www.example.com
转化为com.example.
再开始查询。
下面我们就来介绍一下具体的查找过程:
例如要在通配符哈希表中查找www.domain.com
是否匹配*.domain.com
,则从后往前查找每一个关键词。则查找过程如图所示:
对于www.domain.com
,则先在根哈希表中查找com
,而com
指向一级哈希表。然后在一级哈希表中查找domain
,因为domain是叶子节点了,domain
指向的空间就是用户数据,查找过程结束。
3. 函数ngx_hash_find_wc_tail()
后置通配符hash表查询操作,跟前置通配符查找操作基本类似。差别是后置通配符是从第一个关键字往后查询。例如查找www.example.com
是否匹配www.example.*
,则先查询www
,接着查询example
。函数ngx_hash_find_wc_tail()
用来在后置通配符哈希表中查找到key对应的value值。
下面我们就来介绍一下具体的查找过程:
4. 函数ngx_hash_find_combined()
这里首先进行全匹配hash查找;然后再进行前置通配hash查找;最后进行后置通配hash查找。
5. NGX_HASH_ELT_SIZE宏定义
这里求ngx_hash_elt_t
结构体的大小。注意会进行sizeof(void *)
字节对齐。
6. 函数ngx_hash_init()
本函数根据hint
及name
数组完成精准匹配hash表初始化。下面我们分成几个部分来进行讲解:
1) 检查max_size
及bucket_size
是否合法
max_size
用于指定整个hash表中桶的数目;而bucket_size用于指定每个桶的大小,它的大小必须要保证每个桶至少能存放一个<key,value>
键值对。
上面for循环保证hash的桶至少能装一个<key,value>
键值对。宏NGX_HASH_ELT_SIZE
用于计算一个实际的键值对所占用的空间。之所以后面还要再加上sizeof(void *)
,是因为每个桶都用一个值NULL
的void *指针来标记结束。
2) 计算Hash中桶的个数
这里我们先给出一个示意图:
这里nginx首先会分配一个最大的桶hinit->max_size
,但是一般情况下我们并不需要那么大空间,这里我们会进行相应的运算,从而选出一个适合当前环境的桶大小。这里首先评估每一个ngx_hash_elt_t
元素的大小为2*sizeof(void *)
,则bucket[0]、bucket[1]…中每一个子桶能够容纳的元素个数约为bucket_size/2*sizeof(void *)
,这样就评估出大概的start值。然后我们遍历names
,分别将其hash进桶中,如果当前bucket_size能够容纳这些元素,则我们就可以采用该size
大小作为桶数。
注意上面函数中有:
bucket_size = hinit->bucket_size - sizeof(void *);
这里减去sizeof(void *)
是为了减去最后一个以NULL
结尾的指针空间
3) 计算新创建Hash所占用的空间
这里首先将test[i]的值都初始化为sizeof(void *)
,即一个NULL
结尾指针的大小。再接着将names
所有元素hash进桶中,然后计算出每个子桶占用的空间大小; 最后在求出所有这些子桶总共占用的空间大小。
上面根据实际的<key,value>键值对来实际计算每个桶的大小,而不是所有桶的大小的设置成一样的,这样能很有效的节约内存空间,当然由于每个桶的大小是不固定的,所有每个桶的末尾需要一个额外空间(大小为sizeof(void*))来标记桶的结束。并且每个桶大小满足cache行对齐,这样能加快访问速度,从这里也可以看出nginx无处不在优化程序的性能和资源的使用效率。
4) 分配一级桶空间
这里如果hinit->hash
为NULL时,则首先应该为hash
(ngx_hash_t)本身分配空间,并且需要为hash所指向的桶分配空间。这里为hash本身分配空间时,使用的是sizeof(ngx_hash_wildcard_t)
,多分配了一小点空间。分配后如下所示:
5) 分配二级子桶空间
这里分配的空间为len+ngx_cacheline_size
,这是因为我们下面需要进行ngx_cacheline_size
大小对齐。
6) 初始化一级桶相关指针
因为这里二级桶分配的是一块连续的内存空间,因此这里需要对一级桶指针做一个初始化。
7) 初始化hash表
这里对names
数组中的每一个元素,将其映射到hash表中。第二个for循环用于将每个子桶中的最末尾位置置为NULL,以做结尾使用。
到此位置,就完成了Hash表的初始化,下面给出一幅Hash的大概图景:
[参看]
-
nginx源码分析之hash的实现
-
nginx通配符哈希表