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;//没有合适的
}




查找就特别的简单了。


总结下:其实可以发现,预先计算出每个桶的大小,然后统一分配了内存空间,这样可以不用使用链表那些来连接,节约了空间。

其实实现还是蛮简单的,没啥特别的地方。