根据以往的学习经验, 比如STL中的哈希表, 利用开链法, vector+list作为容器, 当hashtable中的元素总数超过一定数量时, 选择扩充vector.
再比如libevent中的哈希表, 与STL中的哈希表类似, 但比较复杂, 每个bucket中都可能有一个链表,每个链表元素中也可能存在一个链表. 但理解起来都并不复杂.
现在看的Nginx中的哈希表, 则与上面谈到的哈希有很明显的不同之处.
在nginx中, 存储server_name和ngx_http_core_srv_conf_t的映射时用到了hash结构.
配置server_names_hash_max_size可以控制bucket的最大数量,server_names_hash_bucket_size可以控制每个bucket的大小
下图, 就是nginx存储不含通配符的server_name时使用的hash结构:

同时, 每个bucket的大小必须能保证存放至少一个元素.无论这个元素的大小是多少.
好了, 大致的介绍就到这里. 下面是详细的代码分析.
一、 数据结构的定义
1、基本哈希表的元素
每个元素都是 key-value的形式
2、基本哈希表结构
3、支持通配符的哈希表结构
其实也就是多了一个额外的value指针, 当使用ngx_hash_wildcard_t通配符哈希表作为容器元素时,可以使用value指向用户数据。
这里暂时不做多解释
4、该结构也主要用来保存要hash的数据,即键-值对<key,value>
在实际使用中,一般将多个键-值对保存在ngx_hash_key_t结构的数组中,作为参数传给ngx_hash_init()或ngx_hash_wildcard_init()函数
用于表示即将添加到哈希表中的元素
下面这张图来自:http://blog.youkuaiyun.com/chen19870707/article/details/40794285 (若有侵犯, 立刻删除)

二、基本哈希表的初始化
在分析初始化哈希表的代码之前, 还要提一件事. 就是每个bucket中每个元素的大小. 既然它使用了1长度数组的策略, 那么求其占用内存的大小就不再是单纯的使用sizeof了
先加上一个void指针(指向value)的大小, 在64位机器上即为8字节. 然后再加上key的大小, key的大小使用len来表示了, 再加上short类型的2字节, 但是单纯的相加显然不符合内存地址对齐的规则.
比如我们定义了下面这个结构体:
所以这个宏就顺利的求出了某元素占用的内存大小的值.
下面就是哈希表初始化的详细代码以及解析
三、基本哈希表的查找.
最后, 贴上借鉴的几篇博客:
http://blog.youkuaiyun.com/livelylittlefish/article/details/6636229
http://blog.youkuaiyun.com/chen19870707/article/details/40794285
http://www.linuxidc.com/Linux/2012-08/67040.htm
http://blog.chinaunix.net/uid-27767798-id-3766755.html
感谢!
再比如libevent中的哈希表, 与STL中的哈希表类似, 但比较复杂, 每个bucket中都可能有一个链表,每个链表元素中也可能存在一个链表. 但理解起来都并不复杂.
现在看的Nginx中的哈希表, 则与上面谈到的哈希有很明显的不同之处.
在nginx中, 存储server_name和ngx_http_core_srv_conf_t的映射时用到了hash结构.
配置server_names_hash_max_size可以控制bucket的最大数量,server_names_hash_bucket_size可以控制每个bucket的大小
下图, 就是nginx存储不含通配符的server_name时使用的hash结构:
进行解释之前, 需要提一下, nginx中的哈希表一个特别之处在于, 这个hash表是静态只读的,即不能在运行时动态添加新元素的,一切的结构和数据都在配置初始化的时候就已经规划完毕,所以“init”过程的优劣,对运行时查找的性能影响非常大
我们假设哈希表的bucket数量为size , 每个元素的哈希值为 [hash%size], 从上图可以看到, 此哈希表解决冲突的办法类似开链, 但又并不是使用链表来存储具有相同 [hash%size] 值的元素, 而是令具有相同[hash%size]值的元素存放在一块连续的内存中, 以一个NULL指针作为结尾. 上面我们说到了两个可以控制bucket的最大数量和每个bucket的大小的配置, 但并没有说这个哈希表的bucket数量到底是多少. 试想, 当前我们的bucket大小是确定的, 假如冲突太多, 即有多个元素被存放在同一个bucket, 这样肯定会导致某bucket"超载". 那么解决的办法就是扩大bucket的数量, 即size的值(这样不仅能使原来具有相同[hash%size]的元素不再相同, 还能使冲突发生的更稀疏). 看看能不能保证所有存放多个相同 [hash%size]的元素的bucket不"超载". 但是既然我们设定了bucket的最大数量, 一旦size达到这个值还没能满足我们的要求, 那么就返回错误.同时, 每个bucket的大小必须能保证存放至少一个元素.无论这个元素的大小是多少.
好了, 大致的介绍就到这里. 下面是详细的代码分析.
一、 数据结构的定义
1、基本哈希表的元素
每个元素都是 key-value的形式
typedef struct {
void *value; //即为key-value中对应的value
u_short len; //为key-value中key的长度
u_char name[1]; //为key的首地址. 使用长度为1的数组是为了将来申请的len大小的空间是连续的(详细的请搜索 "0或1长度数组")
} ngx_hash_elt_t;
2、基本哈希表结构
typedef struct {
ngx_hash_elt_t **buckets; //即为哈希表
ngx_uint_t size; //哈希表中bucket的个数
} ngx_hash_t;
3、支持通配符的哈希表结构
其实也就是多了一个额外的value指针, 当使用ngx_hash_wildcard_t通配符哈希表作为容器元素时,可以使用value指向用户数据。
这里暂时不做多解释
typedef struct {
ngx_hash_t hash;
void *value;
} ngx_hash_wildcard_t;
4、该结构也主要用来保存要hash的数据,即键-值对<key,value>
在实际使用中,一般将多个键-值对保存在ngx_hash_key_t结构的数组中,作为参数传给ngx_hash_init()或ngx_hash_wildcard_init()函数
用于表示即将添加到哈希表中的元素
typedef struct {
ngx_str_t key;
ngx_uint_t key_hash; //由哈希函数根据key计算出的值. 将来此元素代表的结构体会被插入bucket[key_hash % size]
void *value;
} ngx_hash_key_t;
5、哈希表初始化用的结构体
typedef struct {
ngx_hash_t *hash;
ngx_hash_key_pt key; //即哈希函数
ngx_uint_t max_size; //bucket的最大数量
ngx_uint_t bucket_size; //每个bucket的容量
char *name; //log用
ngx_pool_t *pool; //是在pool中进行内存处理的
ngx_pool_t *temp_pool;
} ngx_hash_init_t;
上面的5个结构体之间的关系必须搞清楚, 所以下面提前附上一张图. 如果看不懂图, 也没关系, 总之在上面的结构体之间关系有个印象后, 继续下面的源代码分析.下面这张图来自:http://blog.youkuaiyun.com/chen19870707/article/details/40794285 (若有侵犯, 立刻删除)
二、基本哈希表的初始化
在分析初始化哈希表的代码之前, 还要提一件事. 就是每个bucket中每个元素的大小. 既然它使用了1长度数组的策略, 那么求其占用内存的大小就不再是单纯的使用sizeof了
typedef struct {
void *value; //即为key-value中对应的value
u_short len; //为key-value中key的长度
u_char name[1]; //为key的首地址. 使用长度为1的数组是为了将来申请的len大小的空间是连续的(详细的请搜索 "0或1长度数组")
} ngx_hash_elt_t;
于是有了下面这个宏:#define NGX_HASH_ELT_SIZE(name) \
(sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))
该宏求大小的过程是:先加上一个void指针(指向value)的大小, 在64位机器上即为8字节. 然后再加上key的大小, key的大小使用len来表示了, 再加上short类型的2字节, 但是单纯的相加显然不符合内存地址对齐的规则.
比如我们定义了下面这个结构体:
struct test{
int x;
char y;
};
那么sizeof(struct test)的大小在编译器内存地址对齐处理过后结果就是8了. 但是这里是我们自己管理内存, 所以内存对齐的事情必须我们自己完成. 所以既然当前的指针占8个字节, 如果key的len加上2的值为13的话, 就应该被调整为16个字节; 如果值为17的话, 就应该被调整为24字节大小.所以这个宏就顺利的求出了某元素占用的内存大小的值.
下面就是哈希表初始化的详细代码以及解析
ngx_int_t
//传入的参数分别是:
// 初始化用的结构体
// 用于表示即将添加到哈希表中的元素, key-value组成的数组
// 即将添加到哈希表中的元素的个数
ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
{
u_char *elts;
size_t len;
u_short *test;
ngx_uint_t i, n, key, size, start, bucket_size;
ngx_hash_elt_t *elt, **buckets;
//这个for循环的主要作用是, 判断配置选项配置的每个bucket的容量是否能装的下任意一个元素(元素之间len可以不同). (换句话说, 就是要求每个bucket至少能装一个元素, 无论它是哪个元素)
//如果不符合, 就会返回错误
for (n = 0; n < nelts; n++) {
//下面这个判断就是说, name[n]中的key-value如果被装进ngx_hash_elt_t结构体(哈希表的每个元素的结构), 那么最后此元素的大小必须小于bucket的容量
//否则, 说明我们设置的bucket容量太小
//在调用宏之后还要加上一个指针的大小, 可以根据文章开头给出的图看出来, 每个bucket最后都会有个NULL指针作为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;
}
}
//下面这个test的用处多多. 它的大小是max_size, 是允许的bucket的最大个数.
//它的大小就表明, 以后使用这个test数组, 它与bucket是一一对应的. 即bucket[i]与test[i]是相关的
test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
if (test == NULL) {
return NGX_ERROR;
}
//bucket_size表示给定的每个bucket的容量.
//减去的就是最后的那个NULL指针的大小. 这个元素是个哨兵元素,用来判断当前bucket是否还有元素
bucket_size = hinit->bucket_size - sizeof(void *);
//既然我们不知到bucket的个数, 那么我们当然是从最小的size开始找(能少就少...)
//什么才是最小的size呢? 根据地址对齐, 一个ngx_hash_elt_t元素最少也要2*8个字节, 那么就拿这个值来找最小的size.
start = nelts / (bucket_size / (2 * sizeof(void *)));
start = start ? start : 1;
//根据实战经验做调整...无法理解
if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {
start = hinit->max_size - 1000;
}
//现在, 我们已经得到了最小的可能size值. size个bucket能把nelts个元素全装下, 假设每个elt的大小都只占16字节. 但假设是不成立的, 所以必须找更合适的size
for (size = start; size <= hinit->max_size; size++) {
//这个test数组中存放每个bucket的当前容量,如果某个bucket[i]的容量test[i]大于了规定的最大容量就意味着需要加大hash桶的个数size了
//利用memzero将每个bucket的当前容量test[i]设置为0
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;
//累加要被存放在bucket[key]的内存占用大小
test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
//一旦超过了, 说明size数目的bucket是不够的
if (test[key] > (u_short) bucket_size) {
goto next;
}
}
goto found;
next:
continue;
}
size = hinit->max_size;
ngx_log_error(NGX_LOG_WARN, hinit->pool->log, 0,
"could not build optimal %s, you should increase "
"either %s_max_size: %i or %s_bucket_size: %i; "
"ignoring %s_bucket_size",
hinit->name, hinit->name, hinit->max_size,
hinit->name, hinit->bucket_size, hinit->name);
found:
//这里表明我们已经成功找到了满足条件的size大小.
//依旧是test[i]对应bucket[i], 现在我们要求出所有元素总共需要多少内存. 最后申请出这块内存, 并一一分配给每个bucket
//首先, 我们算出每个bucket要存放的内存容量, 记录在test数组中
//此时NULL指针就需要算上了
for (i = 0; i < size; i++) {
test[i] = sizeof(void *);
}
//算出每个bucket要存放的内存容量, 记录在test数组中
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;
//得到所有元素总共需要的内存, 记录在len中
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];
}
//如果初始化结构体中的hash表不存在, 那我们需要手动申请一下.
if (hinit->hash == NULL) {
//值得注意的是, 这里申请的并不是单纯的基本哈希表结构的内存, 而是包含基本哈希表的通配符哈希表.
//之所以这样设计, 我认为是为了满足将来可能要init通配符哈希表的需求. 既然ngx_hash_wildcard_t中包含基本哈希表, 且使用起来并没有任何麻烦,
//那么这样是不是要显得更好呢?
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;
}
//一开始就定义的二级指针bucket, 指向哈希表中 ngx_hash_elt_t * 构成的数组. 即bucket构成的数组
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;
}
}
//将所需的所有元素的内存都申请到elts中.
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);
//下面就为每个bucket分配内存. 之前已经用test记录了每个bucket应该得到的内存大小.
for (i = 0; i < size; i++) {
if (test[i] == sizeof(void *)) {
continue;
}
buckets[i] = (ngx_hash_elt_t *) elts;
//根据记录好的每个bucket的大小来分配内存
elts += test[i];
}
//既然每个bucket拥有了它应该有的内存, 那么现在就将key-value数据搬进去
//现在依旧是test[i]对应bucket[i]. 此时的test数组用于记录当前的某bucket已经有多少内存被初始化了.
//如果这个元素已经搬到这个bucket中, 下一个元素首地址就是从当前元素首地址加上test[i]开始.
for (i = 0; i < size; i++) {
test[i] = 0;
}
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;
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]));
}
//在每个bucket最后加上NULL指针. 处理的时候,把它当成一个ngx_hash_elt_t结构看,在该结构中的第一个元素,正好是一个void指针,我们只处理它,别的都不去碰,所以没有越界的问题。
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;
}
ngx_free(test);
hinit->hash->buckets = buckets;
hinit->hash->size = size;
return NGX_OK;
}
对于内存分配, 内存地址对齐, cacheline对齐这些我依然有些迷惑的地方, 所以一些地方没有做解释. 主要还是我基础不够扎实!共勉!三、基本哈希表的查找.
//由key,name,len信息在hash指向的hash table中查找该key对应的value u
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;
elt = hash->buckets[key % hash->size];
if (elt == NULL) {
return NULL;
}
//对该bucket进行搜索. 直到该ngx_hash_elt_t结构中的value为NULL
while (elt->value) {
if (len != (size_t) elt->len) { //先判断长度
goto next;
}
for (i = 0; i < len; i++) {
if (name[i] != elt->name[i]) { //接着比较name的内容, 可以看到这里的比较很直接
goto next;
}
}
return elt->value;
next:
//这里的地址偏移到下一个ngx_hash_elt_t结构
elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
sizeof(void *));
continue;
}
return NULL;
}
这一段没有做过多解释, 理解起来也没有什么难点.最后, 贴上借鉴的几篇博客:
http://blog.youkuaiyun.com/livelylittlefish/article/details/6636229
http://blog.youkuaiyun.com/chen19870707/article/details/40794285
http://www.linuxidc.com/Linux/2012-08/67040.htm
http://blog.chinaunix.net/uid-27767798-id-3766755.html
感谢!