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 实现的是一种"双向循环 链表":

  • 双向:每个节点同时保存 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 中大量使用。


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

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

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

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

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

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

相关推荐
Sammyyyyy3 分钟前
jev-ultrafast 实战:浏览器操作代理加本地 AI 网关,把云控制台配置做成自动化流水线
运维·人工智能·自动化·ai编程·servbay
灯澜忆梦9 分钟前
【Linux】#5 | 文件与目录基础操作命令
java·linux·运维
逐梦之程10 分钟前
EXCEL与DBC格式互转软件
运维·服务器·网络
yychen_java13 分钟前
第十二篇:完整项目实战——构建可验证的能源智能运维 Agent 中枢
运维·人工智能·能源
96245617 分钟前
餐饮 SaaS 优惠券系统架构演进(七)营销定价引擎设计:活动、会员价、优惠券、买赠如何在同一 PriceQuote 中可解释地组合
java·运维·算法
Frank_refuel34 分钟前
分布式IoT数据采集与存储系统(项目复盘)
运维·服务器
wuminyu36 分钟前
stackChunkOop内部结构与GC屏障的配合工作原理剖析
java·linux·c语言·jvm·c++
青衫客3636 分钟前
从网卡到 Socket:再探 Linux 网络协议栈与 iptables 数据包处理流程
linux·网络·网络协议
承渊政道41 分钟前
Linux网络学习【TCP Socket编程实战:connect的断线重连与客户端访问Linux验证】
linux·网络·学习·ubuntu·tcp·socket编程
sunoo-22944 分钟前
嵌入式Linux驱动学习笔记(第3-6天):从misc封装、platform总线到设备树、中断与单总线外设实战
linux·arm开发·驱动开发·笔记·单片机·学习