Nginx-ngx_hash

     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表,由于实现比较复杂,其中参数需要结合上下文来理解,所以放到后面用到的时候再会头看看。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值