09Nginx源码分析之哈希结构(ngx_hash.c)

一 哈希结构

关于哈希的相关概念,可以先参考以下文章。

https://blog.csdn.net/weixin_44517656/article/details/105215349

注意:
下面的词语桶和槽是一样的,有些人习惯将桶称之为槽。

在阅读Nginx的hash结构时,一定不能着急,否则会让自己怒火中烧,建议大家都使用一周左右(甚至以上)去消化。
Nginx的hash表结构主要几个特点:

  • 1)静态只读。当初始化生成hash表结构后,是不能动态修改这个hash表结构的内容。
  • 2)将内存利用最大化。Nginx的hash表,将内存利用率发挥到了极致,并且很多设计上面都是可以供我们学习和参考的。
  • 3)查询速度快。Nginx的hash表做了内存对齐等优化。
  • 4)Nginx的hash结构主要解析配置数据。

1 hash表的元素结构ngx_hash_elt_t

该结构保存着哈希表的k-v键值对结构。

/**
 * 存储hash的元素
 */
typedef struct {
    void             *value; 	/* value指向用户数据。 */
    u_short           len;   	/* key的长度 */
    u_char            name[1]; 	/* 指向key的第一个地址,key长度为变长(设计上的亮点),所以实际也导致ngx_hash_elt_t是变长结构*/
} ngx_hash_elt_t;

2 hash表的基本结构ngx_hash_t

ngx_hash_t 是哈希的桶,它内部利用二级指针存放了一个桶数组和桶数组的大小。

/**
 * Hash的桶
 */
typedef struct {
    ngx_hash_elt_t  **buckets; 	/* hash表的桶数组 */
    ngx_uint_t        size; 	/* hash表的桶数组个数 */
} ngx_hash_t;

3 支持通配符的哈希表结构 ngx_hash_wildcard_t

ngx_hash_wildcard_t专用于表示牵制或后置通配符的哈希表,如:前置*.test.com,后置:www.test.* ,它只是对ngx_hash_t的简单封装,是由一个基本哈希表hash和一个额外的value指针,当使用ngx_hash_wildcard_t通配符哈希表作为容器元素时,可以使用value指向用户数据。

typedef struct {
    ngx_hash_t        hash;     /* hash表的基本结构 */
    void             *value;	/* value指向用户数据,关于ngx_hash_elt_t的value和这个value,在下面讲ngx_hash_wildcard_init的时候会补充 */
} ngx_hash_wildcard_t;

4 预添加哈希散列元素结构 ngx_hash_key_t

ngx_hash_key_t用于表示即将添加到哈希表中的元素,其结构如下:

typedef struct {
    ngx_str_t         key;			/* 元素关键字 */
    ngx_uint_t        key_hash;		/* 由哈希方法算出来的哈希值 */
    void             *value;		/* 指向用户自定义数据。关于ngx_hash_elt_t,ngx_hash_wildcard_t和本value,可以看下面的总结。 */
} ngx_hash_key_t;

其中补充(位于ngx_string.h)typedef struct {
    size_t      len;				/* key的长度 */	
    u_char     *data;				/* 字符串key,例如key为people,则len=6,data指向people,注意区别上面的value */	
} ngx_str_t;	

5 组合类型哈希表 ngx_hash_combined_t

ngx_hash_combined_t是由3个哈希表组成,一个普通hash表hash,一个包含前向通配符的hash表wc_head和一个包含后向通配符的hash表 wc_tail。

typedef struct {
    ngx_hash_t            hash;
    ngx_hash_wildcard_t  *wc_head;
    ngx_hash_wildcard_t  *wc_tail;
} ngx_hash_combined_t;

6 哈希表初始化结构ngx_hash_init_t

ngx_hash_init用于初始化哈希表,初始化哈希表的槽的总数并不是完全由max_size成员决定的,而是由在做初始化时预先加入到哈希表的所有元素决定的,包括这些元素的总数、每个元素的关键字长度等,还包括操作系统的页面大小,这个算法比较复杂,可以在ngx_hash_init函数中找到这个算法它的结构如下:

/**
 * hash表主体结构
 */
typedef struct {
    ngx_hash_t       *hash;			/* 指向hash表基本结构 */
    ngx_hash_key_pt   key;  		/* 计算key散列的方法,函数指针 */
 
    ngx_uint_t        max_size; 	/* hash表的桶数组最大个数,max_size>=ngx_hash_t.size */
    ngx_uint_t        bucket_size; 	/* 每个bucket桶存储ngx_hash_elt_t元素的存储空间大小 */
 
    char             *name;			/* hash表名称 */
    ngx_pool_t       *pool; 		/* 内存池,它负责分配基本哈希列表、前置通配哈希列表、后置哈希列表中所有bucket */
    ngx_pool_t       *temp_pool; 	/* 临时内存池。它仅存在初始化哈希表之前,用于分配一些临时的动态数组,带通配符的元素初始化时需要用到临时动态数组 */
} ngx_hash_init_t;

其中ngx_hash_key_pt:
typedef ngx_uint_t (*ngx_hash_key_pt) (u_char *data, size_t len);

7 ngx_hash_keys_arrays_t结构

可以看到,这里设计了3个简易的哈希列表( keys_hash、dns_wc_head_hash、dns_wc_tail_hash),即采用分离链表法来解决冲突(多个哈希表)。

typedef struct {
    ngx_uint_t        hsize;			/* 散列中槽(bucket桶)的总数 */

    ngx_pool_t       *pool;				/* 内存池 */
    ngx_pool_t       *temp_pool;		/* 临时内存池,下面的临时动态数组都是由临时内存池分配 */

    ngx_array_t       keys;				/* 存放所有非通配符key的数组。 */
    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值,
    									   也就是是否有重复。
    									*/
    									
    ngx_array_t       dns_wc_head;		/* 存放前向通配符key被处理完成以后的值。比如:“*.abc.com”被处理完成以后,变成“com.abc.”被存放在此数组中 */
    ngx_array_t      *dns_wc_head_hash;	/* 该值在调用的过程中用来保存和检测是否有冲突的前向通配符的key值,也就是是否有重复 */

    ngx_array_t       dns_wc_tail;		/* 存放后向通配符key被处理完成以后的值。比如:“mail.xxx.*”被处理完成以后,变成“mail.xxx.”被存放在此数组中 */
    ngx_array_t      *dns_wc_tail_hash;	/* 该值在调用的过程中用来保存和检测是否有冲突的后向通配符的key值,也就是是否有重复。 */
} ngx_hash_keys_arrays_t;

其中ngx_array_t(位于ngx_array.h)typedef struct {
    void        *elts;		
    ngx_uint_t   nelts;
    size_t       size;
    ngx_uint_t   nalloc;
    ngx_pool_t  *pool;
} ngx_array_t;

8 ngx_table_elt_t结构

ngx_table_elt_t不在ngx_hash.c中使用,主要用于HTTP模块。
ngx_table_elt_t是一个key/value对,ngx_str_t类型的key、value成员分别存储的是名字、值字符串。
hash成员表明ngx_table_elt_t也可以是某个散列表数据结构中的成员。ngx_uint_t类型的hash成员可以在ngx_hash_t中更快地找到相同key的ngx_table_elt_t数据。lowcase_key指向的是全小写的key字符串。

ngx_table_elt_t是为HTTP头部量身定制的,其中key存储头部名称,value存储对应的值,lowcase_key是为了忽略HTTP头部名称的大小写,hash用于快速检索到头部。
比如:
Content-Length:1024。

typedef struct {
    ngx_uint_t hash;		/* 内存池 */
    ngx_str_t key;			/* ngx_str_t类型的key */
    ngx_str_t value;		/* ngx_str_t类型的value */
    u_char *lowcase_key;	/* lowcase_key指向的是全小写的key字符串 */
} ngx_table_elt_t;

二 数据结构图

以下是对ngx_hash_init_t,ngx_hash_t,ngx_hash_elt_t,ngx_hash_key_pt,ngx_hash_wildcard_t等结构体的关系图。
在这里插入图片描述

三 具体函数实现

1 哈希表初始化ngx_hash_init函数

哈希表的初始化设计得非常牛逼,具体的体现有:

  • 1)桶大小估算,首先会根据ngx_hash_elt_t类型的数组name及其nelts个数估算最小需要的桶的数目start,然后再从这个数目开始搜索,大大提高了效率,值得学习。
  • 2)ngx_hash_elt_t中uchar name[1]+ushort 类型的len设计,那么如果在name很短的情况下,字节对齐后name和len最多占8字节,最短占4字节,而使用uchar* 的指针64位占8字节,32占4字节,比前者多0~4个字节。可以看到ngx是真的在内存上考虑,节省每一分内存来提高并发。
  • 3)接着,我们看到下面求ngx_hash_elt_t所占字节数的方式,并不是用sizeof(ngx_hash_elt_t),而是定义特定的宏来根据字节对齐来求。

其中,第一个(sizeof(void *)为求成员value的字节数,ngx_align((name)->key.len为key的字节数,2是u_short本身的字节数,通过ngx_align宏来对齐,最终结果就是连对齐后补充的字节数也算进来,所以每次调用该宏都是获取该元素在内存所占的字节数。
并且有个小细节,sizeof(void *)可以每次根据系统的位数来获取8字节还是4字节对齐。

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

/*
注意:NGX_HASH_ELT_SIZE只是用到name(ngx_hash_key_t)的key,
而并非求ngx_hash_key_t结构的大小,而是求ngx_hash_elt_t元素的大小
*/
#define NGX_HASH_ELT_SIZE(name)                                               \
    (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))

初始化哈希表的代码如下,大概流程为:

  • 1)先判断相应条件,例如桶数组大小不能为0。桶的容量bucket_size太大,不能大于65536 - ngx_cacheline_size。每个元素必须在bucket_size范围内。
  • 2)通过一定算法估算出至少需要桶的数量,然后再从最小桶数目开始试,计算容下 nelts 个元素需要多少个桶。
  • 3)分配桶数组的内存。
  • 4)分配每个桶的容量。
ngx_int_t
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;
	
	//桶数组的大小为0返回
    if (hinit->max_size == 0) {
        ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
                      "could not build %s, you should "
                      "increase %s_max_size: %i",
                      hinit->name, hinit->name, hinit->max_size);
        return NGX_ERROR;
    }
	
	/*
	桶的容量bucket_size太大,大于65536 - ngx_cacheline_size时返回,
	减去ngx_cacheline_size是为了后面len+ngx_cacheline_size的对齐
	*/
    if (hinit->bucket_size > 65536 - ngx_cacheline_size) {
        ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
                      "could not build %s, too large "
                      "%s_bucket_size: %i",
                      hinit->name, hinit->name, hinit->bucket_size);
        return NGX_ERROR;
    }

	/*
	判断每个元素是否超过整个桶的容量,加上sizeof(void *)是
    后面为了对齐,bucket_size减去了sizeof(void *)。
	也就是说,每个桶最多占bucket_size-sizeof(void *)以下的容量。
	*/
    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 %s, you should "
                          "increase %s_bucket_size: %i",
                          hinit->name, hinit->name, hinit->bucket_size);
            return NGX_ERROR;
        }
    }

	/*
	test是u_short数组,用于临时保存每个桶的当前容量大小,所以
	开辟max_size个,为后续进行开辟内存等操作提供便利
	*/
    test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
    if (test == NULL) {
        return NGX_ERROR;
    }

	/*
	每个桶最多占hinit->bucket_size - sizeof(void *)以下的容量。
	*/
    bucket_size = hinit->bucket_size - sizeof(void *);
	
	/*
	这里初略估算所需桶的最小数量,方法如下:
	1)由于ngx_hash_elt_t元素结构对齐后,至少所需NGX_HASH_ELT_SIZE(&name[n]) > (2*sizeof(void*))的空间
	2)所以bucket_size 大小的桶最多能容下 bucket_size/(2*sizeof(void*)) 个元素。
	例如bucket_size=10,机器为32位,存两个元素,那么2/(10/(2*4)),那么最少占用2个桶。
	*/
    start = nelts / (bucket_size / (2 * sizeof(void *)));
    start = start ? start : 1;//桶最少为1个

	/*
	实际上上面两步已经初步推算出至少需要的桶数的初始估算下标,
	这一步可以认为是ngx的经验得出,当max_size太大,并且取临界值时,nelts大于100个时,
	使最少需要的桶数的估算下标降低1000,以此让start~max_size有更多的范围获得所需要的桶数
	*/
    if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {
        start = hinit->max_size - 1000;
    }

	/*
	从最小桶数目开始试,计算容下 nelts 个元素需要多少个桶。
	外层循环为桶的估计下标~最大个数。内层循环为传进的元素个数。主看内层循环
	*/
    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) {//key为空则跳过
                continue;
            }

            key = names[n].key_hash % size;//根据元素的哈希值获取桶的下标,即key
            len = test[key] + NGX_HASH_ELT_SIZE(&names[n]);//统计该桶的容量,当超过bucket_size时寻
            		//找下一个桶去存储元素。例如size=1第一个桶,有两个元素28,32,60,取余后都是0,
            		//假设bucket_size能存储28,32但不能存储60,那么60将存储在size=1的桶。
            		//注意:这里只是估计test[key]即每个桶的实际长度,即使key相同也是可以的
#if 0
            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "%ui: %ui %uz \"%V\"",
                          size, key, len, &names[n].key);
#endif

            if (len > bucket_size) {//上面所说的,当前桶容量不够寻找下一桶存储
                goto next;
            }

            test[key] = (u_short) len;//保存每个桶的实际存储长度
        }

		/*
		来到这里,说明内层for即所有元素均找到合适的桶存储
		*/
        goto found;

    next:

        continue;
    }

    size = hinit->max_size;//如果来到这里,说明外层循环遍历满而退出,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:

	//执行到这里,我们已经获取到能存放nelts个元素的size桶个数

	//清空test数组
    for (i = 0; i < size; i++) {
        test[i] = sizeof(void *);
    }
	
	/*
	遍历元素个数,根据每个元素的哈希key和元素大小,统计每个桶的容量并存放在test数组。
	*/
    for (n = 0; n < nelts; n++) {
        if (names[n].key.data == NULL) {//元素的key是空就跳过
            continue;
        }

        key = names[n].key_hash % size;
        len = test[key] + NGX_HASH_ELT_SIZE(&names[n]);//统计每个桶的存储实际元素的大小,注意test[key]是会改变的
		
		/*
		这里前面说过,减去ngx_cacheline_size是为了后面的len+ngx_cacheline_size进行内存对齐
		并且与开始bucket_size一样,也需要让桶的容量len低于65536 - ngx_cacheline_size
		因为前面是没有对实际的len容量进行限制,这一个判断也是ngx后面版本添加的
		*/
        if (len > 65536 - ngx_cacheline_size) {
            ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
                          "could not build %s, you should "
                          "increase %s_max_size: %i",
                          hinit->name, hinit->name, hinit->max_size);
            ngx_free(test);
            return NGX_ERROR;
        }

        test[key] = (u_short) len;
    }

    len = 0;

	/*
	遍历桶的个数,根据每个桶的实际存储容量进行对齐,获得对齐后的容量,
	然后统计得出所有元素对齐后所需的总长度len
	*/
    for (i = 0; i < size; i++) {
        if (test[i] == sizeof(void *)) {//如果没有用到该桶的话 就跳过
            continue;
        }

		/*
		注意这里对齐后,test数组中存放着每个容量对齐后的桶的容量。例如桶的大小为100,
		ngx_cacheline_size=64,那么对齐后test[i]=(((d) + (a - 1)) & ~(a - 1)),
		即100+63&~63=163&000000=10100011&000000=10000000=2^7=128,大家可以自己写个测试程序,
		因为当时我也是写了个测试程序
		*/
        test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));

        len += test[i];//该len是所有元素对齐后的总长度len
    }
	
	/*
	这个步骤就是我们上面所画的图,当哈希表为空时,额外开辟带ngx_hash_wildcard_t的内存,
	不为空就只开辟buckets的内存。
	并且这里似乎看起来很奇怪,既然是hash,为什么分配空间的大小又跟hash结构体一点关联都没有呢?
	因为ngx_hash_wildchard_t包含hash这个结构体,所以就一起分配了
	并且把每个桶的指针也分配在一起了,这种思考跟以前学的面向对象思想很不一样,但这样会很高效
	*/
    if (hinit->hash == NULL) {
    	//开辟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;
        }
		
		//获取每个桶数组首地址,注意末尾有s代表数组
        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;
        }
    }

	/*
	上一步开辟完桶数组的内存后,自然是开辟每个桶的容量内存。
	但是为了对齐,len总长度必须加上ngx_cacheline_size,上面虽然len的总长度是经过对齐获得的。
	这是因为,即使从pool内存池分配的地址必为偶数,但是由于我们需要对齐,会调用ngx_align_ptr宏导致返回的地址向上对齐,
	这就这导致开辟的内存存在不够用,所以必须加上ngx_cacheline_size。
	例如假设内存池返回的elts=0x2地址,len的64,2+63&000000=64,所以首地址变成64,那么刚刚开辟的内存实际剩下66-64=2
	明显是不对的,所以必须填充一个ngx_cacheline_size以便对齐。
	ngx_cacheline_size的值可能为32位,64位,128位,从cpuinfo函数中读取。
	*/
    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];//存放元素的内存自增每个桶的实际存储长度
    }

	//清零,为哈希表的赋值做准备
    for (i = 0; i < size; i++) {
        test[i] = 0;
    }

	/*
	遍历元素个数,将传进的元素复制给哈希表的元素,这里是真正的赋值操作
	*/
    for (n = 0; n < nelts; n++) {
        if (names[n].key.data == NULL) {//如果key是空就跳过
            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;

		/*
        赋值名字并转小写,因为elt元素是一个数组,所以ngx这里可以将len个字节付给一个字节的name,
        但平时尽量别这样用,容易造成越界,因为你不是大师
        */
        ngx_strlow(elt->name, names[n].key.data, names[n].key.len);

        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    }

	/*
	遍历桶个数,为每个桶的结尾元素的value添加NULL,表示该桶结束。
	*/
    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;//置value为空是因为连续内存的数组最后元素的末尾为value,因为value是elt结构的首个成员。
    }

    ngx_free(test);

    hinit->hash->buckets = buckets;
    hinit->hash->size = size;

#if 0

    for (i = 0; i < size; i++) {
        ngx_str_t   val;
        ngx_uint_t  key;

        elt = buckets[i];

        if (elt == NULL) {
            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "%ui: NULL", i);
            continue;
        }

        while (elt->value) {
            val.len = elt->len;
            val.data = &elt->name[0];

            key = hinit->key(val.data, val.len);

            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "%ui: %p \"%V\" %ui", i, elt, &val, key);

            elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len,
                                                   sizeof(void *));
        }
    }

#endif

    return NGX_OK;
}

至此该函数结束,要想彻底的理解这个函数和我的注释,建议按照以下例子将值代进去代码,一步步计算理解,并且最好代入时可以自己画个图理解,因为这个函数还是很复杂的,切不可操之过急。
例子:

1)假设max_size=100;bucket_size=1000;传进的数组有3个元素,其中具体结构如下:
typedef struct {
    ngx_str_t         key;
    ngx_uint_t        key_hash;
    void             *value;
} ngx_hash_key_t;
继续假设:
key.data="tyy1",那么key.len=4,value设为NULL即可。//第一个元素
key.data="tyy2",那么key.len=4,value设为NULL即可。//第二个元素
key.data="tyy3",那么key.len=4,value设为NULL即可。//第三个元素

2)经过运算,bucket_size=992,每个元素占16字节(假设64位机器),那么start=1。此时只用到一个桶存储这三个元素。

3)test[0]开始占48字节,对齐后占64字节。
然后继续往下代入即可,你就可以理解到该函数了。

2 通过哈希表处理通配符的ngx_hash_wildcard_init初始化函数

2.1 支持通配符的hash表的书写形式
支持通配符的ngx_hash_wildcard_t实际上是对基本hash表的一层封装,可以简单的通过ngx_hash_wildcard_t结构体内部成员看出。
nginx为了处理带有通配符的域名字符串的匹配问题,实现了支持通配符的hash表nginx中的通配符有两种形式:

  • 1)一种是通配符在前面的例如“*.test.com”,这样可以省略"*"号,写成“.test.com”。
  • 2)还有一种是通配符在后面的例如"www.test.*",这样的域名不能省略"* "星号,因为这样的通配符可以匹配“www.test.com”、“www.test.cn”、“www.test.org”等域名。并且注意:nginx不能同时包含在前和在后的通配符例如“*.test.*”。

2.2 初始化的大概流程

  • 1)为临时数组curr_names,next_names申请空间。
  • 2)遍历数组中的元素,找出当前字符串的第一个key字段并放入curr_names数组,若当前字符串仍有剩余字符,则优先放进next_names数组(注意:每循环一次,next_names都会被清0一次)。
  • 3)依次比较names中剩余的元素,若具有相同key字段,除掉相同的部分后,接着将剩余部分放入next_names数组。
  • 4)递归处理next_names数组。
  • 5)回到第二步,如果已经遍历完则将curr_names数组元素放入hash表。

2.3 该函数的参数注意事项

ngx_int_t ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts);

重点注意的是参数2,该传入参数names数组是经过处理后的字符串数组。对于字符串数组: “*.test.tyy.com”、".tyy.com"、"*.example.com"、"*.example.org"、".tyy.org"处理完成之后就变成了
“com.tyy.test.”、“com.tyy”、“com.example.”、“org.example.”、“org.tyy”。
可以看到一个特点,若有*则保留".",没有则去掉"."。

2.4 ngx_hash_wildcard_init源码

ngx_int_t
ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
    ngx_uint_t nelts)
{
    size_t                len, dot_len;
    ngx_uint_t            i, n, dot;
    ngx_array_t           curr_names, next_names;
    ngx_hash_key_t       *name, *next_name;
    ngx_hash_init_t       h;
    ngx_hash_wildcard_t  *wdc;

    
    //为临时数组curr_names申请空间,curr_names用于存放当前节点
    if (ngx_array_init(&curr_names, hinit->temp_pool, nelts,
                       sizeof(ngx_hash_key_t))
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    //为临时数组next_names申请空间,next_names用于存放去除当前key字段后,剩余的字符串
    if (ngx_array_init(&next_names, hinit->temp_pool, nelts,
                       sizeof(ngx_hash_key_t))
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    /* 
    遍历数组中元素,对元素进行分割递归处理。
    注意:该循环是最重要的循环,几乎包含整个函数。
    */
    for (n = 0; n < nelts; n = i) {

#if 0
        ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                      "wc0: \"%V\"", &names[n].key);
#endif

        //标志位,用于标志是否找到".",即字符串是否可以被分割成多个部分
        dot = 0;

        /*
        依次遍历每个元素的key,找到当前元素key值中第一次出现“.”的位置
        其中:len表示key的长度(若找到".",则代表"."之前的长度),data表示key的值。
        */
        for (len = 0; len < names[n].key.len; len++) {
            if (names[n].key.data[len] == '.') {
                dot = 1;
                break;
            }
        }

        /*
        为“.”之前的key字段申请空间。
        注意ngx的数组的push实际上是这样申请空间的:
        若数组还有空间,则直接获取对应内存返回,没有则在内存池上为数组扩容(实际创建新数组,数组大小x2),元素开辟大小由初始化时确定。
        */
        name = ngx_array_push(&curr_names);
        if (name == NULL) {
            return NGX_ERROR;
        }

        //为"."之前的key字段赋值(保存在key_hash)
        name->key.len = len;
        name->key.data = names[n].key.data;//data仍保留原来元素key的整个字符串
        name->key_hash = hinit->key(name->key.data, name->key.len);//散列回调函数,给哈希值赋值,参1为整个字符串,参2为"."的下标
        name->value = names[n].value;//这也是一个值,需要与上面的data,key_hash区分

#if 0
        ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                      "wc1: \"%V\" %ui", &name->key, dot);
#endif

        //记录剩余字符串的开头下标
        dot_len = len + 1;

        //若找到".",则指向剩余字符串的开头
        if (dot) {
            len++;
        }

        /*
        重置next_names数组的元素个数,这一步也相当于重置数组,因为原本next_names数组内的数组会被覆盖掉。
        用于统计下一次递归的next_names.nelts个数。
        很重要,因为下面关系到是否能进入递归的逻辑(我画图的时候忽略了),一时大意,导致这用了不少时间去理解。

        并且看到,ngx若想清空数组,直接将成员nelts置0即可,不用清空,非常快速。所以一般人很难写出这种和平时开发相悖的代码。
        */
        next_names.nelts = 0;

        /*
        如果names[n]的"."后还有剩余字符串,将剩余字符串放入next_names被清空的数组中。
        因为如果next_names数组有仍元素,将会被覆盖,因为上面将nelts置0了
        */
        if (names[n].key.len != len) {
            next_name = ngx_array_push(&next_names);
            if (next_name == NULL) {
                return NGX_ERROR;
            }

            next_name->key.len = names[n].key.len - len;
            next_name->key.data = names[n].key.data + len;//此时data指向剩余字符串开头,即"."的后一位
            next_name->key_hash = 0;//哈希值为0
            next_name->value = names[n].value;

#if 0
            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "wc2: \"%V\"", &next_name->key);
#endif
        }

        /*
        
        搜索names形参数组后面的元素,看有没有和当前元素相同key字段的元素,如果有则把除去当前key字段的剩余字符串装入该数组。

        存在两种情况(两个if条件):
        1)若key不相等,没必要在比较,直接退出(注意此时连"."也比较,因为len++了)。
        2)若没找到dot即没有".",并且len的位置不是".",也退出。
            这种情况是由于当前key作为后面元素的子串造成的,它们实际上也是不相等的,
            只不过在前面的某个部分相等,但不会出现当前key作为后面元素的父串而相等的情况。
            例如当前key为"abc",后一个元素为"abc123.hhh",那么就会存在abc=abc的情况,所以必须排除。

        总结下面的for:实际上,下面的for主要是为了过滤其它字符串的第一个key相同的字段。
                      然后再排除有可能造成第一个key相同的干扰。

        */
        for (i = n + 1; i < nelts; i++) {//传进来只有一个元素时,该for直接跳过
            if (ngx_strncmp(names[n].key.data, names[i].key.data, len) != 0) {/*连"."也比较(因为len加加了!!!),
                                                                        原因是参数传进时是经过反转处理的,所以必须也比较"."(代入理解)
                                                                        注意:这里总觉得mgx写得不好,因为len是从n下标的当前元素找到的,
                                                                        故它不会越界,但是i是n的下一元素下标,容易引起越界误解,例如,
                                                                        下标n指向"tyy.test",而下标i指向"tyy",那么len=3,再len++变成4,
                                                                        容易造成误解,虽然系统一般会在字符串末尾添加0,并且经过实验测试,
                                                                        并不会造成程序报错,ngx真的是大师,每一个字节每个位都不放过使用*/
                break;
            }

            if (!dot
                && names[i].key.len > len           //此条件是为了防止names[i].key.data[len]索引len越界
                && names[i].key.data[len] != '.')
            {
                break;//这个if的意思是只要没找到dot并且下一元素的len下标不是".",则直接返回,因为意味着两个元素的第一个key不相等。
            }

            //如果有则把除去当前key字段的剩余字符串装入该数组
            next_name = ngx_array_push(&next_names);
            if (next_name == NULL) {
                return NGX_ERROR;
            }

            next_name->key.len = names[i].key.len - dot_len;//这里需要用到保留的dot_len
            next_name->key.data = names[i].key.data + dot_len;//此时data指向剩余字符串开头,即"."的后一位
            next_name->key_hash = 0;//哈希值为0
            next_name->value = names[i].value;

#if 0
            ngx_log_error(NGX_LOG_ALERT, hinit->pool->log, 0,
                          "wc3: \"%V\"", &next_name->key);
#endif
        }

        /*
        如果next_names数组中仍有元素,则递归处理该元素;
        否则若找到dot,则将curr_names数组的当前元素name=>value | 1。
        然后继续循环(注意还没退出循环去执行ngx_hash_init)。
        */
        if (next_names.nelts) {

            h = *hinit;//原来传进的哈希表赋值给局部变量h。注意,是赋值,所以每次递归都会创建一个新的哈希表基本结构,交给h管理
            h.hash = NULL;//hash指向空

            if (ngx_hash_wildcard_init(&h, (ngx_hash_key_t *) next_names.elts,
                                       next_names.nelts)//递归,每次传局部变量h与剩余字符串数组next_names
                != NGX_OK)
            {
                return NGX_ERROR;
            }

            wdc = (ngx_hash_wildcard_t *) h.hash;

            if (names[n].key.len == len) {/*将用户value值放入新的hash表。这里的判断类似下面的dot,当中间的递归的cur数组中的names[n]元素
                                            末尾字符是"."的话,则让wdc->value保存names[n].value的值。例如该元素为"test.",那么就会
                                            满足if条件。注意该判断是一般是发生在递归中间,其它情况还没判断过。*/                        
                wdc->value = names[n].value;
            }

            /*暂停分析这里!!!
            并将当前value值指向新的hash表。由于指针都字节对齐了,低2位肯定为0,巧妙使用剩余的字节位数提高效率。
            wdc是每次递归后创建返回的哈希表,后面表示:若当前names数组下标为n元素中找到".",那么就使低两位为11,否则为10.
            name->value是连接每一个哈希表结构的关键。
            */
            name->value = (void *) ((uintptr_t) wdc | (dot ? 3 : 2));

        } else if (dot) {/*带有前置通配符的字符串,因为来到这里说明next数组没元素,并且当前names数组下标为n元素中找到了".",例如"example.",
                           因为传进时是倒转的,所以就是前置,并且最低位 | 1即01,因为dot是找到".",所以反过来后:01仅仅是
                           "*.example.org"的数据指针。*/
            name->value = (void *) ((uintptr_t) name->value | 1);
        }

    }//注意这个是for循环,不要轻易退出,人何常不是这样呢!

    
    /*
    curr_names.elts代表数组的首地址.该语句代表将本次curr_names数组的所有元素送去初始化成哈希表。
    */
    if (ngx_hash_init(hinit, (ngx_hash_key_t *) curr_names.elts, curr_names.nelts) != NGX_OK)
    {
        return NGX_ERROR;
    }

    return NGX_OK;
}

2.5 对ngx_hash_wildcard_init例子解析
以下面字符数组代入进行一步步分析。
“*.test.tyy.com”、".tyy.com"、"*.example.com"、"*.example.org"、".tyy.org"处理完成之后就变成了
“com.tyy.test.”、“com.tyy”、“com.example.”、“org.example.”、“org.tyy”。

即最终代入的5个元素是"com.tyy.test."、“com.tyy”、“com.example.”、“org.example.”、“org.tyy”。

其中分析时比较重要的结构体如下:

ngx_int_t
ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)

/**
 * hash表主体结构
 */
typedef struct {
    ngx_hash_t       *hash;			/* 指向hash表基本结构 */
    ngx_hash_key_pt   key;  		/* 计算key散列的方法,函数指针 */
 
    ngx_uint_t        max_size; 	/* hash表的桶数组最大个数,max_size>=ngx_hash_t.size */
    ngx_uint_t        bucket_size; 	/* 每个bucket桶存储ngx_hash_elt_t元素的存储空间大小 */
 
    char             *name;			/* hash表名称 */
    ngx_pool_t       *pool; 		/* 内存池,它负责分配基本哈希列表、前置通配哈希列表、后置哈希列表中所有bucket */
    ngx_pool_t       *temp_pool; 	/* 临时内存池。它仅存在初始化哈希表之前,用于分配一些临时的动态数组,带通配符的元素初始化时需要用到临时动态数组 */
} ngx_hash_init_t;

/**
 * Hash的桶
 */
typedef struct {
    ngx_hash_elt_t  **buckets; 	/* hash表的桶数组 */
    ngx_uint_t        size; 	/* hash表的桶数组个数 */
} ngx_hash_t;

/* 该元素结构表现为处理元素的主要结构。与下面的存储想区别。*/
typedef struct {
    ngx_str_t         key;			/* 元素关键字 */
    ngx_uint_t        key_hash;		/* 由哈希方法算出来的哈希值 */
    void             *value;		/* 指向用户自定义数据 */
} ngx_hash_key_t;

/* 该元素结构表现为存储元素的主要结构。 */
typedef struct {
    void             *value;
    u_short           len;
    u_char            name[1];
} ngx_hash_elt_t;

假设我们传进的ngx_hash_init_t的hash成员为空。key,max_size,bucket_size,name,pool,temp_pool皆由用户传进,那么下面有(下面的n是对ngx_hash_wildcard_init的最外层for循环的变量n的描述):

  1. 第一次传进上面字符数组的5个元素,并且n=0,即第一个元素"com.tyy.test."时:
    1.1 因为n=0时,next_names数组有三个元素,所以会继续递归调用ngx_hash_wildcard_init,所以也有第二次传进的字符数组有3个元素,并且n=0时:
    1)即,第一次和第二次调用ngx_hash_wildcard_init时,n各自为:n=0,n=0。看下图:
    在这里插入图片描述

    2)继续回到遍历元素的最外层for循环即for (n = 0; n < nelts; n = i),由于上一次循环i=n+1=0+1,所以此时第二次调用ngx_hash_wildcard_init的n=i,即n=1,为了画图方便,将第二次调用ngx_hash_wildcard_init的n=2的步骤也包含在下图。所以此时从len=3,data="tyy"这个元素开始。
    即,第一次和第二次调用ngx_hash_wildcard_init时,n各自为:n=0,n=1,2。看下图:
    注意:下图的第二个next_names此时无元素,是因为代码中执行next_names.nelts = 0后,不会进入递归(下面的同理),但此时next_names数组仍包含上面len=5,"test."这个元素,但若往该数组重新添加元素,则该元素将被覆盖,所以认为它是无元素,ngx这种处理也用到了很多地方。
    在这里插入图片描述

以上的代入过程非常严谨。

  1. 经过上面步骤后,第一次传5个元素的第一个元素"com.tyy.test."已经被处理完毕,即第一次调用的n=0处理过程已经完成。接着就是回到第一次传5个元素的最外层for循环,由于传5个元素时,有3个元素的第一个key即"com"是相同的,所以此时最外层for循环n=i=3,从第四个元素"org.example."开始。并且再次强调,next_names的三个元素由于next_names.nelts = 0清零后,从数组拿到的内存就是数组首地址,所以会覆盖原有的元素。
    看下图:
    在这里插入图片描述

  2. 上面遍历完5个元素后,由于n=4,i=5,所以会退出for循环。最终将下图的curr_names数组中的元素执行最后一次的ngx_hash_init函数调用。

    调用完后,如下图:
    在这里插入图片描述

至此,例子的调用执行完毕。下面就是总结。

三 总结

1 例子的整体结构图
上面每一个小步骤可能都会遗漏之前已经画过的部分,例如上面的第3点部分哈希结构没画出第2点的内容。所以这里为了方便观察,我对上面的所有元素做一个总结(第3点已经分析调用完毕)。这个图是最重要的总结图。
在这里插入图片描述

2 上面的每个小步骤讲完,下面我们总结一下上面的图,看看它们是怎么连接在一起的。
上图第三大点的1可以看到,ngx的连接设计的非常巧妙,每次递归执行到末尾后,先将末尾的curr_names中元素初始化成哈希表,然后哈希表作为上一个哈希表的元素的成员,即使用value保存该哈希表。然后上一个的哈希表又作为上上个哈希表的元素的成员,即又使用value保存该哈希表,层层嵌套。最终通过返回用户传进的hinit,就可以遍历里面所有的哈希表成员。

3 关于value的指向
1)ngx_hash_elt_t

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

对于ngx_hash_elt_t的value,在第三大点的1图中可以看到,它可以指向下一个elt或者NULL,也可以指向下一个hash表结构。

2)ngx_hash_key_t

typedef struct {
    ngx_str_t         key;
    ngx_uint_t        key_hash;
    void             *value;
} ngx_hash_key_t;

对于ngx_hash_key_t,在第二大点的2.5的1或者2这些图中可以看到,它主要是用于临时处理数据的结构,但是该value可以指向真正的数据或者下一个hash表结构,不过起临时作用。
在这里插入图片描述
3)ngx_hash_wildcard_t

typedef struct {
    ngx_hash_t        hash;
    void             *value;
} ngx_hash_wildcard_t;

分析ngx_hash_wildcard_t,由于上面的例子并未涉及到该value,所以需要从下面这句代码分析。
由于names[n].value是ngx_hash_key_t结构,所以ngx_hash_wildcard_t的value既然是ngx_hash_key_t的value赋值,那么它们的意思就是一样的。即可以指向真正的数据或者下一个hash表结构。

if (names[n].key.len == len) {                    
  	 wdc->value = names[n].value;
}

1.4 vlaue的低两位来标志来携带相关信息
由于调用的初始化哈希表函数ngx_hash_init里面必然会进行内存对齐,所以最低两位必定为0。例如:

注意这里对齐后,test数组中存放着每个容量对齐后的桶的容量。
例如桶的大小为100,ngx_cacheline_size=64,
那么对齐后test[i]=(((d) + (a - 1)) & ~(a - 1)),100+63&~63=163&000000=10100011&000000=10000000=2^7=128。
可以看到,最低两位为0,必定为0是因为计算机字节对齐最低32位,
所以ngx调用对齐宏后最低两位必定为0。

实际上这个例子就是ngx_hash_init里面讲过的。

(以下暂未理解)
所以,对比第三大点的1.1的图,总结ngx低两位的的意思:

00 - value 是 "example.com""*.example.com"的数据指针。
01 - value 仅仅是 "*.example.com"的数据指针。
10 - value 是 支持通配符哈希表是 "example.com""*.example.com" 指针。
11 - value 仅仅是 "*.test.tyy.com"("*.example.org")的指针。

到此,ngx的哈希初始化分析完毕。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值