相关介绍
nginx哈希表的实现是 ngx_hash_t。ngx_hash_t的实现也与数据结构教科书上所描述的hash表的实现是大同小异。
对于常用的解决冲突的方法有线性探测,二次探测和开链法等。ngx_hash_t使用的是开链法。
源码分析
哈希表的结构
ngx_hash_elt_t是hash表中每一个元素的结构,实际存储在hash表中的结构
typedef struct {
void *value;
u_short len;
u_char name[1];
} ngx_hash_elt_t;
ngx_hash_key_t这是用于初始化hash表的结构,并不会存储在hash表内部
typedef struct {
ngx_str_t key;
ngx_uint_t key_hash;
void *value;
} ngx_hash_key_t;
key表示元素对应的key
key_hash表示元素的key的hash值
value 表示对应元素的value
ngx_hash_t是实际的hash表的结构。二维数组buckets是hash表的桶,size是桶的数量。
typedef struct {
ngx_hash_elt_t **buckets;
ngx_uint_t size;
} ngx_hash_t;
ngx_hash_init_t结构体用于初始化函数初始化hash表
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 该字段如果为NULL,那么调用完初始化函数后,该字段指向新创建出来的hash表。如果该字段不为NULL,那么在初始的时候,所有的数据被插入了这个字段所指的hash表中
key指向从字符串生成hash值的hash函数
max_size是hash表中的桶的个数。该字段越大,元素存储时冲突的可能性越小,每个桶中存储的元素会更少,则查询起来的速度更快。
bucket_size每个桶的最大限制大小,单位是字节。如果在初始化一个hash表的时候,发现某个桶里面无法存的下所有属于该桶的元素,则hash表初始化失败
name这是这个hash表的名字
pool是hash表分配内存使用的pool
temp_pool是hash表使用的临时pool,在初始化完成以后,该pool可以被释放和销毁掉。
哈希表的初始化
ngx_int_t
ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
ngx_uint_t nelts);
hinit是上面所述的初始化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++) {
/*bucket_size不可以小于一个(内存对齐的ngx_hash_elt_t的大小加上一个指针的大小)*/
if (hinit->bucket_size <
/*NGX_HASH_ELT_SIZE用于获取(单个内存对齐的ngx_hash_elt_t)的大小*/
NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))
{
//如果桶的大小还不足以装下一个元素,就给出创建失败
ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,"could not build the %s, you shouldincrease%s_bucket_size: %i", hinit->name, hinit->name, hinit->bucket_size);
return NGX_ERROR;
}
}
//直接分配内存,大小为无符号short乘以hash表中桶的个数
test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
if (test == NULL) {
return NGX_ERROR;
}
//获取每一个桶实际存储元素的大小
bucket_size = hinit->bucket_size - sizeof(void *);
/*获取hash表包含桶的数量的初始尝试大小,小于该大小的桶的数量一定初始化不了hash表*/
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++) {
ngx_memzero(test, size * sizeof(u_short));
for (n = 0; n < nelts; n++) {
if (names[n].key.data == NULL) {
continue;
}
//hash散列
key = names[n].key_hash % size;
/*计算散列后,每一个桶的总大小(实际只是计算,并无任何处理)*/
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
/*必须保证每一个桶都能装下所有的元素,不然总的桶的数量+1,然后进行下一次尝试*/
if (test[key] > (u_short) bucket_size) {
goto next;
}
}
goto found;
next:
continue;
}
//如果实在没有找到,那么将给出警告,并且将包含桶的数量设置为最大
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);
//此处正式开始,前面都是打酱油。
found:
//其实test里面都是存放的计算的需要的大小
/*先初始化为一个指针所占的大小,多加一个指针,最后会发现
该指针会放在一个桶的所有元素的后面,并赋值为空*/
for (i = 0; i < size; i++) {
test[i] = 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;
//把每一个桶需要的内存(内存对齐后)加起来,放到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];
}
//现在才正式开始,前面都是计算。
//如果初始化的hash没有分配,那么久在pool里面给它分配内存
if (hinit->hash == NULL) {
hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t) + size * sizeof(ngx_hash_elt_t *));
//分配失败就free test然后直接返回
if (hinit->hash == NULL) {
ngx_free(test);
return NGX_ERROR;
}
/*(指向ngx_hash_elt_t 的指针)的一个指针,相当于忽略前面的ngx_hash_wildcard_t,从size * sizeof(ngx_hash_elt_t *)开始指向指针的指针给buckets*/
buckets = (ngx_hash_elt_t **)((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));
} else {
//如果已经给定了hash的内存
buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *));
//只需要分配桶的内存
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进行内存对齐修正
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];
}
//memset test
for (i = 0; i < size; i++) {
test[i] = 0;
}
//将每一个key都放进去
for (n = 0; n < nelts; n++) {
if (names[n].key.data == NULL) {
continue;
}
key = names[n].key_hash % size;
//buckets[key]表示该元素在第几个桶
//test[key]表示该桶,这个元素的起始地址
/*第一个起始地址是桶的开始地址buckets[key],第二个肯定是第一个基础上加起始地址,一次类推,上次的地址绝对偏差保存在test数组里面,每一个桶对应test里面的一个元素*/
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]);
追加一个NULL,分配的时候也多分配了一个指针。
elt->value = NULL;
}
//释放test
ngx_free(test);
//返回数据
hinit->hash->buckets = buckets;
hinit->hash->size = size;
return NGX_OK;
}
hash的查找
hash表的查找比较简单,和我们常见的一样,先hash取值,然后从查找。key是name对应的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];
//这个桶是NULL,那么久直接返回NULL。
if (elt == NULL) {
return NULL;
}
while (elt->value) {
//如果key的长度不一样,肯定不同,直接next
if (len != (size_t) elt->len) {
goto next;
}
//比较key的大小,要是不一样,直接next
for (i = 0; i < len; i++) {
if (name[i] != elt->name[i]) {
goto next;
}
}
//如果一样,就直接return掉。
return elt->value;
next:
//指针后移到下一个元素的位置,并进行内存对齐矫正
elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,sizeof(void *));
continue;
}
return NULL;
}
总结
hash表的实现结构比较简单,但是初始化的时候比较繁琐,会调节几个参数,然后再来根据初始化的key来计算每一个桶的大小,然后将所有的元素填充进去。hash表的查找比较简单,也比较传统,贴上来也是给各位压压惊,我们下一节再研究ngx_hash_wildcard_t(支持通配符的散列表)。