zoukankan      html  css  js  c++  java
  • 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: }
  • 相关阅读:
    架设WCF项目出现的问题
    百思不得其解的"Failed to allocate a managed memory buffer of 268435456 bytes."错误解决
    Ajax 分页
    关于Asp.net调用外部程序的拒绝访问错误
    转贴:[翻译]Visual Studio 2008 Code Metrics
    荀子,劝学篇(部分)
    .net设计模式(转载)
    人月神话读书笔记
    Memory food
    2010年4月12日,今天做计划
  • 原文地址:https://www.cnblogs.com/UnGeek/p/5701937.html
Copyright © 2011-2022 走看看