Linux list 设计

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 成员

  • 链表仅负责维护节点之间的逻辑关系

  • 实际数据由宿主结构体承载

这种设计具有以下显著优势:

  1. 零额外 内存 开销:无需为链表节点单独分配内存

  2. 缓存友好:节点与数据位于同一 cache line 的概率更高

  3. 高通用性:同一结构体可同时挂入多个不同链表

  4. 类型无关:链表实现完全独立于具体业务数据类型

2.2 双向 + 循环 的结构选择

Linux 的 list_head 实现的是一种"双向循环 链表":

  • 双向:每个节点同时保存 nextprev

  • 循环:链表尾节点的 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)这两个名字还挺绕的

这些初始化操作都会将 nextprev 指向自身,从而构成一个合法的空循环链表。


第四章 节点插入机制

4.1 内部原语:__list_add

__list_add() 是所有插入操作的底层实现,其语义为:

将新节点插入到已知的 prevnext 之间

该函数假设调用者已经确保 prevnext 在逻辑上是相邻的。

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_HARDENEDCONFIG_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 中大量使用。


第十二章 使用规范与常见陷阱

  1. 节点必须先初始化再使用

  2. 同一节点不可同时加入同一链表两次

  3. 遍历中删除节点必须使用 *_safe 宏

  4. 并发访问必须由调用者自行加锁或使用 RCU

  5. 切勿在用户态直接复用内核链表实现

相关推荐
遇见火星2 小时前
在Linux中使用parted对大容量磁盘进行分区详细过程
linux·运维·网络·分区·parted
yuyousheng2 小时前
CentOS7更换为阿里源
linux·c语言
微露清风3 小时前
系统性学习Linux-第一讲-Linux基础指令
java·linux·学习
zl_dfq3 小时前
Linux 之 【日志】(实现一个打印日志的类)
linux
EmbedLinX3 小时前
一文理解后端核心概念:同步/异步、阻塞/非阻塞、进程/线程/协程
linux·服务器·c语言·网络
zhangrelay3 小时前
linux下如何通过与AI对话设置thinkpad电池充电阈值
linux·运维·笔记·学习
小王努力学编程3 小时前
LangChain——AI应用开发框架(核心组件2)
linux·服务器·c++·人工智能·python·langchain·信号
hqwest4 小时前
WPF真入门教程36--真硬核【自动化生产管理平台】
运维·自动化·modbus通信·串口设备·自动化生产管理平台·wpf开发
郝学胜-神的一滴4 小时前
深入理解TCP协议:数据格式与核心机制解析
linux·服务器·网络·c++·网络协议·tcp/ip