ngx_hash是nginx内部封装的一个hash实现,其实现思想和我们平时讨论的实现差别不大,但是有几个地方还是值得我们仔细研究下。
1, ngx_hash实现的是一个静态的hash表,只能查询不能插入修改,这个应该是和应用场景相关。
2, hash桶的数量并不是事先指定好,而是在初始初始化hash表的时候通过一定的技巧来找到具体所需要的桶的数量。
3, 解决冲突的办法就是开链,每个桶都是一个链表,但是在实现的时候这里还是有很多技巧。
4, 最应该值得一提的是:注意这里hash实现的时候对内存的高效利用。
那么先来看看涉及到的数据结构:
typedef struct {
void *value; //hash key对应的value值的内存地址
u_short len; //hash key string 对应的长度 sizeof(u_short) = 2
/*key 对应内存地址,这里为什么只申请大小为1个元素的字节?实际上每个elt所需要的空间会按照需求来分配,这里利用了ngx_pool里面的技巧,多分配的空间也会自动最佳在name[1]后面,这么做的好处就是做到了空间的高效利用
*/
u_char name[1];
} ngx_hash_elt_t;
typedef struct {
ngx_hash_elt_t **buckets; //hash表
ngx_uint_t size; //桶的个数
} ngx_hash_t;
/*这个结构就是用来初始化一个hash表用的*/
typedef struct {
ngx_str_t key; //hash key的原始值
ngx_uint_t key_hash; //key经过hash函数变换后得到的hash key
void *value; //key对应的value
} ngx_hash_key_t;
typedef struct {
ngx_hash_t *hash; //hash表的指针
ngx_hash_key_pt key; //hash函数
ngx_uint_t max_size; //桶的最大数量
ngx_uint_t bucket_size; //桶的大小
char *name; //hash表明
ngx_pool_t *pool; //hash中的内存来源
ngx_pool_t *temp_pool; //创建hash表过程中的临时表,创建完了可以删除掉
} ngx_hash_init_t;
结合ngx_hash_init_t的数据结构,画出起内存空间的逻辑结构如下:
在看hash初始化过程之前需要介绍下怎么动态的计算每个元素的大小,先看一个宏定义:
#define NGX_HASH_ELT_SIZE(name) \
(sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
在计算元素占用空间的时候分为三部分,需要结合ngx_hash_elt_t这个结构体来看:
typedef struct {
void *value;
u_short len;
u_char name[1];
} ngx_hash_elt_t;
1,sizeof(void *) : 对应的是 *value这个指针所占空间大小 从这里可以看出,实际上value的值是没有拷贝进hash表的,只是用了一个指针来索引。
2, (name)->key.len : 这里对应到原始的key的长度,实际上这个长度就是name[1]开始往后的内存空间。为什么key的原始串被拷贝进了hash表中,但是value不拷贝了?个人猜测是由于key的长度不会太大,但是value可能是一个非常长的字符串,所以权衡下还是不拷贝了。
3, 2: 这个固定值实际上就是sizeof(u_short) 的值,也就是第二个变量的空间大小
最后再这个元素所在的实际空间上做了一个地址对齐操作。
接下来看看hash表的初始化过程,说实话这个过程还是有点复杂,需要认真仔细体会,并且要非常熟悉指针的相关操作。
ngx_int_t
ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
{
/*此函数里有一些magic number,没有办法解释,只能说是作者的经验值*/
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);
if (test == NULL) {
return NGX_ERROR;
}
// 这里的sizeof(void *)就是一个magic number
bucket_size = hinit->bucket_size - sizeof(void *);
/*start的含义是: 在构造hash数组前需要探测需要多少个桶才够,那么start对应的
就是需要桶的个数,下面会先用一些方法探测出来实际需要的桶数*/
//元素较少的情况,那么起始探测值就会比较小
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++) {
//测试数组,全赋值为0
//test数组的含义是,test中每个元素对应到size个桶每个桶需要的空间大小
ngx_memzero(test, size * sizeof(u_short));
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]));
#if 0
ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
"%ui: %ui %ui \"%V\"",
size, key, test[key], &names[n].key);
#endif
//当前这个桶的容量超过了上限,那么需要把桶的数量加大
if (test[key] > (u_short) bucket_size) {
goto next;
}
}
//一轮计算下来发现当前size个桶刚好满足需求了,那么就不探测了
goto found;
next:
continue;
}
//如果探测到桶的最大上限了还不能满足,那么需要修改配置了
//要么增加最大的桶的数量,要么就增加桶的容量
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);
//临时空间及时free掉
ngx_free(test);
return NGX_ERROR;
found:
//当前size的值就是桶的个数
for (i = 0; i < size; i++) {
//为每个桶多增加一个void*的指针空间,这个
//将作为每个桶的结束标记符,参见ngx_hash_elt_t的第一个成员
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;
for (i = 0; i < size; i++) {
if (test[i] == sizeof(void *)) {
//空桶
continue;
}
//将每个桶的空间大小按照cacheline的大小对齐
test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));
//计算所有桶中的元素需要的空间
len += test[i];
}
if (hinit->hash == NULL) {
//为所有桶分配其自身占用的空间, 这里为hash空间多申请了一个 sizeof(ngx_hash_wildcard_t)的空间
//是为了hash和wildcard hash共用一个hash结构
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);
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;
}
//test[i]从这里开始的含义是: 到当前为止i号桶已经占用掉的空间
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]);
//hash的val初始化
elt->value = names[n].value;
//key的长度
elt->len = (u_short) names[n].key.len;
//将key的原始值转换成小写后拷贝到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]));
}
//为每个桶中的链表添加一个结束标记
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;
#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;
}
理解了hash表的初始化过程,那么再看find函数的时候就相对轻松了
/*参数说明
hash: 查询用的hash表。
key: 经过hash函数处理过的键值
name: key的原始字符串
len : key的原始字符串长度
*/
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;
#if 0
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, "hf:\"%*s\"", len, name);
#endif
//直接对key取模,就得到了对应的桶
elt = hash->buckets[key % hash->size];
if (elt == NULL) {
return NULL;
}
//对于桶中每个元素逐个遍历
while (elt->value) {
//key的长度都不相同必然不是要查找的key
if (len != (size_t) elt->len) {
goto next;
}
//逐个字符的比较原始key,这里需要注意的是name这个字符串必须保证传进来的都是小写字母
for (i = 0; i < len; i++) {
if (name[i] != elt->name[i]) {
goto next;
}
}
//查找到了,那么直接返回value对应的地址指针
return elt->value;
next:
//注意这里的技巧,实际上bucket里不是一个链表,而是相邻两个元素紧密相连,直接取地址就行了
elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
sizeof(void *));
continue;
}
return NULL;
}
在ngx_hash中还实现了一个可以使用通配符的hash表,由于实现比较复杂,其中参数需要结合上下文来理解,所以放到后面用到的时候再会头看看。