nginx----hash

hash也是一种数据结构,相比于list、queue等要复杂一些。在利用hash表存储数据时,都是通过key-value的形式,通过对key进行hash得到一个key-hash值,然后利用该值找到数据应该在hash表中存放的位置,再插入数据。

1、基本数据结构

实现hash表涉及的主要数据结构如下:

typedef struct {    //hash表
    ngx_hash_elt_t  **buckets;    //指向hash桶的二级指针
    ngx_uint_t        size;       //桶的个数
} ngx_hash_t;
hash表中的每一个元素的数据结构如下:

typedef struct {
    void             *value;   //实际数据
    u_short           len;     //key的长度
    u_char            name[1];  //key值,即等待hash的数据
} ngx_hash_elt_t;
nginx用 ngx_hash_init_t结构来初始化hash表,其数据结构如下:

typedef struct {
    ngx_hash_t       *hash;  //指向hash表
    ngx_hash_key_pt   key;   //函数指针

    ngx_uint_t        max_size;    //hash桶的最大个数
    ngx_uint_t        bucket_size;  //hash桶的最大空间

    char             *name;
    ngx_pool_t       *pool;
    ngx_pool_t       *temp_pool;
} ngx_hash_init_t;
将上述数据结构联系在一起,一个简单的示意图如下:

nginx----hash_第1张图片

nginx通过ngx_hash_key_t来保存要hash的数据,在初始化前就已经准备好了,其数据结构如下:

typedef struct {
    ngx_str_t         key;    //等待hash的key值
    ngx_uint_t        key_hash;  //对key值hash后得到的一个整数
    void             *value;  //实际待存储的数据
} ngx_hash_key_t;
其中key使用了nginx中定义的字符串的存储方式,通过一个长度值和一个指针来表示一个字符串,而不再是以前那种以'\0'结尾的字符串。其定义如下

typedef struct {
    size_t      len;    //字符串的长度
    u_char     *data;   //指向字符串的首地址
} ngx_str_t;
上述结构的简单示意图如下:


2、hash的初始化

nginx通过函数ngx_int_t
ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts);来完成hash表的初始化。

其中hinit结构为待初始化的hash表结构,names为等待hash的数据,一般为数组形式的,nelts为等待hash的数据的个数,即names数组的元素个数。

1) 首先判断hash表的桶空间是否足够存储待处理的数据,即每个bucket指向的ngx_hash_elt_t空间是否足够存储待处理的数据

    for (n = 0; n < nelts; n++) {
        if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))
        {
            return NGX_ERROR;
        }
    }
其中hinit->bucket_size为每个桶空间的大小。

NGX_HASH_ELT_SIZE宏定义了获取待存储的元素大小的方法,其定义如下:

#define NGX_HASH_ELT_SIZE(name)                                               \
    (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
sizeof(void *):指向value的指针

ngx_align((name)->key.len:待hash的key值的长度

2:len,即用来指示key的长度,其类型为u_short,2字节

这三个部分分别对应ngx_hash_elt_t结构体中的value、name、len的长度,后半部分的sizeof(void *)表示是4字节对齐的(32位时)。

只有当桶空间足够存储每个待存储的数据时,才会继续初始化。

2) 确定存放所有数据需要的桶的个数

由于hash是通过将key_hash值对一个整数求余来将各个数据分散到不同的桶中的,因此一个桶中可能存储多个数据,因此必须确定合适的桶的个数,来保证将数据分散后仍能将数据存放到对应的桶中。通过一个临时变量test来记录每个桶需要的内存大小,来确定最终需要的桶的个数。

    test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);   //2*max_size
    if (test == NULL) {
        return NGX_ERROR;
    }
test为一个数组,大小为max_size,即桶的个数。其元素类型为u_short,2个字节,用于记录初始化过程中每个桶用到的长度信息,故申请2*max_size大小的内存空间。且由于test只是临时用到,因此没有在内存池中分配内存,而是直接通过malloc申请内存,待函数执行完后,直接free。


粗略计算最小的桶的个数:

在一个hash表中,所有元素的内存都是在同一块内存块上分配的,不同的桶之间通过一个NULL指针分隔开来,因此首先要为每个桶空间预留一个sizeof(NULL)的长度

    bucket_size = hinit->bucket_size - sizeof(void *);

    start = nelts / (bucket_size / (2 * sizeof(void *)));
    start = start ? start : 1;
一个元素最少需要  NGX_HASH_ELT_SIZE(&names[n]) > (2 * sizeof(void *))的空间(sizeof(void *)字节对齐),因此bucket_size大小的桶最多能存放bucket_size / (2 * sizeof(void *)))个元素,nelts个元素则最少需要start个桶。

利用key_hash和size将数据分散到各个桶中,来确定桶的个数

for (size = start; size <= hinit->max_size; 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 (test[key] > (u_short) bucket_size) { //当数据过于集中在一个桶时,增大桶的个数,重新统计
                goto next;
            }
        }
        goto found;
    next:
        continue;
    }
nginx中的hash表采用求余的方法来确定数据对应的hash桶,上述代码表明,nginx是将key_hash对桶的个数求余的,其中size表示了桶的个数。

key为求余后对应的桶的索引,test[key]则记录了该桶所需的大小。

当size=1时,求余后得到的key均为0.则所有数据均会集中到一个桶bucket[0]中。

当test[key]>bucket_size时,表明数据过于集中,有的桶不足以存放如此多的数据。因此跳转到next,增大size的大小,即增加桶的个数,重新分散数据,以保证每个桶都足够存放相应的数据。

若每个桶的大小都足以存放对应数据时,跳转到found,则此时size表示了最终所需的桶的个数。

3) 确定了桶的个数后,进一步详细确定每个桶待存放数据的大小

for (i = 0; i < size; i++) {
        test[i] = sizeof(void *);  //在每个桶的尾部用一个NULL指针,用于分隔相邻的桶
    }
    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));
        len += test[i];
    }
此时test数组中的每个元素记录了对应的桶待存放数据的大小(字节对齐后),len为所需的总大小。
4) 然后为hash表和每个桶分配内存

    if (hinit->hash == NULL) {
        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;
        }
    }
可以发现桶的个数为size,大小为sizeof(ngx_hash_elt_t *);
5)为所有数据的存储区分配内存空间(字节对齐后)

由上可知,len为存储所有数据所需的空间大小

    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);
6)然后根据每个桶中待处理数据的大小来确定每个桶的起始位置

    for (i = 0; i < size; i++) {
        if (test[i] == sizeof(void *)) {   //表示该桶没有数据
            continue;
        }
        buckets[i] = (ngx_hash_elt_t *) elts;  //记录每个桶的起始位置
        elts += test[i];

    }
得到结果如下所示:


7) 将数据放到对应的桶中

    for (i = 0; i < size; i++) {
        test[i] = 0;   //将test中统计的信息清零
    }
    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;  //将数据存入hash表中
        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++) {
        if (buckets[i] == NULL) {
            continue;
        }
        elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);
        elt->value = NULL;
    }
8) 最后释放资源并设置某些字段的值

    ngx_free(test);   //释放临时变量test

    hinit->hash->buckets = buckets;   //将hinit结构中的buckets指针指向已初始化好的buckets
    hinit->hash->size = size;   //设置hash表中桶的个数为size
这样就完成了hash表的初始化


你可能感兴趣的:(nginx----hash)