一 哈希结构
关于哈希的相关概念,可以先参考以下文章。
https://blog.csdn.net/weixin_44517656/article/details/105215349
注意:
下面的词语桶和槽是一样的,有些人习惯将桶称之为槽。
在阅读Nginx的hash结构时,一定不能着急,否则会让自己怒火中烧,建议大家都使用一周左右(甚至以上)去消化。
Nginx的hash表结构主要几个特点:
- 1)静态只读。当初始化生成hash表结构后,是不能动态修改这个hash表结构的内容。
- 2)将内存利用最大化。Nginx的hash表,将内存利用率发挥到了极致,并且很多设计上面都是可以供我们学习和参考的。
- 3)查询速度快。Nginx的hash表做了内存对齐等优化。
- 4)Nginx的hash结构主要解析配置数据。
1 hash表的元素结构ngx_hash_elt_t
该结构保存着哈希表的k-v键值对结构。
/**
* 存储hash的元素
*/
typedef struct {
void *value; /* value指向用户数据。 */
u_short len; /* key的长度 */
u_char name[1]; /* 指向key的第一个地址,key长度为变长(设计上的亮点),所以实际也导致ngx_hash_elt_t是变长结构*/
} ngx_hash_elt_t;
2 hash表的基本结构ngx_hash_t
ngx_hash_t 是哈希的桶,它内部利用二级指针存放了一个桶数组和桶数组的大小。
/**
* Hash的桶
*/
typedef struct {
ngx_hash_elt_t **buckets; /* hash表的桶数组 */
ngx_uint_t size; /* hash表的桶数组个数 */
} ngx_hash_t;
3 支持通配符的哈希表结构 ngx_hash_wildcard_t
ngx_hash_wildcard_t专用于表示牵制或后置通配符的哈希表,如:前置*.test.com,后置:www.test.* ,它只是对ngx_hash_t的简单封装,是由一个基本哈希表hash和一个额外的value指针,当使用ngx_hash_wildcard_t通配符哈希表作为容器元素时,可以使用value指向用户数据。
typedef struct {
ngx_hash_t hash; /* hash表的基本结构 */
void *value; /* value指向用户数据,关于ngx_hash_elt_t的value和这个value,在下面讲ngx_hash_wildcard_init的时候会补充 */
} ngx_hash_wildcard_t;
4 预添加哈希散列元素结构 ngx_hash_key_t
ngx_hash_key_t用于表示即将添加到哈希表中的元素,其结构如下:
typedef struct {
ngx_str_t key; /* 元素关键字 */
ngx_uint_t key_hash; /* 由哈希方法算出来的哈希值 */
void *value; /* 指向用户自定义数据。关于ngx_hash_elt_t,ngx_hash_wildcard_t和本value,可以看下面的总结。 */
} ngx_hash_key_t;
其中补充(位于ngx_string.h):
typedef struct {
size_t len; /* key的长度 */
u_char *data; /* 字符串key,例如key为people,则len=6,data指向people,注意区别上面的value */
} ngx_str_t;
5 组合类型哈希表 ngx_hash_combined_t
ngx_hash_combined_t是由3个哈希表组成,一个普通hash表hash,一个包含前向通配符的hash表wc_head和一个包含后向通配符的hash表 wc_tail。
typedef struct {
ngx_hash_t hash;
ngx_hash_wildcard_t *wc_head;
ngx_hash_wildcard_t *wc_tail;
} ngx_hash_combined_t;
6 哈希表初始化结构ngx_hash_init_t
ngx_hash_init用于初始化哈希表,初始化哈希表的槽的总数并不是完全由max_size成员决定的,而是由在做初始化时预先加入到哈希表的所有元素决定的,包括这些元素的总数、每个元素的关键字长度等,还包括操作系统的页面大小,这个算法比较复杂,可以在ngx_hash_init函数中找到这个算法它的结构如下:
/**
* hash表主体结构
*/
typedef struct {
ngx_hash_t *hash; /* 指向hash表基本结构 */
ngx_hash_key_pt key; /* 计算key散列的方法,函数指针 */
ngx_uint_t max_size; /* hash表的桶数组最大个数,max_size>=ngx_hash_t.size */
ngx_uint_t bucket_size; /* 每个bucket桶存储ngx_hash_elt_t元素的存储空间大小 */
char *name; /* hash表名称 */
ngx_pool_t *pool; /* 内存池,它负责分配基本哈希列表、前置通配哈希列表、后置哈希列表中所有bucket */
ngx_pool_t *temp_pool; /* 临时内存池。它仅存在初始化哈希表之前,用于分配一些临时的动态数组,带通配符的元素初始化时需要用到临时动态数组 */
} ngx_hash_init_t;
其中ngx_hash_key_pt:
typedef ngx_uint_t (*ngx_hash_key_pt) (u_char *data, size_t len);
7 ngx_hash_keys_arrays_t结构
可以看到,这里设计了3个简易的哈希列表( keys_hash、dns_wc_head_hash、dns_wc_tail_hash),即采用分离链表法来解决冲突(多个哈希表)。
typedef struct {
ngx_uint_t hsize; /* 散列中槽(bucket桶)的总数 */
ngx_pool_t *pool; /* 内存池 */
ngx_pool_t *temp_pool; /* 临时内存池,下面的临时动态数组都是由临时内存池分配 */
ngx_array_t keys; /* 存放所有非通配符key的数组。 */
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值,
也就是是否有重复。
*/
ngx_array_t dns_wc_head; /* 存放前向通配符key被处理完成以后的值。比如:“*.abc.com”被处理完成以后,变成“com.abc.”被存放在此数组中 */
ngx_array_t *dns_wc_head_hash; /* 该值在调用的过程中用来保存和检测是否有冲突的前向通配符的key值,也就是是否有重复 */
ngx_array_t dns_wc_tail; /* 存放后向通配符key被处理完成以后的值。比如:“mail.xxx.*”被处理完成以后,变成“mail.xxx.”被存放在此数组中 */
ngx_array_t *dns_wc_tail_hash; /* 该值在调用的过程中用来保存和检测是否有冲突的后向通配符的key值,也就是是否有重复。 */
} ngx_hash_keys_arrays_t;
其中ngx_array_t(位于ngx_array.h):
typedef struct {
void *elts;
ngx_uint_t nelts;
size_t size;
ngx_uint_t nalloc;
ngx_pool_t *pool;
} ngx_array_t;
8 ngx_table_elt_t结构
ngx_table_elt_t不在ngx_hash.c中使用,主要用于HTTP模块。
ngx_table_elt_t是一个key/value对,ngx_str_t类型的key、value成员分别存储的是名字、值字符串。
hash成员表明ngx_table_elt_t也可以是某个散列表数据结构中的成员。ngx_uint_t类型的hash成员可以在ngx_hash_t中更快地找到相同key的ngx_table_elt_t数据。lowcase_key指向的是全小写的key字符串。
ngx_table_elt_t是为HTTP头部量身定制的,其中key存储头部名称,value存储对应的值,lowcase_key是为了忽略HTTP头部名称的大小写,hash用于快速检索到头部。
比如:
Content-Length:1024。
typedef struct {
ngx_uint_t hash; /* 内存池 */
ngx_str_t key; /* ngx_str_t类型的key */
ngx_str_t value; /* ngx_str_t类型的value */
u_char *lowcase_key; /* lowcase_key指向的是全小写的key字符串 */
} ngx_table_elt_t;
二 数据结构图
以下是对ngx_hash_init_t,ngx_hash_t,ngx_hash_elt_t,ngx_hash_key_pt,ngx_hash_wildcard_t等结构体的关系图。
三 具体函数实现
1 哈希表初始化ngx_hash_init函数
哈希表的初始化设计得非常牛逼,具体的体现有:
- 1)桶大小估算,首先会根据ngx_hash_elt_t类型的数组name及其nelts个数估算最小需要的桶的数目start,然后再从这个数目开始搜索,大大提高了效率,值得学习。
- 2)ngx_hash_elt_t中uchar name[1]+ushort 类型的len设计,那么如果在name很短的情况下,字节对齐后name和len最多占8字节,最短占4字节,而使用uchar* 的指针64位占8字节,32占4字节,比前者多0~4个字节。可以看到ngx是真的在内存上考虑,节省每一分内存来提高并发。
- 3)接着,我们看到下面求ngx_hash_elt_t所占字节数的方式,并不是用sizeof(ngx_hash_elt_t),而是定义特定的宏来根据字节对齐来求。
其中,第一个(sizeof(void *)为求成员value的字节数,ngx_align((name)->key.len为key的字节数,2是u_short本身的字节数,通过ngx_align宏来对齐,最终结果就是连对齐后补充的字节数也算进来,所以每次调用该宏都是获取该元素在内存所占的字节数。
并且有个小细节,sizeof(void *)可以每次根据系统的位数来获取8字节还是4字节对齐。
typedef struct {
void *value;
u_short len;
u_char name[1];
} ngx_hash_elt_t;
/*
注意:NGX_HASH_ELT_SIZE只是用到name(ngx_hash_key_t)的key,
而并非求ngx_hash_key_t结构的大小,而是求ngx_hash_elt_t元素的大小
*/
#define NGX_HASH_ELT_SIZE(name) \
(sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
初始化哈希表的代码如下,大概流程为:
- 1)先判断相应条件,例如桶数组大小不能为0。桶的容量bucket_size太大,不能大于65536 - ngx_cacheline_size。每个元素必须在bucket_size范围内。
- 2)通过一定算法估算出至少需要桶的数量,然后再从最小桶数目开始试,计算容下 nelts 个元素需要多少个桶。
- 3)分配桶数组的内存。
- 4)分配每个桶的容量。
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;
//桶数组的大小为0返回
if (hinit->max_size == 0) {
ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
"could not build %s, you should "
"increase %s_max_size: %i",
hinit->name, hinit->name, hinit->max_size);
return NGX_ERROR;
}
/*
桶的容量bucket_size太大,大于65536 - ngx_cacheline_size时返回,
减去ngx_cacheline_size是为了后面len+ngx_cacheline_size的对齐
*/
if (hinit->bucket_size > 65536 - ngx_cacheline_size) {
ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
"could not build %s, too large "
"%s_bucket_size: %i",
hinit->name, hinit->name, hinit->bucket_size);
return NGX_ERROR;
}
/*
判断每个元素是否超过整个桶的容量,加上sizeof(void *)是
后面为了对齐,bucket_size减去了sizeof(void *)。
也就是说,每个桶最多占bucket_size-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 %s, you should "
"increase %s_bucket_size: %i",
hinit->name, hinit->name, hinit->bucket_size);
return NGX_ERROR;
}
}
/*
test是u_short数组,用于临时保存每个桶的当前容量大小,所以
开辟max_size个,为后续进行开辟内存等操作提供便利
*/
test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
if (test == NULL) {
return NGX_ERROR;
}
/*
每个桶最多占hinit->bucket_size - sizeof(void *)以下的容量。
*/
bucket_size = hinit->bucket_size - sizeof(void *);
/*
这里初略估算所需桶的最小数量,方法如下:
1)由于ngx_hash_elt_t元素结构对齐后,至少所需NGX_HASH_ELT_SIZE(&name[n]) > (2*sizeof(void*))的空间
2)所以bucket_size 大小的桶最多能容下 bucket_size/(2*sizeof(void*)) 个元素。
例如bucket_size=10,机器为32位,存两个元素,那么2/(10/(2*4)),那么最少占用2个桶。
*/
start = nelts / (bucket_size / (2 * sizeof(void *)));
start = start ? start : 1;//桶最少为1个
/*
实际上上面两步已经初步推算出至少需要的桶数的初始估算下标,
这一步可以认为是ngx的经验得出,当max_size太大,并且取临界值时,nelts大于100个时,
使最少需要的桶数的估算下标降低1000,以此让start~max_size有更多的范围获得所需要的桶数
*/
if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {
start = hinit->max_size - 1000;
}
/*
从最小桶数目开始试,计算容下 nelts 个元素需要多少个桶。
外层循环为桶的估计下标~最大个数。内层循环为传进的元素个数。主看内层循环
*/
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) {//key为空则跳过
continue;
}
key = names[n].key_hash % size;//根据元素的哈希值获取桶的下标,即key
len = test[key] + NGX_HASH_ELT_SIZE(&names[n]);//统计该桶的容量,当超过bucket_size时寻
//找下一个桶去存储元素。例如size=1第一个桶,有两个元素28,32,60,取余后都是0,
//假设bucket_size能存储28,32但不能存储60,那么60将存储在size=1的桶。
//注意:这里只是估计test[key]即每个桶的实际长度,即使key相同也是可以的
#if 0
ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
"%ui: %ui %uz \"%V\"",
size, key, len, &names[n].key);
#endif
if (len > bucket_size) {//上面所说的,当前桶容量不够寻找下一桶存储
goto next;
}
test[key] = (u_short) len;//保存每个桶的实际存储长度
}
/*
来到这里,说明内层for即所有元素均找到合适的桶存储
*/
goto found;
next:
continue;
}
size = hinit->max_size;//如果来到这里,说明外层循环遍历满而退出,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);
found:
//执行到这里,我们已经获取到能存放nelts个元素的size桶个数
//清空test数组
for (i = 0; i < size; i++) {
test[i] = sizeof(void *);
}
/*
遍历元素个数,根据每个元素的哈希key和元素大小,统计每个桶的容量并存放在test数组。
*/
for (n = 0; n < nelts; n++) {
if (names[n].key.data == NULL) {//元素的key是空就跳过
continue;
}
key = names[n].key_hash % size;
len = test[key] + NGX_HASH_ELT_SIZE(&names[n]);//统计每个桶的存储实际元素的大小,注意test[key]是会改变的
/*
这里前面说过,减去ngx_cacheline_size是为了后面的len+ngx_cacheline_size进行内存对齐
并且与开始bucket_size一样,也需要让桶的容量len低于65536 - ngx_cacheline_size
因为前面是没有对实际的len容量进行限制,这一个判断也是ngx后面版本添加的
*/
if (len > 65536 - ngx_cacheline_size) {
ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
"could not build %s, you should "
"increase %s_max_size: %i",
hinit->name, hinit->name, hinit->max_size);
ngx_free(test);
return NGX_ERROR;
}
test[key] = (u_short) len;
}
len = 0;
/*
遍历桶的个数,根据每个桶的实际存储容量进行对齐,获得对齐后的容量,
然后统计得出所有元素对齐后所需的总长度len
*/
for (i = 0; i < size; i++) {
if (test[i] == sizeof(void *)) {//如果没有用到该桶的话 就跳过
continue;
}
/*
注意这里对齐后,test数组中存放着每个容量对齐后的桶的容量。例如桶的大小为100,
ngx_cacheline_size=64,那么对齐后test[i]=(((d) + (a - 1)) & ~(a - 1)),
即100+63&~63=163&000000=10100011&000000=10000000=2^7=128,大家可以自己写个测试程序,
因为当时我也是写了个测试程序
*/
test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));
len += test[i];//该len是所有元素对齐后的总长度len
}
/*
这个步骤就是我们上面所画的图,当哈希表为空时,额外开辟带ngx_hash_wildcard_t的内存,
不为空就只开辟buckets的内存。
并且这里似乎看起来很奇怪,既然是hash,为什么分配空间的大小又跟hash结构体一点关联都没有呢?
因为ngx_hash_wildchard_t包含hash这个结构体,所以就一起分配了
并且把每个桶的指针也分配在一起了,这种思考跟以前学的面向对象思想很不一样,但这样会很高效
*/
if (hinit->hash == NULL) {
//开辟ngx_hash_wildcard_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;
}
//获取每个桶数组首地址,注意末尾有s代表数组
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;
}
}
/*
上一步开辟完桶数组的内存后,自然是开辟每个桶的容量内存。
但是为了对齐,len总长度必须加上ngx_cacheline_size,上面虽然len的总长度是经过对齐获得的。
这是因为,即使从pool内存池分配的地址必为偶数,但是由于我们需要对齐,会调用ngx_align_ptr宏导致返回的地址向上对齐,
这就这导致开辟的内存存在不够用,所以必须加上ngx_cacheline_size。
例如假设内存池返回的elts=0x2地址,len的64,2+63&000000=64,所以首地址变成64,那么刚刚开辟的内存实际剩下66-64=2
明显是不对的,所以必须填充一个ngx_cacheline_size以便对齐。
ngx_cacheline_size的值可能为32位,64位,128位,从cpuinfo函数中读取。
*/
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) {//如果key是空就跳过
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;
/*
赋值名字并转小写,因为elt元素是一个数组,所以ngx这里可以将len个字节付给一个字节的name,
但平时尽量别这样用,容易造成越界,因为你不是大师
*/
ngx_strlow(elt->name, names[n].key.data, names[n].key.len);
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
}
/*
遍历桶个数,为每个桶的结尾元素的value添加NULL,表示该桶结束。
*/
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;//置value为空是因为连续内存的数组最后元素的末尾为value,因为value是elt结构的首个成员。
}
ngx_free(test);
hinit->hash->buckets = buckets;
hinit->hash->size = size;
#if 0
for (i = 0; i < size; i++) {
ngx_str_t val;
ngx_uint_t key;
elt = buckets[i];
if (elt == NULL) {
ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
"%ui: NULL", i);
continue;
}
while (elt->value) {
val.len = elt->len;
val.data = &elt->name[0];
key = hinit->key(val.data, val.len);
ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
"%ui: %p \"%V\" %ui", i, elt, &val, key);
elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
sizeof(void *));
}
}
#endif
return NGX_OK;
}
至此该函数结束,要想彻底的理解这个函数和我的注释,建议按照以下例子将值代进去代码,一步步计算理解,并且最好代入时可以自己画个图理解,因为这个函数还是很复杂的,切不可操之过急。
例子:
1)假设max_size=100;bucket_size=1000;传进的数组有3个元素,其中具体结构如下:
typedef struct {
ngx_str_t key;
ngx_uint_t key_hash;
void *value;
} ngx_hash_key_t;
继续假设:
key.data="tyy1",那么key.len=4,value设为NULL即可。//第一个元素
key.data="tyy2",那么key.len=4,value设为NULL即可。//第二个元素
key.data="tyy3",那么key.len=4,value设为NULL即可。//第三个元素
2)经过运算,bucket_size=992,每个元素占16字节(假设64位机器),那么start=1。此时只用到一个桶存储这三个元素。
3)test[0]开始占48字节,对齐后占64字节。
然后继续往下代入即可,你就可以理解到该函数了。
2 通过哈希表处理通配符的ngx_hash_wildcard_init初始化函数
2.1 支持通配符的hash表的书写形式
支持通配符的ngx_hash_wildcard_t实际上是对基本hash表的一层封装,可以简单的通过ngx_hash_wildcard_t结构体内部成员看出。
nginx为了处理带有通配符的域名字符串的匹配问题,实现了支持通配符的hash表nginx中的通配符有两种形式:
- 1)一种是通配符在前面的例如“*.test.com”,这样可以省略"*"号,写成“.test.com”。
- 2)还有一种是通配符在后面的例如"www.test.*",这样的域名不能省略"* "星号,因为这样的通配符可以匹配“www.test.com”、“www.test.cn”、“www.test.org”等域名。并且注意:nginx不能同时包含在前和在后的通配符例如“*.test.*”。
2.2 初始化的大概流程
- 1)为临时数组curr_names,next_names申请空间。
- 2)遍历数组中的元素,找出当前字符串的第一个key字段并放入curr_names数组,若当前字符串仍有剩余字符,则优先放进next_names数组(注意:每循环一次,next_names都会被清0一次)。
- 3)依次比较names中剩余的元素,若具有相同key字段,除掉相同的部分后,接着将剩余部分放入next_names数组。
- 4)递归处理next_names数组。
- 5)回到第二步,如果已经遍历完则将curr_names数组元素放入hash表。
2.3 该函数的参数注意事项
ngx_int_t ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts);
重点注意的是参数2,该传入参数names数组是经过处理后的字符串数组。对于字符串数组: “*.test.tyy.com”、".tyy.com"、"*.example.com"、"*.example.org"、".tyy.org"处理完成之后就变成了
“com.tyy.test.”、“com.tyy”、“com.example.”、“org.example.”、“org.tyy”。
可以看到一个特点,若有*则保留".",没有则去掉"."。
2.4 ngx_hash_wildcard_init源码
ngx_int_t
ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
ngx_uint_t nelts)
{
size_t len, dot_len;
ngx_uint_t i, n, dot;
ngx_array_t curr_names, next_names;
ngx_hash_key_t *name, *next_name;
ngx_hash_init_t h;
ngx_hash_wildcard_t *wdc;
//为临时数组curr_names申请空间,curr_names用于存放当前节点
if (ngx_array_init(&curr_names, hinit->temp_pool, nelts,
sizeof(ngx_hash_key_t))
!= NGX_OK)
{
return NGX_ERROR;
}
//为临时数组next_names申请空间,next_names用于存放去除当前key字段后,剩余的字符串
if (ngx_array_init(&next_names, hinit->temp_pool, nelts,
sizeof(ngx_hash_key_t))
!= NGX_OK)
{
return NGX_ERROR;
}
/*
遍历数组中元素,对元素进行分割递归处理。
注意:该循环是最重要的循环,几乎包含整个函数。
*/
for (n = 0; n < nelts; n = i) {
#if 0
ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
"wc0: \"%V\"", &names[n].key);
#endif
//标志位,用于标志是否找到".",即字符串是否可以被分割成多个部分
dot = 0;
/*
依次遍历每个元素的key,找到当前元素key值中第一次出现“.”的位置
其中:len表示key的长度(若找到".",则代表"."之前的长度),data表示key的值。
*/
for (len = 0; len < names[n].key.len; len++) {
if (names[n].key.data[len] == '.') {
dot = 1;
break;
}
}
/*
为“.”之前的key字段申请空间。
注意ngx的数组的push实际上是这样申请空间的:
若数组还有空间,则直接获取对应内存返回,没有则在内存池上为数组扩容(实际创建新数组,数组大小x2),元素开辟大小由初始化时确定。
*/
name = ngx_array_push(&curr_names);
if (name == NULL) {
return NGX_ERROR;
}
//为"."之前的key字段赋值(保存在key_hash)
name->key.len = len;
name->key.data = names[n].key.data;//data仍保留原来元素key的整个字符串
name->key_hash = hinit->key(name->key.data, name->key.len);//散列回调函数,给哈希值赋值,参1为整个字符串,参2为"."的下标
name->value = names[n].value;//这也是一个值,需要与上面的data,key_hash区分
#if 0
ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
"wc1: \"%V\" %ui", &name->key, dot);
#endif
//记录剩余字符串的开头下标
dot_len = len + 1;
//若找到".",则指向剩余字符串的开头
if (dot) {
len++;
}
/*
重置next_names数组的元素个数,这一步也相当于重置数组,因为原本next_names数组内的数组会被覆盖掉。
用于统计下一次递归的next_names.nelts个数。
很重要,因为下面关系到是否能进入递归的逻辑(我画图的时候忽略了),一时大意,导致这用了不少时间去理解。
并且看到,ngx若想清空数组,直接将成员nelts置0即可,不用清空,非常快速。所以一般人很难写出这种和平时开发相悖的代码。
*/
next_names.nelts = 0;
/*
如果names[n]的"."后还有剩余字符串,将剩余字符串放入next_names被清空的数组中。
因为如果next_names数组有仍元素,将会被覆盖,因为上面将nelts置0了
*/
if (names[n].key.len != len) {
next_name = ngx_array_push(&next_names);
if (next_name == NULL) {
return NGX_ERROR;
}
next_name->key.len = names[n].key.len - len;
next_name->key.data = names[n].key.data + len;//此时data指向剩余字符串开头,即"."的后一位
next_name->key_hash = 0;//哈希值为0
next_name->value = names[n].value;
#if 0
ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
"wc2: \"%V\"", &next_name->key);
#endif
}
/*
搜索names形参数组后面的元素,看有没有和当前元素相同key字段的元素,如果有则把除去当前key字段的剩余字符串装入该数组。
存在两种情况(两个if条件):
1)若key不相等,没必要在比较,直接退出(注意此时连"."也比较,因为len++了)。
2)若没找到dot即没有".",并且len的位置不是".",也退出。
这种情况是由于当前key作为后面元素的子串造成的,它们实际上也是不相等的,
只不过在前面的某个部分相等,但不会出现当前key作为后面元素的父串而相等的情况。
例如当前key为"abc",后一个元素为"abc123.hhh",那么就会存在abc=abc的情况,所以必须排除。
总结下面的for:实际上,下面的for主要是为了过滤其它字符串的第一个key相同的字段。
然后再排除有可能造成第一个key相同的干扰。
*/
for (i = n + 1; i < nelts; i++) {//传进来只有一个元素时,该for直接跳过
if (ngx_strncmp(names[n].key.data, names[i].key.data, len) != 0) {/*连"."也比较(因为len加加了!!!),
原因是参数传进时是经过反转处理的,所以必须也比较"."(代入理解)
注意:这里总觉得mgx写得不好,因为len是从n下标的当前元素找到的,
故它不会越界,但是i是n的下一元素下标,容易引起越界误解,例如,
下标n指向"tyy.test",而下标i指向"tyy",那么len=3,再len++变成4,
容易造成误解,虽然系统一般会在字符串末尾添加0,并且经过实验测试,
并不会造成程序报错,ngx真的是大师,每一个字节每个位都不放过使用*/
break;
}
if (!dot
&& names[i].key.len > len //此条件是为了防止names[i].key.data[len]索引len越界
&& names[i].key.data[len] != '.')
{
break;//这个if的意思是只要没找到dot并且下一元素的len下标不是".",则直接返回,因为意味着两个元素的第一个key不相等。
}
//如果有则把除去当前key字段的剩余字符串装入该数组
next_name = ngx_array_push(&next_names);
if (next_name == NULL) {
return NGX_ERROR;
}
next_name->key.len = names[i].key.len - dot_len;//这里需要用到保留的dot_len
next_name->key.data = names[i].key.data + dot_len;//此时data指向剩余字符串开头,即"."的后一位
next_name->key_hash = 0;//哈希值为0
next_name->value = names[i].value;
#if 0
ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
"wc3: \"%V\"", &next_name->key);
#endif
}
/*
如果next_names数组中仍有元素,则递归处理该元素;
否则若找到dot,则将curr_names数组的当前元素name=>value | 1。
然后继续循环(注意还没退出循环去执行ngx_hash_init)。
*/
if (next_names.nelts) {
h = *hinit;//原来传进的哈希表赋值给局部变量h。注意,是赋值,所以每次递归都会创建一个新的哈希表基本结构,交给h管理
h.hash = NULL;//hash指向空
if (ngx_hash_wildcard_init(&h, (ngx_hash_key_t *) next_names.elts,
next_names.nelts)//递归,每次传局部变量h与剩余字符串数组next_names
!= NGX_OK)
{
return NGX_ERROR;
}
wdc = (ngx_hash_wildcard_t *) h.hash;
if (names[n].key.len == len) {/*将用户value值放入新的hash表。这里的判断类似下面的dot,当中间的递归的cur数组中的names[n]元素
末尾字符是"."的话,则让wdc->value保存names[n].value的值。例如该元素为"test.",那么就会
满足if条件。注意该判断是一般是发生在递归中间,其它情况还没判断过。*/
wdc->value = names[n].value;
}
/*暂停分析这里!!!
并将当前value值指向新的hash表。由于指针都字节对齐了,低2位肯定为0,巧妙使用剩余的字节位数提高效率。
wdc是每次递归后创建返回的哈希表,后面表示:若当前names数组下标为n元素中找到".",那么就使低两位为11,否则为10.
name->value是连接每一个哈希表结构的关键。
*/
name->value = (void *) ((uintptr_t) wdc | (dot ? 3 : 2));
} else if (dot) {/*带有前置通配符的字符串,因为来到这里说明next数组没元素,并且当前names数组下标为n元素中找到了".",例如"example.",
因为传进时是倒转的,所以就是前置,并且最低位 | 1即01,因为dot是找到".",所以反过来后:01仅仅是
"*.example.org"的数据指针。*/
name->value = (void *) ((uintptr_t) name->value | 1);
}
}//注意这个是for循环,不要轻易退出,人何常不是这样呢!
/*
curr_names.elts代表数组的首地址.该语句代表将本次curr_names数组的所有元素送去初始化成哈希表。
*/
if (ngx_hash_init(hinit, (ngx_hash_key_t *) curr_names.elts, curr_names.nelts) != NGX_OK)
{
return NGX_ERROR;
}
return NGX_OK;
}
2.5 对ngx_hash_wildcard_init例子解析
以下面字符数组代入进行一步步分析。
“*.test.tyy.com”、".tyy.com"、"*.example.com"、"*.example.org"、".tyy.org"处理完成之后就变成了
“com.tyy.test.”、“com.tyy”、“com.example.”、“org.example.”、“org.tyy”。
即最终代入的5个元素是"com.tyy.test."、“com.tyy”、“com.example.”、“org.example.”、“org.tyy”。
其中分析时比较重要的结构体如下:
ngx_int_t
ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
/**
* hash表主体结构
*/
typedef struct {
ngx_hash_t *hash; /* 指向hash表基本结构 */
ngx_hash_key_pt key; /* 计算key散列的方法,函数指针 */
ngx_uint_t max_size; /* hash表的桶数组最大个数,max_size>=ngx_hash_t.size */
ngx_uint_t bucket_size; /* 每个bucket桶存储ngx_hash_elt_t元素的存储空间大小 */
char *name; /* hash表名称 */
ngx_pool_t *pool; /* 内存池,它负责分配基本哈希列表、前置通配哈希列表、后置哈希列表中所有bucket */
ngx_pool_t *temp_pool; /* 临时内存池。它仅存在初始化哈希表之前,用于分配一些临时的动态数组,带通配符的元素初始化时需要用到临时动态数组 */
} ngx_hash_init_t;
/**
* Hash的桶
*/
typedef struct {
ngx_hash_elt_t **buckets; /* hash表的桶数组 */
ngx_uint_t size; /* hash表的桶数组个数 */
} ngx_hash_t;
/* 该元素结构表现为处理元素的主要结构。与下面的存储想区别。*/
typedef struct {
ngx_str_t key; /* 元素关键字 */
ngx_uint_t key_hash; /* 由哈希方法算出来的哈希值 */
void *value; /* 指向用户自定义数据 */
} ngx_hash_key_t;
/* 该元素结构表现为存储元素的主要结构。 */
typedef struct {
void *value;
u_short len;
u_char name[1];
} ngx_hash_elt_t;
假设我们传进的ngx_hash_init_t的hash成员为空。key,max_size,bucket_size,name,pool,temp_pool皆由用户传进,那么下面有(下面的n是对ngx_hash_wildcard_init的最外层for循环的变量n的描述):
-
第一次传进上面字符数组的5个元素,并且n=0,即第一个元素"com.tyy.test."时:
1.1 因为n=0时,next_names数组有三个元素,所以会继续递归调用ngx_hash_wildcard_init,所以也有第二次传进的字符数组有3个元素,并且n=0时:
1)即,第一次和第二次调用ngx_hash_wildcard_init时,n各自为:n=0,n=0。看下图:
2)继续回到遍历元素的最外层for循环即for (n = 0; n < nelts; n = i),由于上一次循环i=n+1=0+1,所以此时第二次调用ngx_hash_wildcard_init的n=i,即n=1,为了画图方便,将第二次调用ngx_hash_wildcard_init的n=2的步骤也包含在下图。所以此时从len=3,data="tyy"这个元素开始。
即,第一次和第二次调用ngx_hash_wildcard_init时,n各自为:n=0,n=1,2。看下图:
注意:下图的第二个next_names此时无元素,是因为代码中执行next_names.nelts = 0后,不会进入递归(下面的同理),但此时next_names数组仍包含上面len=5,"test."这个元素,但若往该数组重新添加元素,则该元素将被覆盖,所以认为它是无元素,ngx这种处理也用到了很多地方。
以上的代入过程非常严谨。
-
经过上面步骤后,第一次传5个元素的第一个元素"com.tyy.test."已经被处理完毕,即第一次调用的n=0处理过程已经完成。接着就是回到第一次传5个元素的最外层for循环,由于传5个元素时,有3个元素的第一个key即"com"是相同的,所以此时最外层for循环n=i=3,从第四个元素"org.example."开始。并且再次强调,next_names的三个元素由于next_names.nelts = 0清零后,从数组拿到的内存就是数组首地址,所以会覆盖原有的元素。
看下图:
-
上面遍历完5个元素后,由于n=4,i=5,所以会退出for循环。最终将下图的curr_names数组中的元素执行最后一次的ngx_hash_init函数调用。
调用完后,如下图:
至此,例子的调用执行完毕。下面就是总结。
三 总结
1 例子的整体结构图
上面每一个小步骤可能都会遗漏之前已经画过的部分,例如上面的第3点部分哈希结构没画出第2点的内容。所以这里为了方便观察,我对上面的所有元素做一个总结(第3点已经分析调用完毕)。这个图是最重要的总结图。
2 上面的每个小步骤讲完,下面我们总结一下上面的图,看看它们是怎么连接在一起的。
上图第三大点的1可以看到,ngx的连接设计的非常巧妙,每次递归执行到末尾后,先将末尾的curr_names中元素初始化成哈希表,然后哈希表作为上一个哈希表的元素的成员,即使用value保存该哈希表。然后上一个的哈希表又作为上上个哈希表的元素的成员,即又使用value保存该哈希表,层层嵌套。最终通过返回用户传进的hinit,就可以遍历里面所有的哈希表成员。
3 关于value的指向
1)ngx_hash_elt_t
typedef struct {
void *value;
u_short len;
u_char name[1];
} ngx_hash_elt_t;
对于ngx_hash_elt_t的value,在第三大点的1图中可以看到,它可以指向下一个elt或者NULL,也可以指向下一个hash表结构。
2)ngx_hash_key_t
typedef struct {
ngx_str_t key;
ngx_uint_t key_hash;
void *value;
} ngx_hash_key_t;
对于ngx_hash_key_t,在第二大点的2.5的1或者2这些图中可以看到,它主要是用于临时处理数据的结构,但是该value可以指向真正的数据或者下一个hash表结构,不过起临时作用。
3)ngx_hash_wildcard_t
typedef struct {
ngx_hash_t hash;
void *value;
} ngx_hash_wildcard_t;
分析ngx_hash_wildcard_t,由于上面的例子并未涉及到该value,所以需要从下面这句代码分析。
由于names[n].value是ngx_hash_key_t结构,所以ngx_hash_wildcard_t的value既然是ngx_hash_key_t的value赋值,那么它们的意思就是一样的。即可以指向真正的数据或者下一个hash表结构。
if (names[n].key.len == len) {
wdc->value = names[n].value;
}
1.4 vlaue的低两位来标志来携带相关信息
由于调用的初始化哈希表函数ngx_hash_init里面必然会进行内存对齐,所以最低两位必定为0。例如:
注意这里对齐后,test数组中存放着每个容量对齐后的桶的容量。
例如桶的大小为100,ngx_cacheline_size=64,
那么对齐后test[i]=(((d) + (a - 1)) & ~(a - 1)),
即100+63&~63=163&000000=10100011&000000=10000000=2^7=128。
可以看到,最低两位为0,必定为0是因为计算机字节对齐最低32位,
所以ngx调用对齐宏后最低两位必定为0。
实际上这个例子就是ngx_hash_init里面讲过的。
(以下暂未理解)
所以,对比第三大点的1.1的图,总结ngx低两位的的意思:
00 - value 是 "example.com" 和 "*.example.com"的数据指针。
01 - value 仅仅是 "*.example.com"的数据指针。
10 - value 是 支持通配符哈希表是 "example.com" 和 "*.example.com" 指针。
11 - value 仅仅是 "*.test.tyy.com"("*.example.org")的指针。
到此,ngx的哈希初始化分析完毕。