hlist浅析

本文详细介绍了hlist哈希链表在Linux内核中的实现与使用方法,包括其与普通链表的区别、结构定义、宏定义以及常用操作函数。通过对比分析,揭示了hlist在空间效率方面的优势,并提供了实际应用场景的指导。

hlist浅析

 

hlist哈希链表是内核中常用的一个数据结构,由于它不同于普通的链表,所以这里对hlist哈希链表进行一下浅析,很多也是参考的网友和书上的资料希望对大家有所帮助。
在include/Linux/list.h中有list链表与hlist哈希链表结构的定义,下面都列出它们的定义,可以对比一下:

 

    双头(next,prev)的双链表对于Hash表来说“过于浪费”,因而另行设计了一套Hash表专用的hlist数据结构——单指针表头双循环链表,hlist的表头仅有一个指向首节点的指针,而没有指向尾节点的指针,这样在可能是海量的Hash表中存储的表头就能减少一半的空间消耗。
     pprev因为hlist不是一个完整的循环链表而不得不使用。在list中,表头和节点是同一个数据结构,直接用prev没问题;在hlist中,表头没有prev,也没有next,只有一个first。为了能统一地修改表头的first指针,即表头的first指针必须修改指向新插入的节点, hlist就设计了pprev。hlist节点的pprev不再是指向前一个节点的指针,而是指向前一个节点(可能是表头)中的next(对于表头则是 first)指针(struct hlist_node  **pprev),从而在表头插入的操作可以通过一致的 "*(node->pprev)" 访问和修改前节点的next(或first)指针。
注:pprev是二级指针指向前一个节点中的next指针,next是指向hlist_node的指针,所以pprev是一个指向hlist_node的指针的指针。
     hlist链表结构如下图:

hlist结构示意

可以发现,表头只是指向了仅有一个指向首节点的指针,而没有指向尾节点的指针,这样在可能是海量的hash表中存储的表头就能减少一半的空间消耗。

下面是hlist中常用的几个宏,和几个函数:

 

`struct hlist_node` 是 Linux 内核中用于实现 **哈希链表(Hash List)** 的核心数据结构,它是内核链表家族的一部分,专为 **哈希表桶(bucket)中的链表头设计的节省空间的双向链表节点**。 --- ## ✅ 为什么需要 `hlist_node`?普通 `list_head` 不够用吗? ### 对比:`struct list_head` vs `struct hlist_node` | 特性 | `list_head`(通用双向链表) | `hlist_node`(哈希链表) | |------|-------------------------------|---------------------------| | 结构大小 | 两个指针:`next`, `prev`(16 字节 x86_64) | 使用技巧减少 `prev` 开销(仅 12 字节) | | 是否支持双端链表 | 是 | 是(但 `prev` 是指针的指针) | | 主要用途 | 普通双向链表(如进程列表) | 哈希表桶内链表(大量小链表场景) | | 内存效率 | 较低 | 更高(尤其当作为第一个成员时) | > 💡 设计目标:在哈希表这种“有很多短链表”的场景下,节省每个节点的内存开销。 --- ## ✅ `struct hlist_node` 定义(来自 `<linux/types.h>`) ```c struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; }; ``` ### 成员解释: | 成员 | 类型 | 含义 | |------|------|------| | `next` | `struct hlist_node *` | 指向下一个节点 | | `pprev` | `struct hlist_node **` | 指向“前一个节点的 next 指针”或 `hlist_head.first` 的地址 | > 🧠 关键点:`pprev` 不是指向前一个节点本身,而是指向那个节点的 `next` 指针! > 这样可以让 `hlist_head` `hlist_node` 共享相同的指针类型。 --- ## 🎯 内存布局图解 假设我们有一个哈希桶: ``` hlist_head bucket[5] = { .first = NULL }; ``` 插入两个 FDB 条目后: ``` bucket[i] ↓ .first → fdb1 → fdb2 → NULL ↑ ↑ &fdb1 &fdb2 .hlist .hlist ``` 具体指针关系: ``` bucket[i].first → &fdb1->hlist fdb1->hlist.next → &fdb2->hlist fdb1->hlist.pprev → &bucket[i].first fdb2->hlist.next → NULL fdb2->hlist.pprev → &fdb1->hlist.next ``` 这样就能实现: - 正向遍历:通过 `.next` - 反向修改:通过 `*pprev = new_node` 实现删除/插入 --- ## 🔁 常用操作宏(定义于 `<linux/list.h>`) ### 1. 遍历链表(RCU 安全) ```c hlist_for_each_entry(f, head, hlist) ``` - `f`: 当前结构体指针(如 `struct vxlan_fdb *`) - `head`: `struct hlist_head *`,即桶头 - `hlist`: 节点字段名(即 `f->hlist`) 展开后大致等价于: ```c struct hlist_node *node; for (node = (head)->first; node; node = node->next) { f = container_of(node, struct vxlan_fdb, hlist); ... } ``` --- ### 2. RCU 安全遍历(用于数据面查表) ```c hlist_for_each_entry_rcu(f, head, hlist) ``` - 用于不加锁的并发读取(例如 VXLAN 收包线程查找 FDB) - 必须在 `rcu_read_lock()` 保护下使用 --- ### 3. 插入头部 ```c hlist_add_head_rcu(&f->hlist, &vxlan->fdb_head[hash]); ``` - 将 `f->hlist` 插入到 `head->first` 位置 - `_rcu` 版本确保发布安全(其他 CPU 能正确看到新条目) --- ### 4. 删除节点 ```c hlist_del_rcu(&f->hlist); ``` - 从链表中移除,但内存不能立即释放(可能有 RCU 读者正在访问) - 应配合 `kfree_rcu(f, rcu)` 延迟释放 --- ## 🧱 在 VXLAN 中的实际应用示例 ```c struct vxlan_fdb { struct hlist_node hlist; // 用于接入 FDB 哈希表 u8 eth_addr[ETH_ALEN]; __be32 vni; struct list_head remotes; // 多宿主远端(另一个链表) ... }; ``` 插入时: ```c hlist_add_head_rcu(&f->hlist, vxlan_fdb_head(vxlan, mac, vni)); ``` 查找时: ```c hlist_for_each_entry_rcu(f, head, hlist) { if (ether_addr_equal(mac, f->eth_addr) && f->vni == vni) return f; } ``` --- ## ✅ 优势总结 | 优点 | 说明 | |------|------| | **节省内存** | `hlist_node` 比 `list_head` 少一个指针空间(当作为首字段时) | | **兼容哈希表头** | `hlist_head.first` `hlist_node.next` 类型一致,简化代码 | | **支持 RCU** | 提供 `_rcu` 系列操作,适合高性能网络子系统 | | **高效插入删除** | 所有操作 O(1) | --- ## ❗️注意事项 1. **不能像 `list_head` 那样简单反向遍历** - 因为 `pprev` 是二级指针,无法直接获取前驱节点 - 通常只用于单向遍历 + 快速删除 2. **必须配合 `container_of` 使用** - 要从 `hlist_node *` 获取宿主结构体: ```c f = container_of(node, struct vxlan_fdb, hlist); ``` 3. **RCU 使用必须规范** - 写操作需 `rtnl_lock()` - 读操作需 `rcu_read_lock()` 或在软中断上下文中 --- ## ✅ 总结 `struct hlist_node` 是 Linux 内核为 **哈希表优化设计的轻量级双向链表节点**,它通过巧妙使用 `**pprev` 减少了内存占用,特别适用于像 VXLAN FDB、邻居表、ARP 表这类“大量短链表”的场景。 它是内核高效转发平面的重要基石之一。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值