老规矩,先来看下ngx_hash大概的结构图:
注意: name的剩余字节是不在ngx_hash_elt_t的结构体占用内存中的,这里是个奇技淫巧,一定程度上保证key的不定长和内存浪费
在进入源码分析之前,先上一段debug用的示例代码
#include <stdio.h>
#include <string.h>
#include "nginx.h"
#include "ngx_hash.h"
#include "ngx_string.h"
#include "ngx_config.h"
#include "ngx_conf_file.h"
#include "ngx_core.h"
#include "ngx_palloc.h"
#define POOL_SIZE 2000
typedef struct Test_2
{
int a ;
int b ;
ngx_queue_t testq;
} test2;
void testHash(){
//demo中没有其他位置在启动时设置缓存行大小,此处调用cpuinfo()设置缓存行
//在后面元素和bucket内存对齐时需要用到缓存行
ngx_cpuinfo();
u_short *test = test = ngx_alloc(4 * sizeof(u_short), NULL);;
test[0] = 58;
test[1] = 23;
test[2] = 121;
test[3] = 140;
int a = 0;
for( a = 0; a<4;a++){
printf("test: %d \n" , test[a]);
test[a] = (u_short) (ngx_align(test[a], ngx_cacheline_size));
printf("test: %d \n" , test[a]);
}
ngx_pool_t * pool = ngx_create_pool(POOL_SIZE,NULL);
ngx_array_t *arr = ngx_array_create(pool, 32, sizeof(ngx_hash_key_t));
ngx_hash_keys_arrays_t haArr;
ngx_memzero(&haArr, sizeof(ngx_hash_keys_arrays_t));
haArr.temp_pool = pool;
ngx_hash_keys_array_init(&haArr,NGX_HASH_LARGE_ASIZE);
printf("size of ngx_hash_elt_t: %d \n" , sizeof(ngx_hash_elt_t) );
printf("size of ngx_hash_key_t: %d \n" , sizeof(ngx_hash_key_t) );
printf("size of u_short: %d \n" , sizeof(u_short) );
printf("size of void*: %d \n" , sizeof(void*) );
printf("size of char: %d \n" , sizeof(u_char) );
test2 value[5];
ngx_str_t key0 = ngx_string("bbbbb-key");
ngx_str_t key1 = ngx_string("aaaaa-key");
ngx_str_t key2 = ngx_string("ccccc-key");
ngx_str_t key3 = ngx_string("ggggg-key");
ngx_str_t key4 = ngx_string("kkkkk-key");
ngx_uint_t i;
for(i = 0;i<5;i++){
value[i].a = i;
value[i].b = i*i;
}
ngx_hash_init_t *hash;
hash = ngx_palloc(pool, sizeof(ngx_hash_init_t));
hash->pool = pool;
hash->name = "test_2_hash";
hash->key = ngx_hash_key;
hash->max_size = 128;
hash->bucket_size = 64/*sizeof(test2) *8*/;
ngx_hash_add_key(&haArr,&key0,&value[0],NGX_HASH_READONLY_KEY);
ngx_hash_add_key(&haArr,&key1,&value[1],NGX_HASH_READONLY_KEY);
ngx_hash_add_key(&haArr,&key2,&value[2],NGX_HASH_READONLY_KEY);
ngx_hash_add_key(&haArr,&key3,&value[3],NGX_HASH_READONLY_KEY);
ngx_hash_add_key(&haArr,&key4,&value[4],NGX_HASH_READONLY_KEY);
int testsize = hash->bucket_size / (2 * sizeof(void *));
ngx_hash_key_t *names = haArr.keys.elts;
test2 *value00 = names[0].value;
test2 *value01 = names[1].value;
ngx_hash_init(hash,haArr.keys.elts,haArr.keys.nelts);
test2 *node = ngx_hash_find(hash->hash, names[2].key_hash, key2.data, key2.len);
}
void testHashWC(){
ngx_cpuinfo();
u_char a[10] = "hElLo";
u_char b[10] = "";
u_char *c = a;
u_char *d = b;
int n = ngx_strlen(a);
while (n--) {
u_char e = *c;
*d = ngx_tolower(e);
c++;
d++;
}
ngx_strlow(b,a,3);
// 每个地址对应8bit的内存空间(最小内存单元,1字节)
// 在64位系统中,内存对齐8字节(跨8个地址),每分配两段8字节相邻的内存对齐后的相邻指针相差8,因此指针二进制的后三位为0(32位系统,后两位为0);
// 因此获取的指针地址都是8的倍数
void *ptra = (void *) ((uintptr_t) a );
void *ptr = (void *) ((uintptr_t) a | 1);
void *ptr1 = (void *) ((uintptr_t) a | 2);
void *ptr2 = (void *) ((uintptr_t) a | 3);
ngx_pool_t * pool = ngx_create_pool(POOL_SIZE,NULL);
ngx_hash_keys_arrays_t haArr;
ngx_memzero(&haArr, sizeof(ngx_hash_keys_arrays_t));
haArr.temp_pool = pool;
ngx_hash_keys_array_init(&haArr,NGX_HASH_LARGE_ASIZE);
test2 value[6];
// ngx_str_t *key0 = ngx_pcalloc(pool, sizeof(ngx_str_t));
// key0->data = "*.com.cn"; //ngx_strlow不能改变常量,因此data的值不能是常量
// key0->len = strlen("*.com.cn");
ngx_str_t key0 = ngx_string("*.com.cn");
key0.data = ngx_pcalloc(pool, key0.len);
ngx_memcpy(key0.data,"*.com.cn",key0.len);
ngx_str_t key1 = ngx_string("*.a.com.cn");
key1.data = ngx_pcalloc(pool, key1.len);
ngx_memcpy(key1.data,"*.a.com.cn",key1.len);
ngx_str_t key2 = ngx_string(".b.com.cn");
key2.data = ngx_pcalloc(pool, key2.len);
ngx_memcpy(key2.data,".b.com.cn",key2.len);
ngx_str_t key3 = ngx_string("cn.org.*");
key3.data = ngx_pcalloc(pool, key3.len);
ngx_memcpy(key3.data,"cn.org.*",key3.len);
ngx_str_t key4 = ngx_string("*.test.cc");
key4.data = ngx_pcalloc(pool, key4.len);
ngx_memcpy(key4.data,"*.test.cc",key4.len);
ngx_str_t key5 = ngx_string(".com.cn");
key5.data = ngx_pcalloc(pool, key5.len);
ngx_memcpy(key5.data,".com.cn",key5.len);
ngx_str_t key6 = ngx_string("*.c.com.cn");
key6.data = ngx_pcalloc(pool, key6.len);
ngx_memcpy(key6.data,"*.c.com.cn",key6.len);
ngx_uint_t i;
for(i = 0;i<6;i++){
value[i].a = i;
value[i].b = i*i;
}
ngx_hash_init_t *hash;
hash = ngx_palloc(pool, sizeof(ngx_hash_init_t));
hash->pool = pool;
hash->temp_pool = pool;
hash->name = "test_2_hash";
hash->key = ngx_hash_key;
hash->max_size = 128;
hash->bucket_size = 64/*sizeof(test2) *8*/;
ngx_hash_add_key(&haArr,&key0,&value[0],NGX_HASH_WILDCARD_KEY);
//*.com.cn与.com.cn在dns_wc_head_hash中都是com.cn。因此这里.com.cn不能添加
ngx_hash_add_key(&haArr,&key5,&value[5],NGX_HASH_WILDCARD_KEY);
ngx_hash_add_key(&haArr,&key1,&value[1],NGX_HASH_WILDCARD_KEY);
ngx_hash_add_key(&haArr,&key2,&value[2],NGX_HASH_WILDCARD_KEY);
ngx_hash_add_key(&haArr,&key3,&value[3],NGX_HASH_WILDCARD_KEY);
ngx_hash_add_key(&haArr,&key4,&value[4],NGX_HASH_WILDCARD_KEY);
ngx_hash_add_key(&haArr,&key6,&value[6],NGX_HASH_WILDCARD_KEY);
ngx_hash_key_t *names = haArr.dns_wc_head.elts;
test2 *value00 = names[0].value;
test2 *value01 = names[1].value;
ngx_hash_wildcard_init(hash,haArr.dns_wc_head.elts,haArr.dns_wc_head.nelts);
ngx_str_t test_0 = ngx_string("e.com.cn");
test2 *node = ngx_hash_find_wc_head(hash->hash,test_0.data,test_0.len);
ngx_str_t test_1 = ngx_string("a.a.com.cn");
test2 *node1 = ngx_hash_find_wc_head(hash->hash,test_1.data,test_1.len);
ngx_str_t test_2 = ngx_string(".com.cn");
test2 *node2 = ngx_hash_find_wc_head(hash->hash,test_2.data,test_2.len);
// ngx_hash_wildcard_init(hash,haArr.dns_wc_tail.elts,haArr.dns_wc_tail.nelts);
}
void main(){
// testHash();
testHashWC();
}
1、key数组初始化:ngx_hash_keys_array_init
key数组的初始化,主要是给ngx_hash_keys_arrays_t中的keys,dns_wc_head,dns_wc_tail,几个数组申请内存空间,ngx_hash_keys_arrays_t的主要成员:
ngx_uint_t hsize; //hash表bucket(槽)个数
ngx_array_t keys; //普通(非通配符)key数组
ngx_array_t *keys_hash; //数组的指针,可以指向多个数组,将keys,按hash % hsize分组存放。
//实际上是一个二维数组。
ngx_array_t dns_wc_head; //前缀通配符的key数组,如:*.com.cn
ngx_array_t *dns_wc_head_hash;
ngx_array_t dns_wc_tail; //后缀通配符的key数组,如:test.com*
ngx_array_t *dns_wc_tail_hash;
2、添加key:ngx_hash_add_key
添加key有两种类型的,一个是NGX_HASH_READONLY_KEY普通的key,一个是
NGX_HASH_WILDCARD_KEY通配符的key。
ngx_hash_add_key(&haArr,&key0,&value[0],NGX_HASH_READONLY_KEY);
//or
ngx_hash_add_key(&haArr,&key0,&value[0],NGX_HASH_WILDCARD_KEY);
通配符key的处理:
demo结果如图:
3、普通hash表初始化:ngx_hash_init
方法的最开始,对几个参数做了限制:
max_size不能为0;单个bucket大小不能超过65536-缓存行大小,同时由于bucket之间有个void指针相隔,因此单个bucket的大小不能小于一个ngx_hash_elt_t+一个void指针的大小。由于做了指针对齐,因此ngx_hash_elt_t的大小最小都要两个void指针的大小。bucket的大小最小都要三个void指针大小;64位系统中,一个指针大小8个字节,因此bucket最小3*8 = 24;
对齐方法解析:
#define ngx_align(d, a) (((d) + (a - 1)) & ~(a - 1))//a为2的n次幂
假设a=8,那么a-1二进制为00000111,d的低四位任一一位只要为1,d+(a-1)后,d的第四位或者高于第四位的某一位肯定进1。也就是d的4位之前的值肯定是8的倍数。如d = 17;二进制为00010001,
c = 00010011 + 00000111 == 00011010,第四位之前的00011000值为24,为8的倍数,那么怎么把c的后三位清零了:c & ~(a-1)即可: 00011010 & 11111000 = = 00011000
几个结构体区分
ngx_hash_t,ngx_hash_wildcard_t这两个是hash表,表结构体
ngx_hash_key_t这个是建立hash表前的每个key的结构体
ngx_hash_elt_t 这个是建立hash表后,每个bucket中的元素的结构体。
4、普通hash表查询元素:ngx_hash_find
5、通配符hash初始化:ngx_hash_wildcard_init
方法开始先初始化了两个数组curr_names与next_names,curr_names一个记录当前同级的每个name; next_names则记录当前name第一个点的后续字符串。
下面是通配符hash生成过程:
效果如下:
注意这里:cn.com.c中间有个cc.test.从源码看没有在cn.com时一起处理。而是以完成的cn.com.c另起炉灶创建通配符hash表。计算hash时,都是以cn开头计算hash,则不管最开始的names需要多少个bucket,cn.com.c的通配符hash与cn.com都是放在同一个bucket里面。(即hash冲突场景)
关于对齐指针的后两位为00:
每个地址对应8bit的内存空间(最小内存单元,1字节)
在64位系统中,内存对齐8字节(跨8个地址),每分配两段8字节相邻的内存对齐后的相邻指针相差8,因此指针二进制的后三位为0(32位系统,后两位为0);
因此获取的指针地址都是8的倍数
6、通配符hash表元素查找:ngx_hash_find_wc_head(ngx_hash_find_wc_tail)
这里仅以ngx_hash_find_wc_head为例:
从上面的图可以知道,通配符字符串的每一点分级后,每个分级的结构为:
hwc(或hhint)= =>ngx_hash_t = =>*bucket = =>elt.value= =>下一级hwc(或匹配的数据)
- 当hash表以 *.com.cn构建:即从name = cn.com.开始:一并构建hash的name还有cn.com.a. 与cn.com.b
查找输入为 com.cn : 无法匹配到hash节点;
e.com.cn : 查找到com所在hwc节点的value;即最长匹配到.com.cn
.com.cn : 同e.com.cn
cn : 直接查找第一级的普通hash,但value肯定是null;
a.a.com.cn: 最长匹配到a,匹配到a数据结点的value;- 当hash表以 .com.cn构建:
查找输入为com.cn :查找到com所在hwc节点的value;即最长匹配到.com.cn
其他的输入与*.com.cn构建的hash表一样。
总的来说,通配符的hash,ngx做得挺啰嗦的,个人感觉以点开头或结尾的通配符配置方式没有必要存在,只需要处理*号开头或者结尾的通配字符串,毕竟约定大于配置,搞那么多兼容的配置代码复杂,配置也复杂。不过也可能跟毛子的习惯有关。
参考:
菜鸟nginx源码剖析
Nginx源码分析