nginx学习——从基本hash表到支持通配符的hash表(上)

原创 2016年08月28日 23:09:32
hash表示典型的空间换时间的数据结构,对元素进行查找、插入速度的时间复杂度为O(1),这种高效地方式非常适合频繁读取、插入、删除元素,在nginx服务器中,hash表也得到了广泛应用。在nginx基本hash表中,关键字一般是字符串(URL域名),但是如果要让hash表支持统配符,比如”*.test.com“,就需要用到nginx中特有的支持通配符的hash表。

一、基本hash表
hash表是可以根据元素的关键字直接进行访问的数据结构,它通过把hash值映射到表中的一个位置来访问记录,以加快查找速度。这个映射函数f就叫做hash函数,存放记录的数组就叫做hash表。但是对于不同的关键字,通过hash函数可能会得到相同的hash值,即key1 != key2 但是 f(key1) = f(key2),这就是hash冲突,nginx中的hash是通过开放地址法解决冲突的。因为在nginx中相同hash值的元素会被映射到一个桶中,这个桶可以存放很多个元素,假如之前某个桶已经存放过元素,下一次计算的hash值又映射到这个桶,那么就要找到这个桶中最后一个元素的结束地址,然后该地址开始继续向后存放,当然nginx还考虑到了内存对齐、指针对齐等等因素,这些我会在下面一一讲解。
(1)基本hash表的数据结构
在nginx的hash表中有一个ngx_hash_init_t的结构,该结构就记录了该hash表的属性信息比如hash表、hash表的映射函数、桶的大小、桶的最大个数、hash表名称等等。在最开始只有桶的大小和桶的最大个数是确定的,nginx中根据预先要加入到hash表中的元素ngx_hash_key_t(元素个数、元素关键字长度,元素值)来决定生成的hash表(包括真正需要的桶的个数)。
//hash表中的元素数据结构
typedef struct
{
    void              *value;//指向value值的指针
    u_short           len;//key的长度
    u_char            name[1];//key值,由于key不定长,这里使用柔性数组
} ngx_hash_elt_t;
//预添加到hash表中的元素结构
typedef struct
{
    ngx_str_t         key;//key字符串
    ngx_uint_t        key_hash; //由该key计算出的hash值
    void              *value;//指向value值的指针
} ngx_hash_key_t;
注意hash表中的元素ngx_hash_elt_t和后面预添加到hash表中的元素ngx_hash_key_t是一一对应的,但是两者的结构和作用不一样,ngx_hash_elt_t是多个聚集起来组成一个hash表便于后续的查找,ngx_hash_key_t是单个的key-value元素,nginx中的hash表结构(包括ngx_hash_elt_t)就是通过多个ngx_hash_key_t计算得来的。
//基本hash表结构
typedef struct
{
    ngx_hash_elt_t    **buckets; //指向hash桶(有size个桶)
    ngx_uint_t        size;//hash桶个数
} ngx_hash_t;
//hash初始化结构
typedef struct
{
    ngx_hash_t        *hash;//指向待初始化的hash表结构。
    ngx_hash_key_pt   key;//hash函数指针
    ngx_uint_t        max_size;//hash表中桶的最大个数
    ngx_uint_t        bucket_size;//hash表中一个桶的大小
    char              *name;//该hash结构的名称
    ngx_pool_t        *pool;//给该hash结构分配空间的内存池
    ngx_pool_t        *temp_pool;//分配临时数据空间的内存池
} ngx_hash_init_t;

(2)基本hash表的内存布局图



(3)一个关键的宏
在初始化基本hash表时用到了一个非常重要的宏:
#define NGX_HASH_ELT_SIZE(name) (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
这里的name是指向ngx_hash_key_t结构体的指针。我们对比ngx_hash_key_t和ngx_hash_elt_t的结构就会发现,ngx_hash_key_t比ngx_hash_elt_t多了一个hash值字段,这个hash值只是在确定该元素会被放到哪个桶才会用到,是不需要保存下来的。
//hash表中的元素数据结构
typedef struct
{
    void              *value;//指向value值的指针
    u_short           len;//key的长度
    u_char            name[1];//key值,由于key不定长,这里使用柔性数组
} ngx_hash_elt_t;

NGX_HASH_ELT_SIZE是该元素最终要放到hash表中会占用的空间,而hash表又是基于ngx_hash_elt_t结构,根据ngx_hash_elt_t的结构体可以得知,至少需要一个指向value值的指针(四个字节)、key长度字段(两个字节)、不定长的key字符串(长度由len决定)。
由该宏我们可以得知,如果长度字段和key值字段所需要的长度不是四的倍数,那么要以sizeof(void *)(四个字节)为基准进行内存对齐,虽然内存对齐会浪费一点存储空间,但是可以加快CPU的读取速度。

(4)基本hash表的初始化
hash表的初始化工作在函数ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)中。在进行初始化之前,hash初始化的结构体中只保存了hash表桶的最大个数以及hash表中桶的大小。在进行初始化工作的时候,根据传入的元素数组指针names,以及数组中元素的个数nelts来初始化hinit这个结构体。
总的流程即为:确保桶能够放入最大的元素->预估需要的桶数量->搜索真实需要的桶数量->计算每个桶的使用容量以及hash数据的总长度->为hash表的各个桶分配内存->将数组元素放入对应的桶中。

1、确保桶能够放入最大的元素
遍历传入的数组names,通过NGX_HASH_ELT_SIZE(&names[n])计算该数组中每一个元素的大小,由于桶中最后一定是以NULL结尾的,所以要预留sizeof(void *)(四个字节的空间。
//判断桶的大小是否够存放下names数组中最大的元素
for (n = 0; n < nelts; n++)
{
	if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))
	{
		//存在一个元素,桶的大小不够为该元素分配空间,则退出
		ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0, "could not build the %s, you should ", 
"increase %s_bucket_size: %i", hinit->name, hinit->name, hinit->bucket_size);
		return NGX_ERROR;
	}
}

2、预估需要的桶的数量
由于每个桶最终都要以NULL指针结尾,这里的指针占用四个字节,而每个桶的最大长度为hinit->bucket_size,也就是说每个桶真实能用的最大空间为:
bucket_size = hinit->bucket_size - sizeof(void *);
通过上面分析过的ngx_hash_elt_t结构体我们可以知道,当元素的key值字段为0长度时,元素占用最小的空间,大小为sizeof(void *)(两个字节) + sizeof(u_short)(两个字节) + 内存对齐(两个字节) = 八个字节。 
根据每个桶真实能用的最大容量和元素占用的最小空间,即可计算每个桶能够装下的最大元素个数,根据元素的总个数(传入数组长度),即可算出需要桶数目的下界。
start = nelts / (bucket_size / (2 * sizeof(void *)));
start = start ? start : 1;

3、搜索真实需要的桶数量size
根据上一步计算的桶数目的下界start,来确认所有的桶是否足够存放下所有属于这个桶的所有元素。因为桶的最大容量是一开始就确定下来的,桶的数量少那么最终分到各个桶中的元素会变多,如果某个桶的不足以存放下属于这个桶的元素,说明桶的个数太少,需要增加桶数量。在这里还使用了一个临时数组test,用来记录每个桶容量的使用情况。
for (size = start; size < hinit->max_size; size++)
{
	ngx_memzero(test, size * sizeof(u_short));
	//size个桶是否够分配hash数据
	for (n = 0; n < nelts; n++)
	{
		if (names[n].key.data == NULL)
		{
			continue;
		}
		//计算该元素属于哪一个桶
		key = names[n].key_hash % size; //若size=1,则key一直为0
		//
		test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));

		//若超过了桶的大小,则到下一个桶重新计算
		if (test[key] > (u_short) bucket_size)
		{
			goto next;
		}
	}

	goto found;

next:
	continue;
}

//若没有找到合适的bucket,退出
ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0, "could not build the %s, you should increase ",
 "either %s_max_size: %i or %s_bucket_size: %i", hinit->name, hinit->name, hinit->max_size,
 hinit->name, hinit->bucket_size);

ngx_free(test);

return NGX_ERROR;

found: //找到合适的bucket

4、计算每个桶的使用容量以及hash数据的总长度
在上一步我们找出了合适的桶数量size,我们就可以计算出每个桶应该装哪些元素,再根据元素的大小来确定该桶的使用容量,而hash数据总长度即为所有桶使用容量之和。
假设第一个桶中有八个元素,元素大小分别为:8、9、8、10、14、15、7、21。
第二个桶中有两个元素,元素大小分别为:13、12。
第三个桶中有三个元素,元素大小分别为:33、17、10。
第四个桶......
那么hash数据的总长度len = (8+9+8+10+14+15+7+21+4(空指针大小)) + (13+12+4(空指针大小)+3(内存对齐))
+(33+17+10+4(空指针大小))+......

//将test数组前size个元素初始化为sizeof(void *),之前说过每个桶最后一个元素必为空指针
for (i = 0; i < size; i++)
{
	test[i] = sizeof(void *);
}

//计算每个桶的容量使用情况
for (n = 0; n < nelts; n++)
{
	//如果元素key值为空
	if (names[n].key.data == NULL)
	{
		continue;
	}

	key = names[n].key_hash % size;
	//test[key]用来累加属于这个桶的元素长度
	test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
}

//hash数据的总长度
len = 0;
for (i = 0; i < size; i++)
{
	if (test[i] == sizeof(void *))
	{
		//若test[i]仍为初始化的值为sizeof(void *),即该桶中没有存放元素
		continue;
	}

	//对test[i]按ngx_cacheline_size对齐
	test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));
	len += test[i];
}

5、为hash表的各个桶分配内存
这里主要是分两步,一是为hash表中的桶指针数组分配空间,二是为hash表中所有的桶指针指向的桶分配空间(这里分配是一块连续空间)。
注意:存放数组元素的桶实际使用的总的空间是经过计算后才确定下来的,也是确定下来后才去申请的空间,而 hinit->bucket_size只是告诉你每个桶的最大容量,这个容量在初始化之前没有在内存中申请。
举个很简单的例子:比如有一个公司告诉你面试路费报销不能超过1000块(桶的最大容量),那个公司并不会直接把钱打给你(初始化前并没有申请内存),你可以采取坐火车、飞机等你喜欢的的方式(方法不一样,使用空间就不一样)去面试,如果坐飞机太贵,经过计算发现路费会超过1000(桶类元素所占空间超过该桶最大容量),那么只能改成做火车了(增加桶的数量,减少整体分配到每个桶的元素个数),但是最终公司给你报销的费用以发票为准(计算出实际使用空间并分配相应空间∩_∩)。

//如果hash表为空
if (hinit->hash == NULL)
{
	//在内存池中分配hash头以及桶指针数组分配内存空间
	hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t) + size * sizeof(ngx_hash_elt_t *));
	if (hinit->hash == NULL)
	{
		ngx_free(test);
		return NGX_ERROR;
	}

	//计算桶指针数组的起始位置
	buckets = (ngx_hash_elt_t **)((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));

}
//hash表已经存在
else
{
	//hash头已经存在,只用为桶指针数组分配空间
	buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *));
	if (buckets == NULL)
	{
		ngx_free(test);
		return NGX_ERROR;
	}
}

//接着分配所有桶空间(一块连续空间)
elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size);
if (elts == NULL)
{
	ngx_free(test);
	return NGX_ERROR;
}

// 对齐
elts = ngx_align_ptr(elts, ngx_cacheline_size);

//将各个桶指针与相应的桶空间对应起来
for (i = 0; i < size; i++)
{
	if (test[i] == sizeof(void *))
	{
		continue;
	}

	buckets[i] = (ngx_hash_elt_t *) elts;
	elts += test[i];
}

6、将数组元素放入对应的桶中
由于之前每个桶的真实大小都是已经计算好的,那么每个桶自然能放下所有属于这个桶的元素以及最后四个字节的空指针。在将数组元素放入桶时,首先根据元素key的hash值计算该元素是属于哪个桶,然后定位到桶中可用空间的起始地址(相当于该元素在桶内的偏移地址),然后将元素的信息拷贝到该地址,大小即为NGX_HASH_ELT_SIZE(&name[n])
假设第一个桶中有八个元素,key值分别为:ye、xin、is、best、veronica、yesterday、a、instantaneously。
第二个桶中有两个元素,key值分别为:waiting、result。(有内存对齐)
第三个桶中有三个元素,key值分别为:honorificabilitudinitatibus、forthcoming、like。
将元素放入对应的桶中后,如下图所示:

for (i = 0; i < size; i++)
{
	test[i] = 0;
}

//将传进来的每一个数据存入hash表相应的桶中
for (n = 0; n < nelts; n++)
{
	if (names[n].key.data == NULL)
	{
		continue;
	}

	key = names[n].key_hash % size;//计算该元素的hash值计算该元素属于哪一个桶
	elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);//桶地址+桶内偏移地址

	//对桶中ngx_hash_elt_t结构赋值
	elt->value = names[n].value;
	elt->len = (u_short) names[n].key.len;

	ngx_strlow(elt->name, names[n].key.data, names[n].key.len);

	test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));//计算第key个桶内下一个元素要存放时的偏移地址
}

for (i = 0; i < size; i++)
{
	if (buckets[i] == NULL)
	{
		continue;
	}

	elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);//定位到每个桶中最后一个元素的结束位置

	elt->value = NULL;//将每个桶空间的最后一个指针大小区域置NULL
}

(5)基本hash表的查找
基本hash表的查找很简单,总的来说就是根据要查找的元素的hash值定位到这个元素属于的桶中,然后和桶中的元素一个一个进行比较,直到到达桶的结尾(NULL)——这就是为什么桶空间需要用NULL结尾,就像字符串要以'\0'结尾。
//在hash表中查找元素
void * ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len)
{
    ngx_uint_t       i;
    ngx_hash_elt_t  *elt;

    //根据hash值计算所属桶的起始地址
    elt = hash->buckets[key % hash->size];

    //桶类为空
    if (elt == NULL)
    {
        return NULL;
    }

    //与桶类所有元素的key值进行比较
    while (elt->value)
    {
        if (len != (size_t) elt->len)
        {
            goto next;
        }

        for (i = 0; i < len; i++)
        {
            if (name[i] != elt->name[i])
            {
                goto next;
            }
        }

        //找到相应的key,返回相应value值
        return elt->value;

    next:
        //定位到桶中的下一个元素
        elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len, sizeof(void *));
        continue;
    }

    return NULL;
}

参考:
《深入理解nginx》
http://blog.csdn.net/chen19870707/article/details/40794285
http://blog.csdn.net/chen19870707/article/details/40739127
http://my.oschina.net/chenzhuo/blog/177866


版权声明:本文为博主原创文章,未经博主允许不得转载。

nginx通配符哈希表

nginx服务器的配置文件支持前置通配符或者后置通配符(例如: *.baidu.com,  www.sina.*), 不支持通配符在中间位置。在解析nginx.conf时,如果server_name配...
  • ApeLife
  • ApeLife
  • 2016年11月09日 22:05
  • 1338

nginx学习——从基本hash表到支持通配符的hash表(下)

在上一篇博文介绍了nginx中基本hash表的实现,今天主要是来介绍nginx是如何实现支持通配符的hash表。话说在看支持通配符的hash表源码时我惊奇地发现它的设计思路居然和我之前设计的中文字典树...

nginx配置location总结及rewrite规则写法

location正则写法 一个示例: location = / { # 精确匹配 / ,主机名后面不能带任何字符串 [ configuration A ] } location / ...

nginx 正则表达式匹配入门篇

1、nginx配置基础 1、正则表达式匹配 ~ 区分大小写匹配 ~* 不区分大小写匹配 !~和!~*分别为区分大小写不匹配及不区分大小写不匹配 ^ 以什么开头的匹配 $ 以什么结尾的匹配 ...

queue结构分析(二)ngx_queue 未完

nginx的队列结构是一个双链接指针队列,只包含队列指针。提供的办法只对队列指针操作,不负责队列元素的分配。...

nginx学习——从基本hash表到支持通配符的hash表(下)

在上一篇博文介绍了nginx中基本hash表的实现,今天主要是来介绍nginx是如何实现支持通配符的hash表。话说在看支持通配符的hash表源码时我惊奇地发现它的设计思路居然和我之前设计的中文字典树...

nginx的通配符哈希表--ngx_hash_wildcard_t

概述 nginx的哈希表的一个重要的应用场景是虚拟主机server name 的匹配,因此除了提供常规的哈希表匹配操作符,基于通配符的哈希表也就必不可少了 nginx基于通...

nginx 源码学习笔记(十)——基本容器——ngx_hash

ngx_hash.{c|h}实现了nginx里面比较重要的一个hash结构,这个在模块配置解析里经常被用到。该hash结构是只读的,仅在初始创建时可以给出保存在其中的key-val对儿,然后就只能进行...

nginx 源码学习笔记(十)——基本容器——ngx_hash

ngx_hash.{c|h}实现了nginx里面比较重要的一个hash结构,这个在模块配置解析里经常被用到。该hash结构是只读的,仅在初始创建时可以给出保存在其中的key-val对儿,然后就只能进行...

Nginx源代码分析--基本数据结构--hash

我们来看一下wildcard初始化函数。 //函数ngx_int_t //ngx_array_s结构体   //elts是指向内存池中的存储元素的指针。...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:nginx学习——从基本hash表到支持通配符的hash表(上)
举报原因:
原因补充:

(最多只允许输入30个字)