nginx中hash表的设计与实现 .

在nginx中使用的hash中一个非常核心的函数就是ngx_hash_init,由于nginx这个hash表是静态只读的,即不能在运行时动态添加新元素的,一切的结构和数据都在配置初始化的时候就已经规划完毕,所以“init”过程的优劣,对运行时查找的性能影响非常大。在正式分析之前,下面的这个连接给出了一个非常详细的hash结构的整体布局,对理解代码帮助会很大,一定要仔细看一下。


    先思考这样一个问题,假设让你来设计一个静态的hash表,对于一批确定数量的关键字,如何建立一个合理并且高效的hash表,让运行时的查找足够高效呢?你的hash表槽位多少合适?key冲突问题如何解决,用链式?链表长度该如何确定,太长效率低,那么多长合适?想想这些问题,然后我们看看nginx是如何去做的。

  1. ngx_int_t  
  2. ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)  
  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.       
  10.     /* nelts是关键字的数量,bucket_size为一个bucket的大小,这里注意的就是一个bucket至少可以容得下一个关键字, 
  11.      * 而下面的NGX_HASH_ELT_SIZE(&name[n] + sizeof(void *))正好就是一个关键字所占的空间。 
  12.      * 通过判断条件来看,如果我们设定的bucket大小,必须保证能容得下任何一个关键字,否则,就报错,提示bucket指定的太小。 
  13.      * 关于NGX_HASH_ELT_SIZE这个宏,这里提一下,nginx把所以定位到某个bucket的关键字,即冲突的,封装成ngx_hash_elt_t结构 
  14.      * 挨在一起放置,这样组成了一个ngx_hash_elt_t数组,这个数组空间的地址,由ngx_hash_t中的buckets保存。对于某个关键字来说, 
  15.      * 它有一个ngx_hash_elt_t的头结构和紧跟在后面的内容组成,从这个角度看一个关键字所占用的空间正好等于NGX_HASH_ELT_SIZE宏的值 
  16.      * 只是里面多了一个对齐的动作。 
  17.      */  
  18.     for (n = 0; n < nelts; n++) {  
  19.         /*  
  20.          * 这里考虑放置每个bucket最后的null指针所需要的空间,即代码中的sizeof(void *),这个NULL在find过程中作为一个bucket 
  21.          * 的结束标记来使用。 
  22.          */  
  23.         if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))  
  24.         {  
  25.             return NGX_ERROR;  
  26.         }  
  27.     }  
  28.       
  29.     /* max_size是bucket的最大数量, 这里的test是用来做探测用的,探测的目标是在当前bucket的数量下,冲突发生的是否频繁。 
  30.      * 过于频繁则说明当前的bucket数量过少,需要调整。那么如何判定冲突过于频繁呢?就是利用这个test数组,它总共有max_size个 
  31.      * 元素,即最大的bucket。每个元素会累计落到该位置关键字长度,当大于256个字节,即u_short所表示的最大大小时,则判定 
  32.      * bucket过少,引起了严重的冲突。后面会看到具体的处理。 
  33.      */  
  34.     test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);  
  35.     if (test == NULL) {  
  36.         return NGX_ERROR;  
  37.     }  
  38.       
  39.     /* 每个bucket的末尾一个null指针作为bucket的结束标志, 这里bucket_size是容纳实际数据大小,故减去一个指针大小 */  
  40.     bucket_size = hinit->bucket_size - sizeof(void *);  
  41.       
  42.     /*  
  43.      * 这里考虑NGX_HASH_ELT_SIZE中,由于对齐的缘故,一个关键字最少需要占用两个指针的大小。 
  44.      * 在这个前提下,来估计所需要的bucket最小数量,即考虑元素越小,从而一个bucket容纳的数量就越多, 
  45.      * 自然使用的bucket的数量就越少,但最少也得有一个。 
  46.      */  
  47.     start = nelts / (bucket_size / (2 * sizeof(void *)));  
  48.     start = start ? start : 1;  
  49.       
  50.     /*  
  51.      * 调整max_size,即bucket数量的最大值,依据是:bucket超过10000,且总的bucket数量与元素个数比值小于100 
  52.      * 那么bucket最大值减少1000,至于这几个判断值的由来,尚不清楚,经验值或者理论值。 
  53.      */  
  54.     if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {  
  55.         start = hinit->max_size - 1000;  
  56.     }  
  57.       
  58.     /* 在之前确定的最小bucket个数的基础上,开始探测(通过test数组)并根据需要适当扩充,前面有分析其原理 */  
  59.     for (size = start; size < hinit->max_size; size++) {  
  60.   
  61.         ngx_memzero(test, size * sizeof(u_short));  
  62.   
  63.         for (n = 0; n < nelts; n++) {  
  64.             if (names[n].key.data == NULL) {  
  65.                 continue;  
  66.             }  
  67.   
  68.             key = names[n].key_hash % size;  
  69.             test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));  
  70.   
  71.             if (test[key] > (u_short) bucket_size) {  
  72.                 goto next;  
  73.             }  
  74.         }  
  75.   
  76.         goto found;  
  77.           
  78.     next:  
  79.         /* 到next这里,就是实际处理bucket扩充的情况了,即递增表示bucket数量的size变量 */  
  80.         continue;  
  81.     }  
  82.   
  83.     ngx_free(test);  
  84.   
  85.     return NGX_ERROR;  
  86.   
  87. found:  
  88.     /* 确定了合适的bucket数量,即size。 重新初始化test数组,初始值为一个指针大小。*/  
  89.     for (i = 0; i < size; i++) {  
  90.         test[i] = sizeof(void *);  
  91.     }  
  92.       
  93.     /* 统计各个bucket中的关键字所占的空间,这里要提示一点,test[i]中除了基本的数据大小外,还有一个指针的大小 
  94.      * 如上面的那个for循环所示。 
  95.      */  
  96.     for (n = 0; n < nelts; n++) {  
  97.         if (names[n].key.data == NULL) {  
  98.             continue;  
  99.         }  
  100.   
  101.         key = names[n].key_hash % size;  
  102.         test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));  
  103.     }  
  104.   
  105.     len = 0;  
  106.       
  107.     /* 调整成对齐到cacheline的大小,并记录所有元素的总长度 */  
  108.     for (i = 0; i < size; i++) {  
  109.         if (test[i] == sizeof(void *)) {  
  110.             continue;  
  111.         }  
  112.   
  113.         test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));  
  114.   
  115.         len += test[i];  
  116.     }  
  117.       
  118.     /*  
  119.      * 申请bucket元素所占的空间,这里注意的一点就是,如果之前hash表头结构没有申请, 
  120.      * 那么在申请时将ngx_hash_wildcard_t结构也一起申请了。 
  121.      */  
  122.     if (hinit->hash == NULL) {  
  123.         hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t)  
  124.                                              + size * sizeof(ngx_hash_elt_t *));  
  125.         if (hinit->hash == NULL) {  
  126.             ngx_free(test);  
  127.             return NGX_ERROR;  
  128.         }  
  129.   
  130.         buckets = (ngx_hash_elt_t **)  
  131.                       ((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));  
  132.   
  133.     } else {  
  134.         buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *));  
  135.         if (buckets == NULL) {  
  136.             ngx_free(test);  
  137.             return NGX_ERROR;  
  138.         }  
  139.     }  
  140.   
  141.     elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size);  
  142.     if (elts == NULL) {  
  143.         ngx_free(test);  
  144.         return NGX_ERROR;  
  145.     }  
  146.   
  147.     elts = ngx_align_ptr(elts, ngx_cacheline_size);  
  148.       
  149.     /* 设置各个bucket中包含实际数据的空间的地址(或者说位置) */  
  150.     for (i = 0; i < size; i++) {  
  151.         if (test[i] == sizeof(void *)) {  
  152.             continue;  
  153.         }  
  154.   
  155.         buckets[i] = (ngx_hash_elt_t *) elts;  
  156.         elts += test[i];  
  157.   
  158.     }  
  159.       
  160.     /* 用来累计真实数据的长度,不计结尾指针的长度 */  
  161.     for (i = 0; i < size; i++) {  
  162.         test[i] = 0;  
  163.     }  
  164.       
  165.     /* 依次向各个bucket中填充实际的内容,代码没什么好分析的。*/  
  166.     for (n = 0; n < nelts; n++) {  
  167.         if (names[n].key.data == NULL) {  
  168.             continue;  
  169.         }  
  170.   
  171.         key = names[n].key_hash % size;  
  172.         elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);  
  173.   
  174.         elt->value = names[n].value;  
  175.         elt->len = (u_short) names[n].key.len;  
  176.   
  177.         ngx_strlow(elt->name, names[n].key.data, names[n].key.len);  
  178.         /* test[key]记录当前bucket内容的填充位置,即下次填充的开始位置 */  
  179.         test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));  
  180.     }  
  181.       
  182.     /* 设置bucket结束位置的null指针,*/  
  183.     for (i = 0; i < size; i++) {  
  184.         if (buckets[i] == NULL) {  
  185.             continue;  
  186.         }  
  187.         /*  
  188.          * 由于前面bucket的处理中多留出了一个指针的空间,而此时的test[i]是bucket中实际数据的共长度, 
  189.          * 所以bucket[i] + test[i]正好指向了末尾null指针所在的位置。处理的时候,把它当成一个ngx_hash_elt_t结构看, 
  190.          * 在该结构中的第一个元素,正好是一个void指针,我们只处理它,别的都不去碰,所以没有越界的问题。 
  191.          */  
  192.         elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);  
  193.   
  194.         elt->value = NULL;  
  195.     }  
  196.   
  197.     ngx_free(test);  
  198.   
  199.     hinit->hash->buckets = buckets;  
  200.     hinit->hash->size = size;  
  201.   
  202.     return NGX_OK;  
  203. }  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值