Linux 内核链表(list.h)机制详解
第一章 引言
在 Linux 内核中,链表是一种极其核心且高频使用的数据结构。无论是进程调度、设备管理、内存管理、网络协议栈,还是驱动与子系统内部的数据组织,都大量依赖链表完成对象的组织、遍历与维护。Linux 内核并未直接使用标准 C 语言教材中的"链表节点 + 数据域"模式,而是设计了一套高度通用、零额外内存开销、与宿主结构体深度融合的链表基础设施,其核心定义位于头文件 include/linux/list.h 中。
本文将以当前主线内核中的 list.h 为蓝本,对 Linux 双向循环链表(list)与单向哈希链表(hlist)的设计理念、数据结构、核心 API、并发与内存语义、安全加固机制以及典型使用模式进行系统性、深入、书面化的介绍。
第二章 Linux 链表设计理念
2.1 内嵌式链表(Intrusive List)思想
Linux 内核链表采用"内嵌式链表"设计,即链表节点并不单独分配,而是作为成员直接嵌入到宿主结构体中。典型形式如下:
-
用户自定义结构体包含一个
struct list_head成员 -
链表仅负责维护节点之间的逻辑关系
-
实际数据由宿主结构体承载
这种设计具有以下显著优势:
-
零额外 内存 开销:无需为链表节点单独分配内存
-
缓存友好:节点与数据位于同一 cache line 的概率更高
-
高通用性:同一结构体可同时挂入多个不同链表
-
类型无关:链表实现完全独立于具体业务数据类型
2.2 双向 + 循环 的结构选择
Linux 的 list_head 实现的是一种"双向循环 链表":
-
双向:每个节点同时保存
next与prev -
循环:链表尾节点的
next指向头节点,头节点的prev指向尾节点
循环结构带来的核心收益包括:
-
不需要
NULL作为终止条件 -
插入、删除操作逻辑高度统一
-
空链表与非空链表状态判断简单可靠
第三章 基础数据结构定义
3.1 struct list_head

该结构本身不存储任何业务数据,仅用于维护链表关系。
3.2 链表头与空链表
Linux 中"链表头"本身也是一个 struct list_head,并不区分"头节点"与"普通节点"。
-
空链表的判定条件:
-
head->next == head -
head->prev == head
-
3.3 初始化宏与接口
-
静态初始化:
-
LIST_HEAD(name) -
LIST_HEAD_INIT(name)
-
-
动态初始化:
INIT_LIST_HEAD(struct list_head *list)这两个名字还挺绕的
这些初始化操作都会将 next 与 prev 指向自身,从而构成一个合法的空循环链表。

第四章 节点插入机制
4.1 内部原语:__list_add
__list_add() 是所有插入操作的底层实现,其语义为:
将新节点插入到已知的
prev与next之间
该函数假设调用者已经确保 prev 与 next 在逻辑上是相邻的。

4.2 头插与尾插
在 __list_add() 之上,Linux 提供了两种最常用的插入接口:
-
list_add(new, head):-
插入到
head之后 -
逻辑上为"头插法"
-
适合栈(LIFO)语义
-
-
list_add_tail(new, head):-
插入到
head之前 -
逻辑上为"尾插法"
-
适合队列(FIFO)语义
-
4.3 插入过程的顺序保证
在 SMP 与弱内存模型架构下,Linux 使用 WRITE_ONCE``() 等原语,确保指针更新的可见性与一致性,避免编译器或 CPU 重排导致的中间状态被其他 CPU 观察到。
第五章 节点删除与替换
5.1 基本删除流程
删除操作的本质是:
-
让
prev->next指向next -
让
next->prev指向prev
底层实现由 __list_del() 完成。

5.2 list_del 与 list_del_init
-
list_del(entry):

-
将节点从链表中移除
-
使用
LIST_POISON填充指针 -
有助于在调试阶段快速发现 UAF 或重复删除错误
-
-
list_del_init(entry):
-
删除节点后重新初始化
-
使该节点重新成为一个"空链表"
-
5.3 节点替换与交换
-
list_replace(old, new):
- 用新节点替换旧节点在链表中的位置
-
list_swap(entry1, entry2):
- 交换两个节点在链表中的逻辑位置
这些操作常用于调度队列、LRU 链表等场景。
第六章 链表状态判断接口
Linux 提供了一系列高可读性的辅助接口:
-
list_empty(head):判断链表是否为空 -
list_is_first(entry, head):是否为首元素 -
list_is_last(entry, head):是否为尾元素 -
list_is_singular(head):是否仅包含一个节点

此外,还存在面向并发场景的:
list_empty_careful()
用于在特定约束条件下,配合内存屏障判断链表是否为空。

第七章 链表遍历宏体系
7.1 基础遍历(struct list_head 级别)
list_for_each(pos, head)

list_for_each_prev(pos, head)

7.2 安全遍历(允许删除当前节点)
-
list_for_each_safe(pos, n, head) -
list_for_each_prev_safe(pos, n, head)
安全遍历通过提前保存下一个节点指针,避免当前节点删除后指针失效。

7.3 类型感知遍历(最常用)
-
list_for_each_entry(pos, head, member) -
list_for_each_entry_reverse(pos, head, member)

这些宏通过 container_of(),直接返回宿主结构体指针,是内核代码中最常见的用法。
第八章 container_of 与类型恢复机制
8.1 container_of 的基本原理
container_of(ptr, type, member) 的核心思想是:
-
已知结构体成员地址
-
已知该成员在结构体中的偏移
-
通过地址减偏移,反推出结构体起始地址

8.2 list_entry 家族宏
Linux 在 container_of 基础上封装了:
-
list_entry() -
list_first_entry() -
list_last_entry()
用于在遍历与访问中提高代码可读性与安全性。
第九章 链表拼接、裁剪与旋转
9.1 链表拼接(splice)
-
list_splice() -
list_splice_tail() -
list_splice_init()
这些接口可在 O(1) 时间内完成整段链表的合并,广泛用于批量任务迁移、队列合并等场景。
9.2 链表裁剪(cut)
-
list_cut_position() -
list_cut_before()
可将一个链表拆分为两个独立链表,而无需逐节点操作。
9.3 链表旋转
-
list_rotate_left() -
list_rotate_to_front()
常用于调度公平性与轮询算法中。
第十章 链表完整性校验与安全加固
10.1 CONFIG_LIST_HARDENED
在开启 CONFIG_LIST_HARDENED 或 CONFIG_DEBUG_LIST 时:
-
插入与删除操作会进行一致性校验
-
检测到链表损坏会触发 WARN 或 BUG
-
有助于尽早发现内存越界、并发破坏等问题
10.2 LIST_POISON 机制
删除节点后将指针置为非法地址:
-
提高 UAF 错误的可观测性
-
减少 silent data corruption 风险
第十一章 HLIST:单向哈希链表
11.1 设计动机
hlist 主要用于哈希表桶中:
-
仅在头节点保存指针
-
节省一半指针空间
-
支持 O(1) 删除(通过 pprev)
11.2 核心结构

其中 pprev 是指向"前驱 next 指针"的指针,这是 hlist 的关键设计。
11.3 常用操作接口
-
hlist_add_head() -
hlist_del()/hlist_del_init() -
hlist_for_each_entry()
hlist 在网络协议栈、inode cache、dentry cache 中大量使用。

第十二章 使用规范与常见陷阱
-
节点必须先初始化再使用
-
同一节点不可同时加入同一链表两次
-
遍历中删除节点必须使用 *_safe 宏
-
并发访问必须由调用者自行加锁或使用 RCU
-
切勿在用户态直接复用内核链表实现
