nginx普通哈希表

        nginx的各个模块都在使用哈希表结构。例如:在解析nginx.conf时,为了解析www.baidu.com.*后置通配符, *.baidu.com后置通配符, 以及www.baidu.com正常server关键字时,会使用哈希表进行存储。nginx实现了3个哈希表,分别为普通哈希表、前置通配符哈希表、后置通过符哈希表。 这三个哈希表都只实现了初始化接口,查询接口,而没有实现插入接口。也就是说nginx的哈希表的所有数据都是初始化的时候预先构造好的,不支持动态的往哈希表中插入数据。因为nginx.conf配置文件中的数据是一种静态结构,预先可以知道有多少配置项。

一、哈希表结构

//哈希表中的元素
typedef struct 
{
    void             *value;			//value值,指向用户定义的结构
    u_short           len;				//name的长度,也就是键的长度
    u_char            name[1];			//key的首地址
} ngx_hash_elt_t;


//哈希表结构
typedef struct 
{
    ngx_hash_elt_t  **buckets;			//哈希表的首地址
    ngx_uint_t        size;				//哈希表中槽的总数
} ngx_hash_t;


图: 哈希表内存布局

        这是哈希表内存布局图,后面要分析的哈希表初始化,就是为了构造如图所示的结构。查找哈希表也以这张图为例。

1、buckets是一个哈希槽指针数组,每一个哈希槽指向的哈希元素为ngx_hash_elt_t结构。那为什么有些哈希槽指向的哈希元素是一个ngx_hash_elt_t数组呢。这是为了解决哈希冲突,当key相同时,就需要查找哈希元素数组了,直到找到value值。

2、每一个哈希元素结尾都有一个ngx_hash_elt_t空指针,例如value = NULL; 实际上这个value为ngx_hash_elt_t结构中的成员。然后只使用了value字段,其它字段没有使用。将value强制转换为ngx_hash_elt_t结构。例如下图所示:


3、另外哈希元素缓冲区是一个预先开辟的一个缓冲区,之后根据每一个哈希槽指向的哈希元素大小进行分割。

二、哈希表初始化

        函数ngx_hash_init使用参数names数组中的元素构造一个如上图所示的哈希表,使用names数组中的元素填充哈希表。参数nelts为数组的大小。

//功能: 初始化哈希表,将names数组中的元素插入到哈希表中
//参数: hinit 哈希表封装结构
//		names 需要插入到哈希表中的数组元素
//		nelts 数组元素的个数
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;

	//有任何一个元素,桶的大小不够为该元素分配空间,则退出  
    for (n = 0; n < nelts; n++) 
	{
		//加上sizeof(void *)的目的是每个bucket有个结束元素,这个元素的value是个指针,指向null。
        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;
        }
    }

	//开辟临时空间,用于测试实际需要多少个bucket
	//test数组中存放每个bucket的当前容量,如果某一个key的容量大于了bucket size就意味着需要加大hash桶的个数了
    test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
    if (test == NULL) 
	{
        return NGX_ERROR;
    }

	//统计一个bucket有效空间大小,不包括最后一个null指针
    bucket_size = hinit->bucket_size - sizeof(void *);

	//(2 * sizeof(void *)表示每一个ngx_hash_elt_t结构的最小值
	//bucket_size / (2 * sizeof(void *)) 表示每一个bucket可以存放多少个ngx_hash_elt_t
	//nelts / (bucket_size / (2 * sizeof(void *))) 表示最小一共需要多少个ngx_hash_elt_t
    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从start开始,逐渐加大bucket的个数,直到恰好满足所有具有相同hash%size的元素都在同一个bucket,这样hash的size就能确定了
    for (size = start; size < hinit->max_size; size++) 
	{
		//每次循环新的size的时候需要将旧test的数据清空
        ngx_memzero(test, size * sizeof(u_short));

		//对每一个size,统计该size是否满足每一个哈希元素都尽可能分配到每一个哈希槽中
        for (n = 0; n < nelts; n++) 
		{
            if (names[n].key.data == NULL)
			{
                continue;
            }

            key = names[n].key_hash % size;
			//test数组中存放每个bucket的当前容量
            test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));

			//如果某个bucket的size超过了bucket_size,那么加大bucket的个数,使得元素分布更分散一些
            if (test[key] > (u_short) bucket_size) 
			{
                goto next;
            }
        }

        goto found;

    next:

        continue;
    }

	//循环结束都没有找到每一个可以尽可能平均分配到每一个槽中,则报错,说明max_size设置得不合理
	//最终如果size超过了max size,就报错
    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都有一个空指针
    for (i = 0; i < size; i++) 
	{
        test[i] = sizeof(void *);
    }

	//遍历每个bucket计算每个bucket的大小
    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];
    }

    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;
        }
    }

	//开辟一段连续的元素空间
    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);
	//把elts地址空间分发到每个bucket中,此时test中每一个元素表示对应槽的大小
    for (i = 0; i < size; i++) 
	{
		//如果每个槽没有需要存入的元素,则buckets[i] = NULL;
        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;
    }

	//将names数组中的值插入到哈希表中
    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);

		 //更新同一个bucket中下一个元素的位置
        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    }

	//对每一个哈希槽,如果哈希槽中有元素,则需要把该槽下的最后一个元素后加入一个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;
    }

    ngx_free(test);

	//更新哈希表结构
    hinit->hash->buckets = buckets;
    hinit->hash->size = size;
    return NGX_OK;
}


图:哈希表初始化流程

        初始化流程比较多,大体上分为流程图中的几个步骤。有个疑问? names数组有nelts个元素,那使用nelts个哈希槽构造的哈希表进行存储不就好了。何必大费周章的计算哈希槽元素的个数。这是因为names数组是由nelts个元素组成, 但有可能数组中的元素可能存在哈希冲突,这个时候只创建nelts个哈希槽就不够用了。因此为了解决哈希冲突,需要曾大哈希元素个数,来减少冲突。这个时候就需要计算哈希槽元素的个数,目的是为了尽量减少哈希元素的冲突。

        nginx使用试错法来计算需要多少个哈希槽元素。也就是对每一个哈希槽个数,都计算这个哈希表是否能满足names数组中的元素冲突最少。如果满足,这个值就为哈希槽个数。否则加大哈希槽元素个数,继续试错,直到满足条件。

三、哈希查找

使用ngx_hash_find函数可以在图:哈希表内存布局中查找到key对应的value值。查找过程比较简单,直接上代码:

//功能: 	在哈希表中按照key查找对应的value
//参数:		hash 哈希表结构
//	   		key 是根据散列函数计算出来的值
//	   		name  关键字
//	   		len   关键字长度
//返回值:	key对应的value
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;
    }

	//对key值相同的哈希槽,使用连续的数组存放了多个value值。目的是为了解决冲突,
	//则遍历这个连续的数组空间,找到value
    while (elt->value) 
	{
		//先比较长度,长度不相等,则说明key肯定不相等
        if (len != (size_t) elt->len) 
		{
            goto next;
        }

		//长度相等后,则比较key
        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;
}
需要注意的是,如果哈希存在冲突, 则需要在哈希元素数组中进行查找,这个时候时间复杂度为O(n)




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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值