Nginx-ngx_hash

来源:互联网 发布:删失数据的定义 编辑:程序博客网 时间:2024/05/21 10:21

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


原创粉丝点击