Nginx基础. Nginx基本哈希表

来源:互联网 发布:网站源代码怎么修改seo 编辑:程序博客网 时间:2024/05/16 14:32
根据以往的学习经验, 比如STL中的哈希表, 利用开链法, vector+list作为容器, 当hashtable中的元素总数超过一定数量时, 选择扩充vector.
再比如libevent中的哈希表, 与STL中的哈希表类似, 但比较复杂, 每个bucket中都可能有一个链表,每个链表元素中也可能存在一个链表. 但理解起来都并不复杂.
现在看的Nginx中的哈希表, 则与上面谈到的哈希有很明显的不同之处.

在nginx中, 存储server_name和ngx_http_core_srv_conf_t的映射时用到了hash结构.
配置server_names_hash_max_size可以控制bucket的最大数量,server_names_hash_bucket_size可以控制每个bucket的大小
下图, 就是nginx存储不含通配符的server_name时使用的hash结构:
             


       进行解释之前, 需要提一下, nginx中的哈希表一个特别之处在于, 这个hash表是静态只读的,即不能在运行时动态添加新元素的,一切的结构和数据都在配置初始化的时候就已经规划完毕,所以“init”过程的优劣,对运行时查找的性能影响非常大

        我们假设哈希表的bucket数量为size , 每个元素的哈希值为 [hash%size], 从上图可以看到, 此哈希表解决冲突的办法类似开链, 但又并不是使用链表来存储具有相同 [hash%size] 值的元素, 而是令具有相同[hash%size]值的元素存放在一块连续的内存中, 以一个NULL指针作为结尾. 上面我们说到了两个可以控制bucket的最大数量和每个bucket的大小的配置, 但并没有说这个哈希表的bucket数量到底是多少. 试想, 当前我们的bucket大小是确定的, 假如冲突太多, 即有多个元素被存放在同一个bucket, 这样肯定会导致某bucket"超载". 那么解决的办法就是扩大bucket的数量, 即size的值(这样不仅能使原来具有相同[hash%size]的元素不再相同, 还能使冲突发生的更稀疏). 看看能不能保证所有存放多个相同 [hash%size]的元素的bucket不"超载". 但是既然我们设定了bucket的最大数量, 一旦size达到这个值还没能满足我们的要求, 那么就返回错误.
同时, 每个bucket的大小必须能保证存放至少一个元素.无论这个元素的大小是多少.
好了, 大致的介绍就到这里. 下面是详细的代码分析.




一、 数据结构的定义

1、基本哈希表的元素
每个元素都是 key-value的形式
typedef struct {    void             *value;            //即为key-value中对应的value    u_short           len;              //为key-value中key的长度    u_char            name[1];          //为key的首地址. 使用长度为1的数组是为了将来申请的len大小的空间是连续的(详细的请搜索 "0或1长度数组")} ngx_hash_elt_t;

2、基本哈希表结构
typedef struct {    ngx_hash_elt_t  **buckets;          //即为哈希表    ngx_uint_t        size;             //哈希表中bucket的个数} ngx_hash_t;

3、支持通配符的哈希表结构
其实也就是多了一个额外的value指针, 当使用ngx_hash_wildcard_t通配符哈希表作为容器元素时,可以使用value指向用户数据。
这里暂时不做多解释
typedef struct {    ngx_hash_t        hash;    void             *value;} ngx_hash_wildcard_t;

4、该结构也主要用来保存要hash的数据,即键-值对<key,value>
在实际使用中,一般将多个键-值对保存在ngx_hash_key_t结构的数组中,作为参数传给ngx_hash_init()或ngx_hash_wildcard_init()函数
用于表示即将添加到哈希表中的元素
typedef struct {    ngx_str_t         key;    ngx_uint_t        key_hash;         //由哈希函数根据key计算出的值. 将来此元素代表的结构体会被插入bucket[key_hash % size]    void             *value;} ngx_hash_key_t;


5、哈希表初始化用的结构体

typedef struct {    ngx_hash_t       *hash;    ngx_hash_key_pt   key;              //即哈希函数    ngx_uint_t        max_size;         //bucket的最大数量    ngx_uint_t        bucket_size;      //每个bucket的容量    char             *name;             //log用    ngx_pool_t       *pool;             //是在pool中进行内存处理的    ngx_pool_t       *temp_pool;} ngx_hash_init_t;
上面的5个结构体之间的关系必须搞清楚, 所以下面提前附上一张图. 如果看不懂图, 也没关系, 总之在上面的结构体之间关系有个印象后, 继续下面的源代码分析.
下面这张图来自:http://blog.csdn.net/chen19870707/article/details/40794285    (若有侵犯, 立刻删除)





二、基本哈希表的初始化
在分析初始化哈希表的代码之前, 还要提一件事. 就是每个bucket中每个元素的大小. 既然它使用了1长度数组的策略, 那么求其占用内存的大小就不再是单纯的使用sizeof了
typedef struct {    void             *value;            //即为key-value中对应的value    u_short           len;              //为key-value中key的长度    u_char            name[1];          //为key的首地址. 使用长度为1的数组是为了将来申请的len大小的空间是连续的(详细的请搜索 "0或1长度数组")} ngx_hash_elt_t;
于是有了下面这个宏:
#define NGX_HASH_ELT_SIZE(name)                                               \    (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
该宏求大小的过程是:
    先加上一个void指针(指向value)的大小, 在64位机器上即为8字节. 然后再加上key的大小, key的大小使用len来表示了, 再加上short类型的2字节, 但是单纯的相加显然不符合内存地址对齐的规则.
    比如我们定义了下面这个结构体:
struct test{    int x;    char y;};
    那么sizeof(struct test)的大小在编译器内存地址对齐处理过后结果就是8了. 但是这里是我们自己管理内存, 所以内存对齐的事情必须我们自己完成. 所以既然当前的指针占8个字节, 如果key的len加上2的值为13的话, 就应该被调整为16个字节; 如果值为17的话, 就应该被调整为24字节大小.
    所以这个宏就顺利的求出了某元素占用的内存大小的值.

下面就是哈希表初始化的详细代码以及解析

ngx_int_t//传入的参数分别是://    初始化用的结构体//    用于表示即将添加到哈希表中的元素, key-value组成的数组//    即将添加到哈希表中的元素的个数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循环的主要作用是, 判断配置选项配置的每个bucket的容量是否能装的下任意一个元素(元素之间len可以不同). (换句话说, 就是要求每个bucket至少能装一个元素, 无论它是哪个元素)     //如果不符合, 就会返回错误    for (n = 0; n < nelts; n++) {          //下面这个判断就是说, name[n]中的key-value如果被装进ngx_hash_elt_t结构体(哈希表的每个元素的结构), 那么最后此元素的大小必须小于bucket的容量          //否则, 说明我们设置的bucket容量太小          //在调用宏之后还要加上一个指针的大小, 可以根据文章开头给出的图看出来, 每个bucket最后都会有个NULL指针作为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的用处多多. 它的大小是max_size, 是允许的bucket的最大个数.     //它的大小就表明, 以后使用这个test数组, 它与bucket是一一对应的. 即bucket[i]与test[i]是相关的    test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);    if (test == NULL) {        return NGX_ERROR;    }    //bucket_size表示给定的每个bucket的容量.     //减去的就是最后的那个NULL指针的大小. 这个元素是个哨兵元素,用来判断当前bucket是否还有元素    bucket_size = hinit->bucket_size - sizeof(void *);     //既然我们不知到bucket的个数, 那么我们当然是从最小的size开始找(能少就少...)     //什么才是最小的size呢? 根据地址对齐, 一个ngx_hash_elt_t元素最少也要2*8个字节, 那么就拿这个值来找最小的size.    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;    }     //现在, 我们已经得到了最小的可能size值. size个bucket能把nelts个元素全装下, 假设每个elt的大小都只占16字节. 但假设是不成立的, 所以必须找更合适的size    for (size = start; size <= hinit->max_size; size++) {          //这个test数组中存放每个bucket的当前容量,如果某个bucket[i]的容量test[i]大于了规定的最大容量就意味着需要加大hash桶的个数size了          //利用memzero将每个bucket的当前容量test[i]设置为0        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;                //累加要被存放在bucket[key]的内存占用大小            test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));                //一旦超过了, 说明size数目的bucket是不够的            if (test[key] > (u_short) bucket_size) {                goto next;            }        }        goto found;    next:        continue;    }    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);found://这里表明我们已经成功找到了满足条件的size大小.     //依旧是test[i]对应bucket[i], 现在我们要求出所有元素总共需要多少内存. 最后申请出这块内存, 并一一分配给每个bucket     //首先, 我们算出每个bucket要存放的内存容量, 记录在test数组中     //此时NULL指针就需要算上了    for (i = 0; i < size; i++) {        test[i] = sizeof(void *);    }     //算出每个bucket要存放的内存容量, 记录在test数组中    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;     //得到所有元素总共需要的内存, 记录在len中    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];    }     //如果初始化结构体中的hash表不存在, 那我们需要手动申请一下.    if (hinit->hash == NULL) {          //值得注意的是, 这里申请的并不是单纯的基本哈希表结构的内存, 而是包含基本哈希表的通配符哈希表.          //之所以这样设计, 我认为是为了满足将来可能要init通配符哈希表的需求. 既然ngx_hash_wildcard_t中包含基本哈希表, 且使用起来并没有任何麻烦,          //那么这样是不是要显得更好呢?        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;        }          //一开始就定义的二级指针bucket, 指向哈希表中 ngx_hash_elt_t * 构成的数组. 即bucket构成的数组        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中.    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);     //下面就为每个bucket分配内存. 之前已经用test记录了每个bucket应该得到的内存大小.    for (i = 0; i < size; i++) {        if (test[i] == sizeof(void *)) {            continue;        }        buckets[i] = (ngx_hash_elt_t *) elts;          //根据记录好的每个bucket的大小来分配内存        elts += test[i];    }     //既然每个bucket拥有了它应该有的内存, 那么现在就将key-value数据搬进去     //现在依旧是test[i]对应bucket[i]. 此时的test数组用于记录当前的某bucket已经有多少内存被初始化了.     //如果这个元素已经搬到这个bucket中, 下一个元素首地址就是从当前元素首地址加上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]));    }     //在每个bucket最后加上NULL指针. 处理的时候,把它当成一个ngx_hash_elt_t结构看,在该结构中的第一个元素,正好是一个void指针,我们只处理它,别的都不去碰,所以没有越界的问题。    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;    return NGX_OK;}
对于内存分配, 内存地址对齐, cacheline对齐这些我依然有些迷惑的地方, 所以一些地方没有做解释. 主要还是我基础不够扎实!共勉!



三、基本哈希表的查找.
//由key,name,len信息在hash指向的hash table中查找该key对应的value  uvoid *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;    }     //对该bucket进行搜索. 直到该ngx_hash_elt_t结构中的value为NULL    while (elt->value) {        if (len != (size_t) elt->len) {             //先判断长度            goto next;        }        for (i = 0; i < len; i++) {            if (name[i] != elt->name[i]) {         //接着比较name的内容, 可以看到这里的比较很直接                goto next;            }        }        return elt->value;    next:          //这里的地址偏移到下一个ngx_hash_elt_t结构        elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,                                               sizeof(void *));        continue;    }    return NULL;}
这一段没有做过多解释, 理解起来也没有什么难点.



最后, 贴上借鉴的几篇博客:
http://blog.csdn.net/livelylittlefish/article/details/6636229
http://blog.csdn.net/chen19870707/article/details/40794285
http://www.linuxidc.com/Linux/2012-08/67040.htm
http://blog.chinaunix.net/uid-27767798-id-3766755.html
感谢!

0 0
原创粉丝点击