菜鸟nginx源码剖析数据结构篇(六) 哈希表 ngx_hash_t(上)
-
Author:Echo Chen(陈斌)
-
Email:chenb19870707@gmail.com
-
Date:October 31h, 2014
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 、value为value,这里看到name仅为一个字节的uchar数组,仅用于指出key的首地址,而key的长度是可变的,所以哈希表元素的大小并不是由sizeof(ngx_hash_elt_t_t)决定的,而是在初始化时指定的。
- value是指向用户自定义数据类型的指针,如果hash表中这个位置没有元素,则value = NULL
- len 表示关键字key的长度,关键字的长度是不定的
- name 为key的首地址
1: typedef struct {
2: void *value; //指向用户自定义元素的指针,如果该位置没有元素,即为NULL
3: u_short len; //key的长度
4: u_char name[1]; //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;
3: 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 {
2: ngx_hash_t *hash; //指向普通的完全匹配哈希表
3: ngx_hash_key_pt key; //哈希方法
4:
5: ngx_uint_t max_size; //哈希表中槽的最大个数
6: ngx_uint_t bucket_size; //哈希表中一个槽的空间大小,不是sizeof(ngx_hash_elt_t)
7:
8: char *name; //哈希表的名称
9: ngx_pool_t *pool; //内存池,它负责分配基本哈希列表、前置通配哈希列表、后置哈希列表中所有槽
10: ngx_pool_t *temp_pool; //临时内存池,它仅存在初始化哈希表之前。用于分配一些临时的动态数组,带通配符的元素初始化时需要用到临时动态数组
11: } ngx_hash_init_t;
3.6 预添加哈希散列元素结构 ngx_hash_key_t
ngx_hash_key_t用于表示即将添加到哈希表中的元素,其结构如下:
1: typedef struct {
2: ngx_str_t key; //元素关键字
3: ngx_uint_t key_hash; //由哈希方法算出来的哈希值
4: 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 {
2: ngx_uint_t hsize; //散列中槽总数
3:
4: ngx_pool_t *pool; //内存池,用于分配永久性的内存
5: ngx_pool_t *temp_pool; //临时内存池,下面的临时动态数组都是由临时内存池分配
6:
7: ngx_array_t keys; //存放所有非通配符key的数组。
8: ngx_array_t *keys_hash; //这是个二维数组,第一个维度代表的是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:
10: ngx_array_t dns_wc_head; //存放前向通配符key被处理完成以后的值。比如:“*.abc.com”被处理完成以后,变成“com.abc.”被存放在此数组中。
11: ngx_array_t *dns_wc_head_hash; //该值在调用的过程中用来保存和检测是否有冲突的前向通配符的key值,也就是是否有重复。
12:
13: ngx_array_t dns_wc_tail; //存放后向通配符key被处理完成以后的值。比如:“mail.xxx.*”被处理完成以后,变成“mail.xxx.”被存放在此数组中。
14: ngx_array_t *dns_wc_tail_hash; //该值在调用的过程中用来保存和检测是否有冲突的后向通配符的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 {
2: void *value;
3: u_short len;
4: u_char name[1];
5: } ngx_hash_elt_t;
6:
7: #define NGX_HASH_ELT_SIZE(name) \
8: (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
如下是注释版源代码,比较长,总的流程即为:预估需要的桶数量 –> 搜索需要的桶数量->分配桶内存->初始化每一个ngx_hash_elt_t
-
1: //hinit是哈希表初始化结构指针,names是预添加到哈希表结构的数组,nelts为names元素个数
2: ngx_int_t 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;