Nginx 源码分析:ngx_hash_t(上)

源文件路径

版本:1.8.0node

csrc\core\Ngx_hash.h
src\core\Ngx_hash.c

关于hash表

Nginx实现的hash表和常见的hash表大致一致,细节有区别,因此,要了解ngx_hash_t最好对hash表的基础概念进行一下梳理。算法

数组与hash表

从查询的角度来看,数组根据索引值的查询速度很快快。
缘由在于数组内元素的位置是基于数组起始位置的绝对位置,并且数组的存储空间是连续的,能够根据下标直接操做指针跳转。segmentfault

虽然数组的查询速度很快,可是数组的索引值必须是数值,这就很讨厌了。
由于不少状况下,索引值并非数字,而是字符串什么的。好比用名字来索引一我的。数组

解决这个问题的一个很容易的办法就是给每一个人安排一个学号(先不考虑重名的状况),那么,在实际存储时,按照学号为索引值的数组来存储对应的信息;在查询时,只须要知道名字,就能够获得名字对应的学号,根据学号能够直接从数组中取出信息数据结构

这个解决方法中有两个主要部分:框架

  1. 创建从名字到学号的对应关系;
  2. 创建以学号为索引值的数组;

从名字到学号的对应关系能够抽象成从字符串到数值的对应关系,这种对应关系,在数学上表示就是f(k)。其中k表示一个字符串(索引关键字),函数f表示从字符串到数值的对应关系,f(k)表示k通过f映射获得的值。函数

只要有了f(k),那么将f(k)做为数组的下标便可获取k所对应的信息。
性能

k------>f(k)------->info[f(k)]

其中,从kf(k)的映射函数称为哈希函数,数组info[]称为哈希(hash)表优化

hash表的问题及解决方法

理想是丰满的,现实是骨感的。hash表在创建时最关键之处在于找到合适的哈希函数,使得:ui

  1. kf(k)之间是一一映射的。即,保证给定对于k存在惟一的f(k)与之对应,同时对于f(k)存在惟一的k与之对应。
  2. f(k)的集合是连续的。即,对于数组info[]而言,不存在数组项为空的状况,能够更加充分利用资源。

惋惜,知足上述条件的哈希函数很是困难。
如今使用的各类哈希函数基本上只能保证较小几率出现两个不一样的kf(k)相同的状况
基本不能保证f(k)的集合是连续的

由于f(k)的集合不是连续的,因此哈希表也被称为散列表,哈希函数也被称为散列函数。
而出现两个k值对应的f(k)相同的状况,称为哈希冲突。

解决哈希冲突常见的办法
出现散列状况表示可能浪费一点资源,这是能够接受的。可是出现冲突表示会发生信息覆盖,这是错误,不能接受。因此,必须解决哈希冲突。

解决哈希冲突的常见的方法有:
1) 开放地址法;2)再哈希法;3)链地址法;

具体内容请自行google,这里就不去挖老坟了。

哈希表的创建

从上述的分析可知,创建哈希表有两个主要环节:

1)创建哈希函数;
2)创建哈希表(都是窟窿的数组)

其中,为了解决哈希冲突(假设采用链地址法),所创建的哈希表(数组)里的元素多是一个链表或者一个数组。也就是说,哈希表是一个二维的结构。
同时,对于索引关键字,要求哈希函数得到的哈希值控制在必定范围内。

所以,哈希表大概长成这个样子:

ctypedef struct node_s{
     char    *key;
     char    *val;
     node_t  *next;
}node_t;

#define HASHSIZE 101
node_t* hashtable[HASHSIZE];

其中hashtable表示哈希表,key表示索引值,好比上述例子中某个学生的名字,node_t表示哈希表中存储的信息,同时也能够看到node_t是链表的一个节点,用于解决哈希冲突。

假设key的值是字符串"xiaoming",根据某个哈希函数,得出的值为6,那么"xiaoming"的信息就能够从hashtable[6]链表中取得,这样再去遍历hashtable[6]这个链表,找到key等于"xiaoming"的链表节点,其val就是要查找的值。

从上述分析,可知,hash表是一种拿空间换取时间的数据结构。
关于hash表的各类实现方法及算法的算法复杂度,请自行google。


Nginx中的哈希表

须要指出的是,Nginx中自造的哈希表属于内部使用的数据结构,所以,并非一个通用的哈希表。此外,为了提升效率,做者作了至关多的优化,这些优化使得Nginx中的哈希表与常规的哈希表长得不同。

例如,Nginx的哈希表一经初始化就不可更改,既不能增长元素,也不能删除元素。
这样作主要是由于Nginx的哈希表用于解决相似于http模块中域名匹配的问题,这些域名在配置文件中配置,一旦读取配置文件,这些信息是不可修改的,所以,没有增删的需求。

另外,因为Nginx哈希表的这种只读特色,使得能够在性能上有很大的可优化空间。
Nginx也确实在这上面做了不少文章。

数据结构

根据哈希表的概念可知:哈希表自己就是一个数组,所以,是一块连续的内存空间
Nginx中,内存的管理都是经过ngx_pool_t来管理的(不清楚的请移步这里),所以,须要一个用来管理这块连续内存的结构体。

可是因为哈希表为了解决冲突问题,一般采用链地址法,因此,这个管理内存的结构体会使用指针的指针

另外,因为Nginx的哈希表是只读的,冲突的元素个数能够在初始化是肯定,因此使用数组来代替链表解决冲突是更优的选择。

这个用来代替链表的数组还有个名字叫hash桶,因此,会在Nginx源码中看到buckets这样的命名。

Nginx的哈希表在内存上大概是长这个样子的:
图片描述

假设理想状况,全部的索引值key通过哈希函数映射后f(k)集合的大小为4

为了解决冲突,咱们将每一个f(k)对应的数组大小设定为2。这样,咱们的hash表在逻辑上就变成了一个4x2的数组。

固然,为了更好的说明状况,这里假设哈希函数是理想的,所以,hash表不存在未使用的部分

因此,在内存上,Nginx哈希表的本尊,就是一段连续的内存空间,此外,还须要两个用来管理这段内存空间的数据结构。

1)大小为4的数组,类型为ngx_hash_elt_t *,用来分别指向不一样的内存段,表示每一个hash桶
2)类型为ngx_hash_elt_t **的指针buckets,用来表示hash桶数组

因为指针的指针能够完整的表示二维数组,所以,ngx_hash_elt_t *数组并不须要定义。只需定义ngx_hash_elt_t来表示hash表中的每一个元素便可。

所以,Nginx哈希表的核心数据结构以下:

ngx_hash_elt_t用来表示hash表的元素。

ctypedef struct {
    void             *value;
    u_short           len;
    u_char            name[1];
} ngx_hash_elt_t;

ngx_hash_t用来表示整个hash表。

ctypedef struct {
    ngx_hash_elt_t  **buckets;
    ngx_uint_t        size;
} ngx_hash_t;

经过buckets这个指针的指针能够完整的访问二维数组。

Nginx中是如何使用这两个数据结构的呢?或者简化一下,Nginx是如何初始化这两个数据结构的呢?

首先,做为管理内存的结构体,ngx_hash_t既能够做为局部变量在栈上出现,也能够做为堆上的变量,使用ngx_pool_t管理。

以堆为例,

ngx_hash_t  *hash;
// 向ngx_pool_t申请空间,用于存放管理结构体ngx_hash_t及4个 ngx_hash_elt_t指针
hash = ngx_pcalloc(pool, sizeof(ngx_hash_t) + 4*sizeof(ngx_hash_elt_t *));

u_char *elts;
// 向ngx_pool_t申请hash表自己使用的连续内存块
elts = ngx_palloc(pool, 4 * 2 * sizeof(ngx_hash_elt_t));

ngx_hash_elt_t **buckets;
// 将管理结构体成员变量赋于正确的值。
for (i = 0; i < 4; i++) {
    buckets[i] = (ngx_hash_elt_t *) elts;  // 4个ngx_hash_elt_t指针指向正确地址;
    elts += 2 * sizeof(ngx_hash_elt_t);
}
hash->buckets = buckets;
hash->size = 4;

这段代码,在内存池中申请了一段连续的内存,分别用于1ngx_hash_t4ngx_hash_elt_t *

这样就把管理hash表那段连续内存块使用的ngx_hash_elt_t** bucketsngx_hash_elt_t*数组一块儿建立了。

而后依次给每一个ngx_hash_elt_t *赋值,使其指向正确的内存地址。


说明
以上代码自Nginx源码中简化而来,去除了许多用于优化的代码。

因为ngx_hash_t内容较多,这里只从设计角度分析了Nginx中的hash表。主要目的在于理清总体框架及思路。

细节部分,后续添加。先到这里。

相关文章
相关标签/搜索