/vppinfa/bihash_template.h
本文件为本数据结构的核心头文件
key-value-pair 键-值-对。 组成HASH表的各元素,
在C语言中,一般定义成一个struct,其中一部分是key, 另一部分是value。
每1个key在hash表中只能出现1次
通过key用来计算hash值。不同的key可能计算出相同的hash值,如果表中存在2个这样的表项,则称这2个表项hash冲突。
hash bucket. HASH槽。主要用来组织具有相同hash值的hash表项,通过这个hash槽,可以找到此hash值的所有表项。
hash槽,也称为hash冲突链。逻辑上,可以看成具有相同hash值的表项链接在一起。
hash buckets. HASH桶,由多个HASH槽组成。一般定义成一个数组,数组下标即为对应hash槽的HASH值。
1. HASH槽数目固定。HASH槽数目初始化时固定
2. HASH链由数组实现,可动态扩展,实际内存按2的幂指数扩展
3. HASH链内再分组,再HASH(找到某个分组放新hash表项),如果无法解决冲突问题,则回退成线性方式(不分组,找一个空闲的位置放表项)
4. 多线程操作时,读操作无锁
5. 结果,兼顾了容量和性能的较优的HASH结构。
6. 对key 和 value没有特别要求,通用性强。
typedef struct BV (clib_bihash_value)
{
union
{
BVT (clib_bihash_kv) kvp[BIHASH_KVP_PER_PAGE];
u64 next_free_as_u64;
};
} BVT (clib_bihash_value);
page本意是页的意思,在这里与操作系统的内存页没有关系,只是借用了这个概念,代表一小块内存,在这块内存中,存在BIHASH_KVP_PER_PAGE个键值对。这个结构体就是用来保存键值对数据的结构体。
在后面代码中我们会看到,hash槽中,就记录了这个结构体的地址,实际上,在本算法中,每个hash槽记录了以这个结构体定义的动态数组的首地址。
这里使用了C语言联合。在本块内存(动态数组)使用后,回收的时候,不是返回给系统,而是放入空闲链,由next_free_as_u64来组织空闲链。下一次需要时,先从空闲链中获取。
typedef struct
{
union
{
struct
{
u64 offset:BIHASH_BUCKET_OFFSET_BITS;
u64 lock:1;
u64 linear_search:1;
u64 log2_pages:8;
u64 refcnt:16;
};
u64 as_u64;
};
} BVT (clib_bihash_bucket);
offseet共有36bit, 能寻址64G bytes的内存空间。这里使用offset而不是一个明确的指针,是因为BIHASH初始时直接从系统获取一块大内存,然后在此基础上再分配。这里的offset指的是相对于大内存首地址的偏移。
lock主要用于多线程对bucket的互斥访问。
log2_pages主要代表当前hash槽有几页( 1 << log2_pages) 数据。 即offset指向的内存的动态数组的大小( 如log2_pages为4时,此hash槽有16页)。
refcnt 当前hash槽中,有多少有效的键值对,假定当前有16页,每页有4个键值对,则最多可以有64个键值对,其中有效的键值对为refcnt个,其实说的就是此hash槽中有效的hash表项数目。
as_u64用于整体读写上述字段时用,在多线程编程中,方便进行原子操作。
/* *INDENT-OFF* */
typedef CLIB_PACKED (struct {
/*
* Backing store allocation. Since bihash manages its own
* freelists, we simple dole out memory starting from alloc_arena[alloc_arena_next].
*/
u64 alloc_arena_next; /* Next offset from alloc_arena to allocate, definitely NOT a constant */
u64 alloc_arena_size; /* Size of the arena */
/* Two SVM pointers stored as 8-byte integers */
u64 alloc_lock_as_u64;
u64 buckets_as_u64;
/* freelist list-head arrays/vectors */
u64 freelists_as_u64;
u32 nbuckets; /* Number of buckets */
/* Set when header valid */
volatile u32 ready;
u64 pad[2];
}) BVT (clib_bihash_shared_header);
alloc_arena_next. BIHASH内部分配内存时,依次从前往后按需切一块内存。此字段表示已分配内存和未分配内存的分界线,下一次分配内存从这里开始。这个字段是相对于整个大内存的偏移量。
alloc_arena_size. 整个大内存的大小,单位字节。
alloc_lock_as_u64. 多cpu环境下申请bihash内存需要先获取这个lock,此字段只是表面锁变量的偏移位置
buckets_as_u64 hash槽数组在大内存中的偏移位置
freelists_as_u64 空闲链数组在大内存中的偏移位置
nbuckets 当前hash槽的数目
ready表明这个结构体数据是否初始化好了。这个结构体存在的原因是,一个HASH表,在每个CPU上都有1个主结构,但这个share结构体中的字段所对应的信息只能有1份,即在所有CPU间共享。ready字段用来同步多cpu之间的初始化过程。主CPU把主结构初始化,从CPU才初始化自己的结构,共享的结构只能由主CPU初始化。
typedef struct
{
BVT (clib_bihash_bucket) * buckets;
volatile u32 *alloc_lock;
BVT (clib_bihash_value) ** working_copies;
int *working_copy_lengths;
BVT (clib_bihash_bucket) saved_bucket;
u32 nbuckets;
u32 log2_nbuckets;
u8 *name;
u64 *freelists;
#if BIHASH_32_64_SVM
BVT (clib_bihash_shared_header) * sh;
int memfd;
#else
BVT (clib_bihash_shared_header) sh;
#endif
u64 alloc_arena; /* Base of the allocation arena */
/**
* A custom format function to print the Key and Value of bihash_key instead of default hexdump
*/
format_function_t *fmt_fn;
} BVT (clib_bihash);
buckets。 hash桶的地址,即大内存块首地址+hash桶在大内存中的偏移。这个地址也是第0号hash槽所在的内存位置
alloc_lock, BIHASH的内存管理互斥锁变量所占的地址。注意这个变量就存放在大内存块内,刚从系统申请大内存块时,就需要在大内存块中取一块出来,代表这个变量。这个变量需要占用CACHE LINE大小。关于自旋锁等锁变量为何要占用cacheline 大小,请查看其他文档。
working_copies, working_copy_lengths 这2个变量分别定义了2个数组。每个元素对应一个CPU。具体作用是:当我们需要扩大hash槽时,如hash槽原来有16页,现在要扩大到32页,则把原来16页的数据先复制到一个副本中。然后将hash桶切换到副本,所有的其它读线程暂时使用副本中的数据。然后当前线程继续做扩充动作,这么做使得读线程不需要等待hash桶锁。当切换完成后,后续的读操作就操作的是新的32页数据了。
saved_bucket 用于保留hash槽切换前,旧的hash槽的值。hash槽扩充过程中使用
nbuckets 总共的hash槽数,在创建hash表时,调整成2的幂指数形式。如创建hash表时,需要创建含500个hash槽的hash表,则软件调整成含512( 1 << 9) 个hash槽的hash表。这样做的目的是提高寻找hash槽的效率(用位运算&而不是%运算就可以定位hash槽)。
log2_nbuckets, 针对上述例子 , log2_nbuckets == 9。 即等式 (1 << log2_nbuckets) == nbuckets恒成立。
name 本hash表的名称
freelists 空闲页链表首地址。此变量维护了一个空闲页链表的数组。第0号元素维护了含1页的空闲页链表。第1号元素维护了含2页的空闲页链表;第2号元素维护了含4页的空闲页链表。... 重申一下,这里的页指的是包含几个key-value-pair的实际内存空间,与操作系统的内存页大小并不一致。
#if BIHASH_32_64_SVM
BVT (clib_bihash_shared_header) * sh;
int memfd;
#else
BVT (clib_bihash_shared_header) sh;
#endif
这里的sh主要用于BIHASH内部的内存管理,之前已经消息描述了这个结构。memfd表示这个大内存块使用命名的mmap获取。
alloc_arena 指的是大内存块的首地址
fmt_fn用于hash表项的格式化输出函数,由使用者定义,在打印hash表时使用。
在读代码的过程中,发现了疑似bug, 当前使用的版本是,未来的版本可能会解决这些问题
commit 88076749e663e35925c2212eb79e2ec4ce023772 (HEAD -> master, origin/master)
Author: Mike Bly
Date: Mon Sep 24 10:13:06 2018 -0700
static inline void *BV (alloc_aligned) (BVT (clib_bihash) * h, uword nbytes)
{
uword rv;
/* Round to an even number of cache lines */
nbytes += CLIB_CACHE_LINE_BYTES - 1;
nbytes &= ~(CLIB_CACHE_LINE_BYTES - 1);
rv = alloc_arena_next (h);
alloc_arena_next (h) += nbytes;
if (rv >= alloc_arena_size (h))
os_out_of_memory ();
return (void *) (uword) (rv + alloc_arena (h));
}
在if判断中,应该用rv + nbytes来判断,而不是rv来判断,这样才知道当前这个表项是否能使用。后果,当hash表刚好耗尽时,对最后一个表项的写操作可能会内存越界。
void BV (clib_bihash_free) (BVT (clib_bihash) * h)
{
vec_free (h->working_copies);
#if BIHASH_32_64_SVM == 0
vec_free (h->freelists);
#else
if (h->memfd > 0)
(void) close (h->memfd);
#endif
clib_mem_vm_free ((void *) (uword) (alloc_arena (h)), alloc_arena_size (h));
memset (h, 0, sizeof (*h));
}
这里有释放workong_copies,但没有释放working_copy_lengths。 虽然这一小块内存不大,但时间久了,hash模块多了,还是会慢慢泄露掉内存吧。