nginx的各个模块都在使用哈希表结构。例如:在解析nginx.conf时,为了解析www.baidu.com.*后置通配符, *.baidu.com后置通配符, 以及www.baidu.com正常server关键字时,会使用哈希表进行存储。nginx实现了3个哈希表,分别为普通哈希表、前置通配符哈希表、后置通过符哈希表。 这三个哈希表都只实现了初始化接口,查询接口,而没有实现插入接口。也就是说nginx的哈希表的所有数据都是初始化的时候预先构造好的,不支持动态的往哈希表中插入数据。因为nginx.conf配置文件中的数据是一种静态结构,预先可以知道有多少配置项。
一、哈希表结构
//哈希表中的元素
typedef struct
{
void *value; //value值,指向用户定义的结构
u_short len; //name的长度,也就是键的长度
u_char name[1]; //key的首地址
} ngx_hash_elt_t;
//哈希表结构
typedef struct
{
ngx_hash_elt_t **buckets; //哈希表的首地址
ngx_uint_t size; //哈希表中槽的总数
} ngx_hash_t;
图: 哈希表内存布局
这是哈希表内存布局图,后面要分析的哈希表初始化,就是为了构造如图所示的结构。查找哈希表也以这张图为例。
1、buckets是一个哈希槽指针数组,每一个哈希槽指向的哈希元素为ngx_hash_elt_t结构。那为什么有些哈希槽指向的哈希元素是一个ngx_hash_elt_t数组呢。这是为了解决哈希冲突,当key相同时,就需要查找哈希元素数组了,直到找到value值。
2、每一个哈希元素结尾都有一个ngx_hash_elt_t空指针,例如value = NULL; 实际上这个value为ngx_hash_elt_t结构中的成员。然后只使用了value字段,其它字段没有使用。将value强制转换为ngx_hash_elt_t结构。例如下图所示:
3、另外哈希元素缓冲区是一个预先开辟的一个缓冲区,之后根据每一个哈希槽指向的哈希元素大小进行分割。
二、哈希表初始化
函数ngx_hash_init使用参数names数组中的元素构造一个如上图所示的哈希表,使用names数组中的元素填充哈希表。参数nelts为数组的大小。
//功能: 初始化哈希表,将names数组中的元素插入到哈希表中
//参数: hinit 哈希表封装结构
// names 需要插入到哈希表中的数组元素
// nelts 数组元素的个数
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++)
{
//加上sizeof(void *)的目的是每个bucket有个结束元素,这个元素的value是个指针,指向null。
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;
}
}
//开辟临时空间,用于测试实际需要多少个bucket
//test数组中存放每个bucket的当前容量,如果某一个key的容量大于了bucket size就意味着需要加大hash桶的个数了
test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
if (test == NULL)
{
return NGX_ERROR;
}
//统计一个bucket有效空间大小,不包括最后一个null指针
bucket_size = hinit->bucket_size - sizeof(void *);
//(2 * sizeof(void *)表示每一个ngx_hash_elt_t结构的最小值
//bucket_size / (2 * sizeof(void *)) 表示每一个bucket可以存放多少个ngx_hash_elt_t
//nelts / (bucket_size / (2 * sizeof(void *))) 表示最小一共需要多少个ngx_hash_elt_t
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;
}
//size从start开始,逐渐加大bucket的个数,直到恰好满足所有具有相同hash%size的元素都在同一个bucket,这样hash的size就能确定了
for (size = start; size < hinit->max_size; size++)
{
//每次循环新的size的时候需要将旧test的数据清空
ngx_memzero(test, size * sizeof(u_short));
//对每一个size,统计该size是否满足每一个哈希元素都尽可能分配到每一个哈希槽中
for (n = 0; n < nelts; n++)
{
if (names[n].key.data == NULL)
{
continue;
}
key = names[n].key_hash % size;
//test数组中存放每个bucket的当前容量
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
//如果某个bucket的size超过了bucket_size,那么加大bucket的个数,使得元素分布更分散一些
if (test[key] > (u_short) bucket_size)
{
goto next;
}
}
goto found;
next:
continue;
}
//循环结束都没有找到每一个可以尽可能平均分配到每一个槽中,则报错,说明max_size设置得不合理
//最终如果size超过了max size,就报错
ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
"could not build the %s, you should increase "
"either %s_max_size: %i or %s_bucket_size: %i",
hinit->name, hinit->name, hinit->max_size,
hinit->name, hinit->bucket_size);
ngx_free(test);
return NGX_ERROR;
found:
//每个bucket都有一个空指针
for (i = 0; i < size; i++)
{
test[i] = sizeof(void *);
}
//遍历每个bucket计算每个bucket的大小
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;
//统计哈希表所有元素的大小
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];
}
if (hinit->hash == NULL)
{
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;
}
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;
}
}
//开辟一段连续的元素空间
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);
//把elts地址空间分发到每个bucket中,此时test中每一个元素表示对应槽的大小
for (i = 0; i < size; i++)
{
//如果每个槽没有需要存入的元素,则buckets[i] = NULL;
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;
}
//将names数组中的值插入到哈希表中
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);
//更新同一个bucket中下一个元素的位置
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
}
//对每一个哈希槽,如果哈希槽中有元素,则需要把该槽下的最后一个元素后加入一个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;
}
ngx_free(test);
//更新哈希表结构
hinit->hash->buckets = buckets;
hinit->hash->size = size;
return NGX_OK;
}
初始化流程比较多,大体上分为流程图中的几个步骤。有个疑问? names数组有nelts个元素,那使用nelts个哈希槽构造的哈希表进行存储不就好了。何必大费周章的计算哈希槽元素的个数。这是因为names数组是由nelts个元素组成, 但有可能数组中的元素可能存在哈希冲突,这个时候只创建nelts个哈希槽就不够用了。因此为了解决哈希冲突,需要曾大哈希元素个数,来减少冲突。这个时候就需要计算哈希槽元素的个数,目的是为了尽量减少哈希元素的冲突。
nginx使用试错法来计算需要多少个哈希槽元素。也就是对每一个哈希槽个数,都计算这个哈希表是否能满足names数组中的元素冲突最少。如果满足,这个值就为哈希槽个数。否则加大哈希槽元素个数,继续试错,直到满足条件。
三、哈希查找
使用ngx_hash_find函数可以在图:哈希表内存布局中查找到key对应的value值。查找过程比较简单,直接上代码:
//功能: 在哈希表中按照key查找对应的value
//参数: hash 哈希表结构
// key 是根据散列函数计算出来的值
// name 关键字
// len 关键字长度
//返回值: key对应的value
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];
if (elt == NULL)
{
return NULL;
}
//对key值相同的哈希槽,使用连续的数组存放了多个value值。目的是为了解决冲突,
//则遍历这个连续的数组空间,找到value
while (elt->value)
{
//先比较长度,长度不相等,则说明key肯定不相等
if (len != (size_t) elt->len)
{
goto next;
}
//长度相等后,则比较key
for (i = 0; i < len; i++)
{
if (name[i] != elt->name[i])
{
goto next;
}
}
//找到key对应的value
return elt->value;
next:
//获取连续数组中的下一个元素
elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
sizeof(void *));
continue;
}
return NULL;
}
需要注意的是,如果哈希存在冲突, 则需要在哈希元素数组中进行查找,这个时候时间复杂度为O(n)