一、Open vSwitch 中的哈希表
在 Open vSwitch 中存在多种类型的哈希表,如 hmap、smap、simap 和 shash 等,其中 hmap 是最基础的哈希表数据结构,而其他的哈希表都是在 hmap 的基础上进行实现的。在 Open vSwitch 中,很多内容都需要通过哈希表结构进行存储,所以各种类型的哈希表在源码中很常见。
二、hmap 的实现
基础的哈希表 hmap 的结构体和相应的方法在 ovs-main/include/openvswitch/hmap.h 头文件和 ovs-main/include/openvswitch/hmap.c 文件中进行定义和实现。(此处为了节约空间,对代码做了省略和细微调整)。
(1)hmap 哈希表结构体
/* A hash map. */
struct hmap {
struct hmap_node **buckets; /* Must point to 'one' iff 'mask' == 0. */
struct hmap_node *one;
size_t mask;
size_t n;
};
可以看到,这里使用桶(bucket)的方式来存储节点,并通过 mask 成员快速确定节点应该被存储在哪个桶中。当哈希表很小时,则会直接使用 one 成员来存储单个节点,避免了维护桶数组带来的开销。
(2)hmap 哈希节点
/* A hash map node, to be embedded inside the data structure being mapped. */
struct hmap_node {
size_t hash; /* Hash value. */
struct hmap_node *next; /* Next in linked list. */
};
由此可见,哈希节点中维护了一个单向链表,所有哈希值相同的节点会链接到一个链表中,以此来解决哈希冲突的问题(拉链法)。
所以这个 hmap 哈希表大概像下面这样:
(3)hmap 哈希表初始化
/* Initializes 'hmap' as an empty hash table. */
void hmap_init(struct hmap *hmap) {
hmap->buckets = &hmap->one;
hmap->one = NULL;
hmap->mask = 0;
hmap->n = 0;
}
(4)hmap 哈希表插入元素
快速插入元素:
/* Inserts 'node', with the given 'hash', into 'hmap'.
* 'hmap' is never expanded automatically. */
static inline void hmap_insert_fast(struct hmap *hmap, struct hmap_node *node, size_t hash) {
struct hmap_node **bucket = &hmap->buckets[hash & hmap->mask];
node->hash = hash;
node->next = *bucket;
*bucket = node;
hmap->n++;
}
正常插入元素:
/* Inserts 'node', with the given 'hash', into 'hmap', and expands 'hmap' if necessary to optimize search performance. */
static inline void hmap_insert_at(struct hmap *hmap, struct hmap_node *node, size_t hash, const char *where) {
hmap_insert_fast(hmap, node, hash);
if (hmap->n / 2 > hmap->mask) {
hmap_expand_at(hmap, where);
}
}
二者的区别在于正常插入元素后,还要额外检查当前哈希表是否需要扩容。
(5)hmap 哈希表扩容
/* Expands 'hmap', if necessary, to optimize the performance of searches. */
void hmap_expand_at(struct hmap *hmap, const char *where) {
size_t new_mask = calc_mask(hmap->n);
if (new_mask > hmap->mask) {
COVERAGE_INC(hmap_expand);
resize(hmap, new_mask, where);
}
}
扩容的目的主要是为了避免哈希表过于拥挤,导致查找效率下降。这里扩容的标准是 哈希表中的元素数量超过哈希表大小的一半。具体的扩容操作通过 resize() 函数实现:
static void resize(struct hmap *hmap, size_t new_mask, const char *where) {
struct hmap tmp;
size_t i;
ovs_assert(is_pow2(new_mask + 1));
hmap_init(&tmp);
if (new_mask) {
tmp.buckets = xmalloc(sizeof *tmp.buckets * (new_mask + 1));
tmp.mask = new_mask;
for (i = 0; i <= tmp.mask; i++) {
tmp.buckets[i] = NULL;
}
}
int n_big_buckets = 0;
int biggest_count = 0;
int n_biggest_buckets = 0;
for (i = 0; i <= hmap->mask; i++) {
struct hmap_node *node, *next;
int count = 0;
for (node = hmap->buckets[i]; node; node = next) {
next = node->next;
hmap_insert_fast(&tmp, node, node->hash);
count++;
}
if (count > 5) {
n_big_buckets++;
if (count > biggest_count) {
biggest_count = count;
n_biggest_buckets = 1;
} else if (count == biggest_count) {
n_biggest_buckets++;
}
}
}
hmap_swap(hmap, &tmp);
hmap_destroy(&tmp);
if (n_big_buckets) {
static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(10, 10);
COVERAGE_INC(hmap_pathological);
VLOG_DBG_RL(&rl, "%s: %d bucket%s with 6+ nodes, "
"including %d bucket%s with %d nodes "
"(%"PRIuSIZE" nodes total across %"PRIuSIZE" buckets)",
where,
n_big_buckets, n_big_buckets > 1 ? "s" : "",
n_biggest_buckets, n_biggest_buckets > 1 ? "s" : "",
biggest_count,
hmap->n, hmap->mask + 1);
}
}
(6)hmap 哈希表删除元素
/* Removes 'node' from 'hmap'. Does not shrink the hash table;
* call hmap_shrink() directly if desired. */
static inline void hmap_remove(struct hmap *hmap, struct hmap_node *node) {
struct hmap_node **bucket = &hmap->buckets[node->hash & hmap->mask];
while (*bucket != node) {
bucket = &(*bucket)->next;
}
*bucket = node->next;
hmap->n--;
}
(7)hmap 哈希表遍历元素:
常规遍历:
/* Iterates through every node in HMAP. */
#define HMAP_FOR_EACH(NODE, MEMBER, HMAP) \
HMAP_FOR_EACH_INIT(NODE, MEMBER, HMAP, (void) 0)
#define HMAP_FOR_EACH_INIT(NODE, MEMBER, HMAP, ...) \
for (INIT_MULTIVAR_EXP(NODE, MEMBER, hmap_first(HMAP), struct hmap_node, \
__VA_ARGS__); \
CONDITION_MULTIVAR(NODE, MEMBER, ITER_VAR(NODE) != NULL); \
UPDATE_MULTIVAR(NODE, hmap_next(HMAP, ITER_VAR(NODE))))
安全遍历:(允许在遍历的过程中删除节点)
/* Safe when NODE may be freed (not needed when NODE may be removed from the hash map but its members remain accessible and intact). */
#define HMAP_FOR_EACH_SAFE_LONG(NODE, NEXT, MEMBER, HMAP) \
HMAP_FOR_EACH_SAFE_LONG_INIT (NODE, NEXT, MEMBER, HMAP, (void) NEXT)
#define HMAP_FOR_EACH_SAFE_LONG_INIT(NODE, NEXT, MEMBER, HMAP, ...) \
for (INIT_MULTIVAR_SAFE_LONG_EXP(NODE, NEXT, MEMBER, hmap_first(HMAP), \
struct hmap_node, __VA_ARGS__); \
CONDITION_MULTIVAR_SAFE_LONG(NODE, NEXT, MEMBER, \
ITER_VAR(NODE) != NULL, \
ITER_VAR(NEXT) = hmap_next(HMAP, ITER_VAR(NODE)), \
ITER_VAR(NEXT) != NULL); \
UPDATE_MULTIVAR_SAFE_LONG(NODE, NEXT))
(8)总结
对于 Open vSwitch 而言 hmap 是最基础的哈希表结构,其他的各种类型的哈希表都是在 hmap 的基础上,增加对新的数据类型的适配,然后相应增加一些新的方法。
上述内容只列举了一些 hmap 常见的功能实现,但其实 Open vSwitch 中 hmap 实现的功能远不止于此。就我观察发现在 C++ STL 的哈希表 unordered_map 中常用的功能和算法等内容都能在 hmap 的代码中找到相应的实现。
此外,关于 Open vSwitch 为什么不采用 STL 而是选择自己手搓数据结构(如哈希表)的原因有一些猜想:首先 STL 为了提供更通用的接口,可能会牺牲一些性能,而自己实现的数据结构可以实现定制化,让数据结构更加贴合实际场景,获得更好的性能提升(缺点是代码阅读的难度也会提升);其次 STL 使用的动态内存分配机制,对于某些内存要求严格的场景不够友好,自己实现能够采用更贴近需求的内存管理策略;最后,也有可能是最开始写的大佬不想用(悲),但其实 Open vSwitch 源码的结构还是很清晰的(就是风格稍微有一点点怪),凑活读吧。
结语:
由于本人水平有限,以上内容如有不足之处欢迎大家指正(评论区/私信均可)。
参考资料:
浅析 Open vSwitch 数据结构:哈希表 hmap / smap / shash - ovs hash算法-优快云博客