hash 一个常用的数据结构,一般来说 hash主要是主要关注key的散列算法和冲突处理的方法。nginx的hash对冲突处理使用的是开链法。并且ngx_hash是一次初始化,没有删除和添加方法。来看下ngx_hash的实现吧。首先还是数据结构。
typedef struct {
ngx_str_t key; //name
ngx_uint_t key_hash;//计算出来的hash值
void *value;//保存的值
} ngx_hash_key_t;
这个数据结构很清晰,没有什么好特别的。
typedef struct {
void *value; //存储值
u_short len; //长度
u_char name[1]; //名字 1的原因是不知道具体的长度,到时候当成指针使用。配合上面的len就可以完美的找出key
} ngx_hash_elt_t;//节点
每一个hash节点的内容。
typedef struct {
ngx_hash_elt_t **buckets; //保存的桶
ngx_uint_t size; //大小
} ngx_hash_t;
hash表的存储内容。
typedef struct {
ngx_hash_t *hash; //使用哪个hash表来进行存储。
ngx_hash_key_pt key; // 计算key的hash值散列算法指针
ngx_uint_t max_size; //最大的大小
ngx_uint_t bucket_size;//桶的大小
char *name; //名字
ngx_pool_t *pool; //分配内存使用的pool
ngx_pool_t *temp_pool;//临时pool
} ngx_hash_init_t;
#define NGX_HASH_ELT_SIZE(name) \
(sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
计算每一个节点的大小
对应到我们的数据结构ngx_hash_elt_t 第一个sizeof(void*) 就是value的大小。 key.len +2 等于 name + len的大小 ,然后与对齐。
来看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++) {//循环判断下,这个桶装得下不
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;
}
}
test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);//分配max_size的空间
if (test == NULL) {
return NGX_ERROR;
}
bucket_size = hinit->bucket_size - sizeof(void *); //预留一个szieof(void*)大小
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++) { //找粗来一个差不多合适的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; //散列后对应的位置咯。
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));//累计每个桶大小情况
if (test[key] > (u_short) bucket_size) { //装不下了
goto next;
}
}
goto found; //每一个元素都能有个桶来放了。
next:
continue;//这个size玩玩了不行。
}
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);//很明显是找不到合适的size来跟你玩
found:
for (i = 0; i < size; i++) {//找到一个差不多size
test[i] = sizeof(void *); //预留一个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;
for (i = 0; i < size; i++) {
if (test[i] == sizeof(void *)) {
continue;
}
test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));//和ngx_cacheline_size对齐
/*
我们知道,CACHE与内存交换的最小单位为CACHE LINE
假如我们的CACHE LINE为64,如果我们的CACHE LINE全是对齐的,读取一个小于等于CACHE LINE大小的cache需要一个cache,
但是如果是没有对齐的话,一个小于等于CACHE LINE的内存放入cache的时候有可能会占用两个,读的时候有可能是读取两个,读取一个和两个的区别就是类是于内存的缺页中断,再来一次。、
关于对齐,到时候重新写个文章吧
*/
len += test[i];//需要的大小累加
}
if (hinit->hash == NULL) {//米有hash表给你用
hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t)
+ size * sizeof(ngx_hash_elt_t *));//把hash的内存一起分配了
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 *));//只分配buckets的
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);//这里为了对齐
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) {
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);//拷贝小写过去
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));//算出下一个过来的位置
}
for (i = 0; i < size; i++) {//每个buckets多了sizeof(void *)
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;
}
他的处理流程很简单:
bucket_size是否足够支持一个元素的存储。
然后根据散列存储的原则,找出一个桶个数来做了存储
计算出每一个桶的大小。
计算出总共桶的大小。
给桶分配空间。
然后往桶里放元素。
具体的直接参考完整的注释。
接着 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];//看下在哪个桶
if (elt == NULL) { //这个桶没有 代表查询失败
return NULL;
}
while (elt->value) {//尾部 初始化的时候看过
if (len != (size_t) elt->len) {//长度不一样
goto next;
}
for (i = 0; i < len; i++) {
if (name[i] != elt->name[i]) {//内容不一样
goto next;
}
}
return elt->value;//一样的
next:
elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
sizeof(void *));//偏移到下一个位置
continue;
}
return NULL;//没有合适的
}
查找就特别的简单了。
总结下:其实可以发现,预先计算出每个桶的大小,然后统一分配了内存空间,这样可以不用使用链表那些来连接,节约了空间。
其实实现还是蛮简单的,没啥特别的地方。