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. 切勿在用户态直接复用内核链表实现

相关推荐
EMTime7 小时前
Docker运行OpenWRT
运维·docker·容器
lolo大魔王8 小时前
Linux 文件系统超全面详解(原理、结构、挂载、分区、inode、日志、管理命令)
linux·运维·服务器
磊 子9 小时前
详细讲解一下epoll
linux·io·epoll·io多路复用
printfLILEI10 小时前
php中的类与对象以及反序列化
linux·开发语言·php
zyl8372110 小时前
Docker 使用手册
运维·docker·容器
古月方枘Fry10 小时前
MGRE实验
运维·服务器
叠叠乐11 小时前
redmi k90 pro max 强解BL,刷海外rom, 并刷入sukisu ultra
linux
stolentime11 小时前
FreeDomain 本地开发环境快速搭建指南
运维·服务器·网络
xiaoye-duck12 小时前
《Linux系统编程》Linux 进程间通信之管道基础解析:从匿名管道原理到基于管道的进程池实现
linux