hash也是一种数据结构,相比于list、queue等要复杂一些。在利用hash表存储数据时,都是通过key-value的形式,通过对key进行hash得到一个key-hash值,然后利用该值找到数据应该在hash表中存放的位置,再插入数据。
1、基本数据结构
实现hash表涉及的主要数据结构如下:
typedef struct { //hash表 ngx_hash_elt_t **buckets; //指向hash桶的二级指针 ngx_uint_t size; //桶的个数 } ngx_hash_t;hash表中的每一个元素的数据结构如下:
typedef struct { void *value; //实际数据 u_short len; //key的长度 u_char name[1]; //key值,即等待hash的数据 } ngx_hash_elt_t;nginx用 ngx_hash_init_t结构来初始化hash表,其数据结构如下:
typedef struct { ngx_hash_t *hash; //指向hash表 ngx_hash_key_pt key; //函数指针 ngx_uint_t max_size; //hash桶的最大个数 ngx_uint_t bucket_size; //hash桶的最大空间 char *name; ngx_pool_t *pool; ngx_pool_t *temp_pool; } ngx_hash_init_t;将上述数据结构联系在一起,一个简单的示意图如下:
nginx通过ngx_hash_key_t来保存要hash的数据,在初始化前就已经准备好了,其数据结构如下:
typedef struct { ngx_str_t key; //等待hash的key值 ngx_uint_t key_hash; //对key值hash后得到的一个整数 void *value; //实际待存储的数据 } ngx_hash_key_t;其中key使用了nginx中定义的字符串的存储方式,通过一个长度值和一个指针来表示一个字符串,而不再是以前那种以'\0'结尾的字符串。其定义如下
typedef struct { size_t len; //字符串的长度 u_char *data; //指向字符串的首地址 } ngx_str_t;上述结构的简单示意图如下:
2、hash的初始化
nginx通过函数ngx_int_t
ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts);来完成hash表的初始化。
其中hinit结构为待初始化的hash表结构,names为等待hash的数据,一般为数组形式的,nelts为等待hash的数据的个数,即names数组的元素个数。
1) 首先判断hash表的桶空间是否足够存储待处理的数据,即每个bucket指向的ngx_hash_elt_t空间是否足够存储待处理的数据
for (n = 0; n < nelts; n++) { if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *)) { return NGX_ERROR; } }其中hinit->bucket_size为每个桶空间的大小。
NGX_HASH_ELT_SIZE宏定义了获取待存储的元素大小的方法,其定义如下:
#define NGX_HASH_ELT_SIZE(name) \ (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))sizeof(void *):指向value的指针
ngx_align((name)->key.len:待hash的key值的长度
2:len,即用来指示key的长度,其类型为u_short,2字节
这三个部分分别对应ngx_hash_elt_t结构体中的value、name、len的长度,后半部分的sizeof(void *)表示是4字节对齐的(32位时)。
只有当桶空间足够存储每个待存储的数据时,才会继续初始化。
2) 确定存放所有数据需要的桶的个数
由于hash是通过将key_hash值对一个整数求余来将各个数据分散到不同的桶中的,因此一个桶中可能存储多个数据,因此必须确定合适的桶的个数,来保证将数据分散后仍能将数据存放到对应的桶中。通过一个临时变量test来记录每个桶需要的内存大小,来确定最终需要的桶的个数。
test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log); //2*max_size if (test == NULL) { return NGX_ERROR; }test为一个数组,大小为max_size,即桶的个数。其元素类型为u_short,2个字节,用于记录初始化过程中每个桶用到的长度信息,故申请2*max_size大小的内存空间。且由于test只是临时用到,因此没有在内存池中分配内存,而是直接通过malloc申请内存,待函数执行完后,直接free。
粗略计算最小的桶的个数:
在一个hash表中,所有元素的内存都是在同一块内存块上分配的,不同的桶之间通过一个NULL指针分隔开来,因此首先要为每个桶空间预留一个sizeof(NULL)的长度
bucket_size = hinit->bucket_size - sizeof(void *); start = nelts / (bucket_size / (2 * sizeof(void *))); start = start ? start : 1;一个元素最少需要 NGX_HASH_ELT_SIZE(&names[n]) > (2 * sizeof(void *))的空间(sizeof(void *)字节对齐),因此bucket_size大小的桶最多能存放bucket_size / (2 * sizeof(void *)))个元素,nelts个元素则最少需要start个桶。
利用key_hash和size将数据分散到各个桶中,来确定桶的个数
for (size = start; size <= hinit->max_size; size++) { ngx_memzero(test, size * sizeof(u_short)); for (n = 0; n < nelts; n++) { if (names[n].key.data == NULL) { continue; } key = names[n].key_hash % size; //确定数据对应的桶 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; }nginx中的hash表采用求余的方法来确定数据对应的hash桶,上述代码表明,nginx是将key_hash对桶的个数求余的,其中size表示了桶的个数。
key为求余后对应的桶的索引,test[key]则记录了该桶所需的大小。
当size=1时,求余后得到的key均为0.则所有数据均会集中到一个桶bucket[0]中。
当test[key]>bucket_size时,表明数据过于集中,有的桶不足以存放如此多的数据。因此跳转到next,增大size的大小,即增加桶的个数,重新分散数据,以保证每个桶都足够存放相应的数据。
若每个桶的大小都足以存放对应数据时,跳转到found,则此时size表示了最终所需的桶的个数。
3) 确定了桶的个数后,进一步详细确定每个桶待存放数据的大小
for (i = 0; i < size; i++) { test[i] = sizeof(void *); //在每个桶的尾部用一个NULL指针,用于分隔相邻的桶 } for (n = 0; n < nelts; n++) { if (names[n].key.data == NULL) { continue; } key = names[n].key_hash % size; test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n])); } len = 0; for (i = 0; i < size; i++) { if (test[i] == sizeof(void *)) { continue; } test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size)); len += test[i]; }此时test数组中的每个元素记录了对应的桶待存放数据的大小(字节对齐后),len为所需的总大小。
if (hinit->hash == NULL) { 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)); } else { buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *)); if (buckets == NULL) { ngx_free(test); return NGX_ERROR; } }可以发现桶的个数为size,大小为sizeof(ngx_hash_elt_t *);
由上可知,len为存储所有数据所需的空间大小
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);6)然后根据每个桶中待处理数据的大小来确定每个桶的起始位置
for (i = 0; i < size; i++) { if (test[i] == sizeof(void *)) { //表示该桶没有数据 continue; } buckets[i] = (ngx_hash_elt_t *) elts; //记录每个桶的起始位置 elts += test[i]; }得到结果如下所示:
7) 将数据放到对应的桶中
for (i = 0; i < size; i++) { test[i] = 0; //将test中统计的信息清零 } for (n = 0; n < nelts; n++) { if (names[n].key.data == NULL) { continue; } key = names[n].key_hash % size; //确定数据对应的桶索引 elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]); //确定数据的起始位置 elt->value = names[n].value; //将数据存入hash表中 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])); } 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; }8) 最后释放资源并设置某些字段的值
ngx_free(test); //释放临时变量test hinit->hash->buckets = buckets; //将hinit结构中的buckets指针指向已初始化好的buckets hinit->hash->size = size; //设置hash表中桶的个数为size这样就完成了hash表的初始化