nginx 哈希表数据结构

1.哈希表ngx_hash_t的优势和特点

哈希表是一种典型的以空间换取时间的数据结构,在没有冲突的情况下,对任意元素的插入、索引、删除的时间复杂度都是O(1)。这样优秀的时间复杂度是通过将元素的key值以hash方法f映射到哈希表中的某一个位置来访问记录来实现的,即键值为key的元素必定存储在哈希表中的f(key)的位置。当然,不同的元素的hash值可能相同,这就是hash冲突,有两种解决方法(分离链表发和开放地址发),ngx采用的是开放地址法.

  • 分离链表法是通过将冲突的元素链接在一个哈希表外的一个链表中,这样,找到hash表中的位置后,就可以通过遍历这个单链表来找到这个元素。
  • 开放地址法是插入的时候发现 自己的位置f(key)已经被占了,就向后遍历,查看f(key)+1的位置是否被占用,如果没被占用,就占用它,否则继续相后,查询的时候,同样也如果f(key)不是需要的值,也依次向后遍历,一直找到需要的元素。
     

    2.源代码位置

     

    头文件:http://trac.nginx.org/nginx/browser/nginx/src/core/ngx_hash.h

    源文件:http://trac.nginx.org/nginx/browser/nginx/src/core/ngx_hash.c

     

    3.数据结构定义

     

       ngx_hash的内存布局如下图,它采用了三级管理结构,只要由以下几个结构足证:


     

    3.1 hash表中元素ngx_hash_elt_t

    ngx_hash_elt是哈希表的元素,它负责存储key-value值,其中key为name 、valuevalue,这里看到name仅为一个字节的uchar数组,仅用于指出key的首地址,而key的长度是可变的,所以哈希表元素的大小并不是由sizeof(ngx_hash_elt_t_t)决定的,而是在初始化时指定的。


    • value是指向用户自定义数据类型的指针,如果hash表中这个位置没有元素,则value = NULL
    • len 表示关键字key的长度,关键字的长度是不定的
    • name  为key的首地址
    1: typedef struct {
    void             *value;            //指向用户自定义元素的指针,如果该位置没有元素,即为NULL
    //key的长度
    //key的首地址
       5: } ngx_hash_elt_t;

     

     

    3.2 基本哈希表结构 ngx_hash_t

    哈希表结构是一个ngx_hash_elt_t的数组,其中buckets指向哈希表的首地址,也是第一个槽的地址,size为哈希表中槽的总个数

    1: typedef struct {
       2:     ngx_hash_elt_t  **buckets;
       3:     ngx_uint_t        size;
       4: } ngx_hash_t;

    3.3 支持通配符的哈希表结构 ngx_hash_wildcard_t

    ngx_hash_wildcard_t专用于表示牵制或后置通配符的哈希表,如:前置*.test.com,后置:www.test.* ,它只是对ngx_hash_t的简单封装,是由一个基本哈希表hash和一个额外的value指针,当使用ngx_hash_wildcard_t通配 符哈希表作为容器元素时,可以使用value指向用户数据。

    1: typedef struct {
       2:     ngx_hash_t        hash;
    void             *value;
       4: } ngx_hash_wildcard_t;

     

    3.4   组合类型哈希表 ngx_hash_combined_t

    ngx_hash_combined_t是由3个哈希表组成,一个普通hash表hash,一个包含前向通配符的hash表wc_head和一个包含后向通配符的hash表 wc_tail。

    1: typedef struct {
       2:     ngx_hash_t            hash;
       3:     ngx_hash_wildcard_t  *wc_head;
       4:     ngx_hash_wildcard_t  *wc_tail;
       5: } ngx_hash_combined_t;

     

    3.5 哈希表初始化ngx_hash_init_t

    ·hash初始化结构是ngx_hash_init_t,ngx_hash_init用于初始化哈希表,初始化哈希表的槽的总数并不是完全由 max_size成员决定的,而是由在做初始化时预先加入到哈希表的所有元素决定的,包括这些元素的总是、每个元素的关键字长度等,还包括操作系统的页面 大小,这个算法比较复杂,可以在ngx_hash_init函数中找到这个算法它的结构如下:

    1: typedef struct {
    //指向普通的完全匹配哈希表
    //哈希方法
       4:  
    //哈希表中槽的最大个数
    //哈希表中一个槽的空间大小,不是sizeof(ngx_hash_elt_t)
       7:  
    char             *name;            //哈希表的名称
    //内存池,它负责分配基本哈希列表、前置通配哈希列表、后置哈希列表中所有槽
    //临时内存池,它仅存在初始化哈希表之前。用于分配一些临时的动态数组,带通配符的元素初始化时需要用到临时动态数组
      11: } ngx_hash_init_t;

     

    3.6 预添加哈希散列元素结构 ngx_hash_key_t

     

    ngx_hash_key_t用于表示即将添加到哈希表中的元素,其结构如下:

    1: typedef struct {   
    //元素关键字
    //由哈希方法算出来的哈希值
    void             *value;         //指向用户自定义数据
       5: } ngx_hash_key_t;

     

    3.7 ngx_hash_key_t构造结构 ngx_hash_keys_arrays_t

     

    可以看到,这里设计了3个简易的哈希列表( keys_hash、dns_wc_head_hash、dns_wc_tail_hash),即采用分离链表法来解决冲突,这样做的好处是如果没有这三 个次啊用分离链表法来解决冲突的建议哈希列表,那么每添加一个关键字元素都要遍历数组(数组采用开放地址法解决冲突,冲突就必须遍历)。

    1: typedef struct {
    //散列中槽总数
       3:  
    //内存池,用于分配永久性的内存
    //临时内存池,下面的临时动态数组都是由临时内存池分配
       6:  
    //存放所有非通配符key的数组。
    //这是个二维数组,第一个维度代表的是bucket的编号,那么keys_hash[i]中存放的是所有的key算出来的hash值对hsize取模以后的值为i的key。假设有3个key,分别是key1,key2和key3假设hash值算出来以后对hsize取模的值都是i,那么这三个key的值就顺序存放在keys_hash[i][0],keys_hash[i][1], keys_hash[i][2]。该值在调用的过程中用来保存和检测是否有冲突的key值,也就是是否有重复。
       9:  
    //存放前向通配符key被处理完成以后的值。比如:“*.abc.com”被处理完成以后,变成“com.abc.”被存放在此数组中。
    //该值在调用的过程中用来保存和检测是否有冲突的前向通配符的key值,也就是是否有重复。
      12:  
    //存放后向通配符key被处理完成以后的值。比如:“mail.xxx.*”被处理完成以后,变成“mail.xxx.”被存放在此数组中。
    //该值在调用的过程中用来保存和检测是否有冲突的后向通配符的key值,也就是是否有重复。
      15: } ngx_hash_keys_arrays_t;

     

    4.普通哈希表初始化ngx_hash_init

    初始化设计操作设计还是很巧妙的,巧妙的结构设计在这里都得到体现,主要有:

    • 桶大小估算,这里一开始 按照 ngx_hash_elt_t估算最小需要的桶的数目,然后再从这个数目开始搜索,大大提高了效率,值得学习。
    • ngx_hash_elt_t 中uchar name[1]的设计,如果在name很短的情况下,name和 ushort 的字节对齐可能只用占到一个字节,这样就比放一个uchar* 的指针少占用一个字节,可以看到ngx是真的在内存上考虑,节省每一分内存来提高并发。

    先看一下求ngx_hash_elt_t的占用内存大小的方法,前面提到不是用 sizeof(ngx_hash_elt_t),原因是因为name的特殊设计,正确的求法如下,可以看到是一个sizeof(void*)  即用户自定义指针(value),一个长度len(sizeof(unsigned short)) 和 name 的真实长度len 对void*字节对齐。

    1: typedef struct {
    void             *value;
       3:     u_short           len;
       4:     u_char            name[1];
       5: } ngx_hash_elt_t;
       6:  
       7: #define NGX_HASH_ELT_SIZE(name)                                               \
    sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))

    如下是注释版源代码,比较长,总的流程即为:预估需要的桶数量 –> 搜索需要的桶数量->分配桶内存->初始化每一个ngx_hash_elt_t

  • 1: //hinit是哈希表初始化结构指针,names是预添加到哈希表结构的数组,nelts为names元素个数
       3: {
       4:     u_char          *elts;
       5:     size_t           len;
       6:     u_short         *test;
       7:     ngx_uint_t       i, n, key, size, start, bucket_size;
       8:     ngx_hash_elt_t  *elt, **buckets;
       9:     
    //遍历预添加数组names,数组的每一个元素,判断槽的大小bucket_size是否够分配  
    for (n = 0; n < nelts; n++)
      12:      {
    if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))
      14:         {
    //有任何一个元素,槽的大小不够为该元素分配空间,则退出  
      16:             ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
    "could not build the %s, you should "
    "increase %s_bucket_size: %i",
      19:                           hinit->name, hinit->name, hinit->bucket_size);
    return NGX_ERROR;
      21:         }
      22:     }
      23:  
    //test 是short数组,用于临时保存每个桶的当前大小 
    sizeof(u_short), hinit->pool->log);
    if (test == NULL) {
    return NGX_ERROR;
      28:     }
      29:     
    // 为什么会多一个指针大小呢?这里主要还是为了后面将每个元素对齐到指针  
    sizeof(void *);
      32:  
    /* 计算需要桶数目的下界
      34:        每个元素最少需要 NGX_HASH_ELT_SIZE(&name[n]) > (2*sizeof(void*)) 的空间
      35:        因此 bucket_size 大小的桶最多能容下 bucket_size/(2*sizeof(void*)) 个元素
      36:        因此 nelts 个元素就最少需要start个桶。
      37:      */
    sizeof(void *)));
      39:     start = start ? start : 1;
      40:  
    if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {
      42:         start = hinit->max_size - 1000;
      43:     }
      44:  
    /* 从最小桶数目开始试,计算容下 nelts 个元素需要多少个桶 */   
    for (size = start; size <= hinit->max_size; size++) {
    sizeof(u_short));
      48:  
    for (n = 0; n < nelts; n++) {
    if (names[n].key.data == NULL) {
    continue;
      52:             }
      53:             
    //根据哈希值计算计算要放在哪个桶
      55:             key = names[n].key_hash % size;
    //将桶大小增加一个ngx_hash_elt_t
      57:             test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
      58:  
      60:             ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
    "%ui: %ui %ui \"%V\"",
      62:                           size, key, test[key], &names[n].key);
      64:             
    //发现放在size 个桶中,还是有放不下的情况,所以需要的桶+1,再循环
    if (test[key] > (u_short) bucket_size) {
    goto next;
      68:             }
      69:         }
      70:         
    //names中所有元素都可以放入size个桶中,找到正确的size大小了
    goto found;
      73:  
      74:     next:
      75:  
    continue;
      77:     }
      78:  
      79:     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",
      83:                   hinit->name, hinit->name, hinit->max_size,
      84:                   hinit->name, hinit->bucket_size, hinit->name);
      85:  
      87:     /* 执行到这里就得到了 容下 nelts 个元素需要 size 个桶 ,初始化每个桶大小*/  
    for (i = 0; i < size; i++) {
    sizeof(void *);
      90:     }
      91:     
    //计算实际上每个桶的大小
    for (n = 0; n < nelts; n++) {
    if (names[n].key.data == NULL) {
    continue;
      96:         }
      97:         
    //根据哈希值,应该放在第key个桶中,大小增加一个ngx_hash_elt_t
      99:         key = names[n].key_hash % size;
     100:         test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
     101:     }
     102:  
     103:     len = 0;
     104:  
    for (i = 0; i < size; i++) {
    //桶中没有元素  
    if (test[i] == sizeof(void *)) {
    continue;
     109:         }
     110:         
    //行缓存对其,CPU读取内存不是一个一个字节,而是以cacheline_size为单位,以行缓存对其,提高CPU读取效率
     112:         test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));
     113:  
     114:         len += test[i];
     115:     }
     116:    
    // 这里似乎看起来很奇怪,既然是hash,为什么分配空间的大小又跟hash结构体一点关联都没有呢
    // 这里很有意思,因为ngx_hash_wildchard_t包含hash这个结构体,所以就一起分配了
    // 并且把每个桶的指针也分配在一起了,这种思考跟以前学的面向对象思想很不一样,但这样会很高效
    if (hinit->hash == NULL) {
    sizeof(ngx_hash_wildcard_t)
    sizeof(ngx_hash_elt_t *));
    if (hinit->hash == NULL) {
     124:             ngx_free(test);
    return NGX_ERROR;
     126:         }
     127:         
     128:         buckets = (ngx_hash_elt_t **)
    sizeof(ngx_hash_wildcard_t));
     130:  
    else {
    //分配桶
    sizeof(ngx_hash_elt_t *));
    if (buckets == NULL) {
     135:             ngx_free(test);
    return NGX_ERROR;
     137:         }
     138:     }
     139:     
    //将内存池对其到行缓存,提高CPU读取效率
     141:     elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size);
    if (elts == NULL) {
     143:         ngx_free(test);
    return NGX_ERROR;
     145:     }
     146:     
    //对指针地址的对齐操作
     148:     elts = ngx_align_ptr(elts, ngx_cacheline_size);
     149:  
    for (i = 0; i < size; i++) {
    if (test[i] == sizeof(void *)) {
    continue;
     153:         }
    // 给bucket每个桶地址赋值
     155:         buckets[i] = (ngx_hash_elt_t *) elts;
     156:         elts += test[i];
     157:  
     158:     }
    // 清空重新计算
    for (i = 0; i < size; i++) {
     161:         test[i] = 0;
     162:     }
     163:  
    for (n = 0; n < nelts; n++) {
    if (names[n].key.data == NULL) {
    continue;
     167:         }
     168:         
    //根据哈希值找到桶
     170:         key = names[n].key_hash % size;
     171:         
    //找到每个元素的地址
     173:         elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);
     174:     
    //给value和len赋值
     176:         elt->value = names[n].value;
     177:         elt->len = (u_short) names[n].key.len;
     178:         
    //拷贝name,name长度在前面计算size时已经算好了
     180:         ngx_strlow(elt->name, names[n].key.data, names[n].key.len);
     181:         
    //增加这个桶的索引
     183:         test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
     184:     }
     185:     
    // 设置每个桶的结束元素为NULL
    for (i = 0; i < size; i++) {
    if (buckets[i] == NULL) {
    continue;
     190:         }
     191:  
     192:         elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);
     193:  
     194:         elt->value = NULL;
     195:     }
     196:     
    //释放临时动态数组
     198:     ngx_free(test);
     199:  
    //给哈希表赋值
     201:     hinit->hash->buckets = buckets;
     202:     hinit->hash->size = size;
    return NGX_OK;
     204: }

     

    5.支持通配符哈希表ngx_hash_wildcard_init

     

      首先看一下ngx_hash_wildcard_init 的内存结构,当构造此类型的hash表的时候,实际上是构造了一个hash表的一个“链表”,是 通过hash表中的key“链接”起来的。比如:对于“*.abc.com”将会构造出2个hash表,第一个hash表中有一个key为com的表项, 该表项的value包含有指向第二个hash表的指针,而第二个hash表中有一个表项abc,该表项的value包含有指向*.abc.com对应的 value的指针。那么查询的时候,比如查询www.abc.com的时候,先查com,通过查com可以找到第二级的hash表,在第二级hash表中,再查找abc,依次类推,直到在某一级的hash表中查到的表项对应的value对应一个真正的值而非一个指向下一级hash表的指针的时候,查询过程结束。

     


     

    理解了这个,我们就可以看源代码了,ngx_hash_wildcard是一个递归函数,递归创建如上图的hash链表,如下为注释版源代码。

     

    精彩的读点有:

    • 由于指针都字节对齐了,低2位肯定为0,这种操作(name->value = (void *) ((uintptr_t) wdc | (dot ? 3 : 2)) ) 巧妙的使用了指针的低位携带额外信息,节省了内存,让人不得不佩服ngx设计者的想象力。
    1: /*hinit为初始化结构体指针,names为预加入哈希表数组,elts为预加入数组大小
       3: 变成了“com.abc.”。而“mail.xxx.*”则被预处理为“mail.xxx.”*/
       5: {
       6:     size_t                len, dot_len;
       7:     ngx_uint_t            i, n, dot;
       8:     ngx_array_t           curr_names, next_names;
       9:     ngx_hash_key_t       *name, *next_name;
      10:     ngx_hash_init_t       h;
      11:     ngx_hash_wildcard_t  *wdc;
      12:  
    //初始化临时动态数组curr_names,curr_names是存放当前关键字的数组
    if (ngx_array_init(&curr_names, hinit->temp_pool, nelts,
    sizeof(ngx_hash_key_t))
      16:         != NGX_OK)
      17:     {
    return NGX_ERROR;
      19:     }
      20:  
    //初始化临时动态数组next_names,next_names是存放关键字去掉后剩余关键字
    if (ngx_array_init(&next_names, hinit->temp_pool, nelts, sizeof(ngx_hash_key_t)) != NGX_OK)
      23:     {
    return NGX_ERROR;
      25:     }
      26:  
    //遍历 names 数组
    for (n = 0; n < nelts; n = i)
      29:     {
      30:         dot = 0;
      31:  
    //查找 dot
    for (len = 0; len < names[n].key.len; len++)
      34:         {
    if (names[n].key.data[len] == '.')
      36:             {
      37:                 dot = 1;
    break;
      39:             }
      40:         }
      41:  
    //将关键字dot以前的关键字放入curr_names
      43:         name = ngx_array_push(&curr_names);
    if (name == NULL) {
    return NGX_ERROR;
      46:         }
      47:  
      48:         name->key.len = len;
      49:         name->key.data = names[n].key.data;
      50:         name->key_hash = hinit->key(name->key.data, name->key.len);
      51:         name->value = names[n].value;
      52:  
      53:         dot_len = len + 1;
      54:  
    //len指向dot后剩余关键字
    if (dot)
      57:         {
      58:             len++;
      59:         }
      60:  
      61:         next_names.nelts = 0;
      62:  
    //如果names[n] dot后还有剩余关键字,将剩余关键字放入next_names中
    if (names[n].key.len != len)
      65:         {
      66:             next_name = ngx_array_push(&next_names);
    if (next_name == NULL) {
    return NGX_ERROR;
      69:             }
      70:  
      71:             next_name->key.len = names[n].key.len - len;
      72:             next_name->key.data = names[n].key.data + len;
      73:             next_name->key_hash = 0;
      74:             next_name->value = names[n].value;
      75:  
      76:         }
      77:  
    //如果上面搜索到的关键字没有dot,从n+1遍历names,将关键字比它长的全部放入next_name
    for (i = n + 1; i < nelts; i++)
      80:         {
    //前len个关键字相同
    if (ngx_strncmp(names[n].key.data, names[i].key.data, len) != 0) {
    break;
      84:             }
      85:  
      86:  
    if (!dot
      88:                 && names[i].key.len > len
    '.')
      90:             {
    break;
      92:             }
      93:  
      94:             next_name = ngx_array_push(&next_names);
    if (next_name == NULL) {
    return NGX_ERROR;
      97:             }
      98:  
      99:             next_name->key.len = names[i].key.len - dot_len;
     100:             next_name->key.data = names[i].key.data + dot_len;
     101:             next_name->key_hash = 0;
     102:             next_name->value = names[i].value;
     103:  
     104:         }
     105:  
    //如果next_name非空
    if (next_names.nelts)
     108:         {
     109:             h = *hinit;
     110:             h.hash = NULL;
     111:  
    //递归,创建一个新的哈西表
    if (ngx_hash_wildcard_init(&h, (ngx_hash_key_t *) next_names.elts,next_names.nelts) != NGX_OK)
     114:             {
    return NGX_ERROR;
     116:             }
     117:  
     118:             wdc = (ngx_hash_wildcard_t *) h.hash;
     119:             
    //如上图,将用户value值放入新的hash表
    if (names[n].key.len == len)
     122:             {
     123:                 wdc->value = names[n].value;
     124:             }
     125:             
    //并将当前value值指向新的hash表
    void *) ((uintptr_t) wdc | (dot ? 3 : 2));
     128:  
    else if (dot)
     130:         {
    void *) ((uintptr_t) name->value | 1);
     132:         }
     133:     }
     134:  
    //将最外层hash初始化
    if (ngx_hash_init(hinit, (ngx_hash_key_t *) curr_names.elts,curr_names.nelts) != NGX_OK)
     137:     {
    return NGX_ERROR;
     139:     }
     140:  
    return NGX_OK;
     142: }

     

posted @ 2016-07-24 23:12  godjob  Views(786)  Comments(0Edit  收藏  举报