Linux 内核环形双向链表完全指南:从 `<linux/list.h>` 声明到并发安全的实战解析
2026/9/15 21:38:15 网站建设 项目流程

Linux 内核环形双向链表完全指南:从<linux/list.h>声明到并发安全的实战解析

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

导读

链表是内核中应用最广泛的基础数据结构之一,而 Linux 内核实现的环形双向链表(circular doubly-linked list)以struct list_head内嵌进业务结构体的设计闻名。本文以内核官方文档 Documentation/core-api/list.rst 为主体,结合 include/linux/list.h 的源码实现,系统讲解节点的声明与初始化、增删改查、遍历、切割、移动、旋转、交换、拼接等全部核心操作,并延伸至list_private私有链表 API、RCU 并发保护与CONFIG_DEBUG_LIST调试机制。读完本文,你将能独立在内核驱动或模块中正确使用这套链表 API,并理解其「内嵌节点 + container_of」设计的底层原理。

1. 内核链表家族与本文范围

内核实现了多种链表风味,包括环形双向链表(struct list_head,见 include/linux/types.h#L206-L208)、单向哈希链表 hlist、以及配套 RCU 语义的list_*_rcu()变体。本文面向新内核开发者,目标是教会如何使用内核现成的链表实现,而不是泛泛讲解链表理论。

需要特别提醒的是:链表虽然无处不在,但在数组即可胜任的场景下很少是最优选择。由于数据局部性差(poor data locality),在性能敏感的场景下链表往往是坏选择。文档建议开发者熟悉内核中其他通用数据结构,尤其是面向并发访问的场景(如xarrayrbtreehlist等)。这一定位决定了:链表 API 用于「管理一组对象之间的有序关系」,而非「追求极致吞吐」。

2. 环形双向链表设计:内嵌节点与 container_of

2.1 链表本身没有独立类型

先看最底层的定义(include/linux/types.h#L206-L208):

struct list_head { struct list_head *next, *prev; };

一个关键认知点是:链表本身没有自己的类型。「整条链表」的概念与「指向链表中其他条目的struct list_head成员」是同一回事。空链表即头节点自指:

struct clown_car { int tyre_pressure[4]; struct list_head clowns; /* 看起来像个节点! */ };

2.2 为什么是内嵌而不是指针

经典的教科书式链表是「节点结构体内含 payload 和前后指针」,而 Linux 反其道而行:struct list_head成员不是指针,而是数据结构的组成部分。这样做的根本原因在于:链表操作代码可以完全与「列表中装的是什么结构体」解耦,实现真正的泛型(generic)。

因为节点内嵌在业务结构体中,链表实现借助container_of()模式(定义于 include/linux/container_of.h,被 include/linux/list.h#L5 引入)从list_head指针反推出宿主结构体指针,从而在完全不知道 payload 类型的情况下访问到 payload。list_entry()宏就是container_of的封装(include/linux/list.h#L647-L648):

#define list_entry(ptr, type, member) \ container_of(ptr, type, member)

3. 声明节点与初始化链表

3.1 声明一个节点

在希望放入链表的数据结构中添加一个struct list_head成员即可:

struct clown { unsigned long long shoe_size; const char *name; struct list_head node; /* 链表节点成员 */ };

3.2 声明并初始化链表头

双向链表可以声明为另一个struct list_head,在初始赋值时用LIST_HEAD_INIT()宏,或在运行时用INIT_LIST_HEAD()函数:

struct clown_car { int tyre_pressure[4]; struct list_head clowns; /* 看起来像个节点! */ }; /* ... 在驱动中稍后的位置 ... */ static int circus_init(struct circus_priv *circus) { struct clown_car other_car = { .tyre_pressure = {10, 12, 11, 9}, .clowns = LIST_HEAD_INIT(other_car.clowns) }; INIT_LIST_HEAD(&circus->car.clowns); return 0; }

源码实现(include/linux/list.h#L27-L55):

#define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name) static inline void INIT_LIST_HEAD(struct list_head *list) { WRITE_ONCE(list->next, list); WRITE_ONCE(list->prev, list); }
  • LIST_HEAD_INIT(name):把nextprev都指向自身,用于编译期/结构体初始化
  • LIST_HEAD(name):直接定义一个已初始化的链表头变量;
  • INIT_LIST_HEAD():运行期初始化,用WRITE_ONCE保证并发可见性;若用于链表头,结果就是一条空链表;
  • 此外还有LIST_HEAD_GUARDED(name, lock)(include/linux/list.h#L41-L42),用__guarded_by()标注出保护该链表的锁,供内核的锁静态分析(如 sparse/Clang)校验。

4. 添加节点:list_add 与 list_add_tail

4.1 两个插入方向的语义

内核提供两个方向相反的插入宏:

  • list_add(new, head):把new插入到head之后(即成为第一个元素),适合实现(后进先出);
  • list_add_tail(new, head):把new插入到head之前(即成为最后一个元素),适合实现队列(先进先出)。

底层都收敛到__list_add()(include/linux/list.h#L165-L207):

static __always_inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { if (!__list_add_valid(new, prev, next)) return; next->prev = new; new->next = next; new->prev = prev; WRITE_ONCE(prev->next, new); } static __always_inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); /* 插入 head 之后 */ } static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); /* 插入 head 之前 */ }

注意__list_add()先调用__list_add_valid()做链表完整性校验(详见第 13 节),这是内核链表「带校验的实用主义」设计的体现。

4.2 小丑车示例与状态演变

回到文档的经典「马戏团小丑车」示例:

static int circus_fill_car(struct circus_priv *circus) { struct clown_car *car = &circus->car; struct clown *grock; struct clown *dimitri; /* State 1:空链表 */ grock = kzalloc_obj(*grock); if (!grock) return -ENOMEM; grock->name = "Grock"; grock->shoe_size = 1000; /* 注意:添加的是 "node" 成员 */ list_add(&grock->node, &car->clowns); /* State 2 */ dimitri = kzalloc_obj(*dimitri); if (!dimitri) return -ENOMEM; dimitri->name = "Dimitri"; dimitri->shoe_size = 50; list_add(&dimitri->node, &car->clowns); /* State 3 */ return 0; }

以下示意图只画出前向边(forward edges)以保持清晰。State 1 中,唯一的 "clowns" 节点指向自身:

.------. v | .--------. | | clowns |--' '--------'

State 2 中 Grock 被加到链表头之后:

.--------------------. v | .--------. .-------. | | clowns |---->| Grock |--' '--------' '-------'

State 3 中 Dimitri 又被list_add到链表头之后(栈式插入,最新者排最前):

.------------------------------------. v | .--------. .---------. .-------. | | clowns |---->| Dimitri |---->| Grock |--' '--------' '---------' '-------'

若希望 Dimitri 插到队尾,改用list_add_tail()

list_add_tail(&dimitri->node, &car->clowns);

结果(队列式插入,最老者排最前):

.------------------------------------. v | .--------. .-------. .---------. | | clowns |---->| Grock |---->| Dimitri |--' '--------' '-------' '---------'

由于链表是环形的,「从链表头出发向后走一步即队尾元素」这一直觉在list_add_tail场景下正好成立——因为环形结构里head->prev就是最后一个元素。

5. 遍历链表:list_for_each 与 list_for_each_entry

5.1 基础遍历与 list_entry

list_for_each()遍历所有节点,配合list_entry()取出 payload:

static unsigned long long circus_get_max_shoe_size(struct circus_priv *circus) { unsigned long long res = 0; struct clown *e; struct list_head *cur; list_for_each(cur, &circus->car.clowns) { e = list_entry(cur, struct clown, node); if (e->shoe_size > res) res = e->shoe_size; } return res; }

list_entry()内部就是前面提到的container_of()——根据节点成员指针反推宿主结构体实例。

5.2 更优雅的 list_for_each_entry

上面代码略显笨拙:游标类型是struct list_head *,每次循环都要多做一次list_entry()转换。内核提供了list_for_each_entry()直接以 payload 类型为游标:

static unsigned long long circus_get_max_shoe_size(struct circus_priv *circus) { unsigned long long res = 0; struct clown *e; list_for_each_entry(e, &circus->car.clowns, node) { if (e->shoe_size > res) res = e->shoe_size; } return res; }

宏的实现(include/linux/list.h#L834-L837):

#define list_for_each_entry(pos, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member); \ !list_entry_is_head(pos, head, member); \ pos = list_next_entry(pos, member))

它把「取第一个元素 → 判断是否回到链表头 → 取下一个元素」三步封装好,member参数告诉宏:payload 结构体中哪个成员是链表节点,从而能在结构体间行走。配套工具还有:

  • list_first_entry()/list_last_entry():取首/尾元素(要求链表非空);
  • list_first_entry_or_null():空链表时返回 NULL(include/linux/list.h#L680-L684);
  • list_next_entry()/list_prev_entry():取下一个/上一个元素;
  • list_for_each_entry_reverse():反向遍历;
  • list_for_each_entry_continue()/list_for_each_entry_from():从当前位置继续遍历;
  • list_entry_is_head():判断某个 payload 是否就是链表头(遍历终止条件)。

6. 删除节点:list_del、毒化指针与安全遍历

6.1 list_del 会「毒化」指针

list_del()删除指定条目,并且会把该条目的prevnext指针毒化(poison),使删除后的误用不会被忽视:

static inline void list_del(struct list_head *entry) { __list_del_entry(entry); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; }

(实现见 include/linux/list.h#L273-L278,毒化值定义在 include/linux/poison.h。)注意list_empty()对被删除的条目不会返回 true,条目处于未定义状态——这是刻意设计的陷阱,防止你在删除后继续误用该节点。

回到小丑车示例:

list_add(&dimitri->node, &car->clowns); /* State 3 */ list_del(&dimitri->node); /* State 4 */

结果是 Dimitri 变成「孤立节点」,指针指向毒化值而非自身:

.--------------------. v | .--------. .-------. | .---------. | clowns |---->| Grock |--' | Dimitri | '--------' '-------' '---------'

6.2 list_del_init:删除后重新自指

如果希望被删除的节点重新指向自身(像空链表头一样),改用list_del_init()(include/linux/list.h#L331-L335):

static inline void list_del_init(struct list_head *entry) { __list_del_entry(entry); INIT_LIST_HEAD(entry); }

示例结果:Dimitri 再次自指,随时可以重新加入任意链表。

.--------------------. .-------. v | v | .--------. .-------. | .---------. | | clowns |---->| Grock |--' | Dimitri |--' '--------' '-------' '---------'

6.3 遍历中删除:safe 系列宏

在遍历过程中直接删除当前条目会出问题:删除会改写当前条目的next指针,导致遍历无法正确前进到下一个条目。解决方案是list_for_each_safe()list_for_each_entry_safe(),它们多出一个临时存储参数,提前保存下一个条目:

static void circus_eject_insufficient_clowns(struct circus_priv *circus) { struct clown *e; struct clown *n; /* 安全迭代用的临时存储 */ list_for_each_entry_safe(e, n, &circus->car.clowns, node) { if (e->shoe_size < 500) list_del(&e->node); } }

list_for_each_entry_safe的实现(include/linux/list.h#L921-L925)在进入循环时就预先算好n = list_next_entry(pos, member),循环体删除pos也不影响前进。文档特别提醒:被删除节点(如上面分配的struct clown)的内存回收要由调用者负责,确保没有其他引用再指向它。

7. 切割链表:list_cut_position 与 list_cut_before

两个切割辅助函数都从链表head中取出元素,填入链表listlist中原有内容会被破坏,使用时需传入空链表或不关心的链表)。

假设初始链表为:

.----------------------------------------------------------------. v | .--------. .-------. .---------. .-----. .---------. | | clowns |---->| Grock |---->| Dimitri |---->| Pic |---->| Alfredo |--' '--------' '-------' '---------' '-----' '---------'

7.1 list_cut_position:含 entry 本身

list_cut_position()把从head开始entry(含)为止的所有条目搬入list

static void circus_retire_clowns(struct circus_priv *circus) { struct list_head retirement = LIST_HEAD_INIT(retirement); struct clown *grock, *dimitri, *pic, *alfredo; struct clown_car *car = &circus->car; /* ... clown 初始化与加入链表 ... */ list_cut_position(&retirement, &car->clowns, &pic->node); /* State 1 */ }

结果——car->clowns只剩 Alfredo:

.----------------------. v | .--------. .---------. | | clowns |---->| Alfredo |--' '--------' '---------'

retirement链表则变成 Grock → Dimitri → Pic:

.--------------------------------------------------. v | .------------. .-------. .---------. .-----. | | retirement |---->| Grock |---->| Dimitri |---->| Pic |--' '------------' '-------' '---------' '-----'

7.2 list_cut_before:不含 entry 本身

list_cut_before()语义类似,但在entry之前切断,即搬走从head起到entry(不含)为止的条目:

list_cut_before(&retirement, &car->clowns, &pic->node);

结果——car->clowns变成 Pic → Alfredo:

.----------------------------------. v | .--------. .-----. .---------. | | clowns |---->| Pic |---->| Alfredo |--' '--------' '-----' '---------'

retirement链表则是 Grock → Dimitri:

.--------------------------------------. v | .------------. .-------. .---------. | | retirement |---->| Grock |---->| Dimitri |--' '------------' '-------' '---------'

实现上,list_cut_position()entry == head、空链表、单元素链表等边界做了特判(include/linux/list.h#L527-L538),list_cut_before()也有head->next == entry时的短路处理(include/linux/list.h#L554-L568)。

8. 移动条目与批量移动

8.1 list_move 与 list_move_tail

list_move()/list_move_tail()把条目从一条链表搬到另一条链表的头部/尾部

static inline void list_move(struct list_head *list, struct list_head *head) { __list_del_entry(list); list_add(list, head); } static inline void list_move_tail(struct list_head *list, struct list_head *head) { __list_del_entry(list); list_add_tail(list, head); }

(include/linux/list.h#L342-L358)本质就是「先删后插」。

沿用文档的双链表示例,初始状态 State 0 为:

.----------------------------------------------------------------. v | .--------. .-------. .---------. .-----. .---------. | | clowns |---->| Grock |---->| Dimitri |---->| Pic |---->| Alfredo |--' '--------' '-------' '---------' '-----' '---------' .-------------------. v | .----------. .-----. | | sidewalk |---->| Pio |--' '----------' '-----'

执行:

list_move(&pic->node, &sidewalk); /* State 1 */ list_move_tail(&dimitri->node, &sidewalk); /* State 2 */

State 1(Pic 移到 sidewalk 头部,即 Pio 之前):

.-----------------------------------------------------. | | v | .--------. .-------. .---------. .---------. | | clowns |---->| Grock |---->| Dimitri |---->| Alfredo |--' '--------' '-------' '---------' '---------' .-------------------------------. v | .----------. .-----. .-----. | | sidewalk |---->| Pic |---->| Pio |--' '----------' '-----' '-----'

State 2(Dimitri 移到 sidewalk 尾部,即 Pio 之后):

.-------------------------------------. | | v | .--------. .-------. .---------. | | clowns |---->| Grock |---->| Alfredo |--' '--------' '-------' '---------' .-----------------------------------------------. v | .----------. .-----. .-----. .---------. | | sidewalk |---->| Pic |---->| Pio |---->| Dimitri |--' '----------' '-----' '-----' '---------'

8.2 list_bulk_move_tail:批量搬移到队尾

只要源链表头与目标链表头属于同一条链表,就能用list_bulk_move_tail(head, first, last)[first, last]闭区间内的整段元素一次搬移到链表尾部(include/linux/list.h#L369-L381):

list_bulk_move_tail(&sidewalk, &pic->node, &pio->node);

State 3 的 sidewalk 链表变为 Dimitri → Pic → Pio:

.-----------------------------------------------. v | .----------. .---------. .-----. .-----. | | sidewalk |---->| Dimitri |---->| Pic |---->| Pio |--' '----------' '---------' '-----' '-----'

重要警告list_bulk_move_tail()不做任何校验,它假定三个struct list_head *参数确实属于同一条链表。如果在文档约束之外使用它,结果「是你与实现之间的事」——即可能破坏链表结构,后果自负。

9. 旋转链表:list_rotate_left 与 list_rotate_to_front

把链表当队列用时,旋转(把队首元素送到队尾)是常见写操作。内核提供两个旋转函数:

  • list_rotate_left(head):把head之后的第一个条目移到队尾。由于环形结构,整个链表实质上前移一位,像自行车链条一样循环(include/linux/list.h#L464-L472,实现就是list_move_tail(first, head));
  • list_rotate_to_front(list, head):持续旋转直到指定条目成为新的队首(include/linux/list.h#L481-L490,实现为list_move_tail(head, list))。

初始状态 State 0:

.-----------------------------------------------------------------. v | .--------. .-------. .---------. .-----. .---------. .-----. | | clowns |-->| Grock |-->| Dimitri |-->| Pic |-->| Alfredo |-->| Pio |-' '--------' '-------' '---------' '-----' '---------' '-----'

执行:

list_rotate_left(&car->clowns); /* State 1 */ list_rotate_to_front(&alfredo->node, &car->clowns); /* State 2 */

State 1(Grock 被转到队尾):

.-----------------------------------------------------------------. v | .--------. .---------. .-----. .---------. .-----. .-------. | | clowns |-->| Dimitri |-->| Pic |-->| Alfredo |-->| Pio |-->| Grock |-' '--------' '---------' '-----' '---------' '-----' '-------'

State 2(Alfredo 之前的条目被循环到队尾,Alfredo 成为队首):

.-----------------------------------------------------------------. v | .--------. .---------. .-----. .-------. .---------. .-----. | | clowns |-->| Alfredo |-->| Pio |-->| Grock |-->| Dimitri |-->| Pic |-' '--------' '---------' '-----' '-------' '---------' '-----'

从两张图可以清楚看到:list_rotate_to_front()把 Alfredo 前面的所有条目整体平移到队尾。

10. 交换条目:list_swap

list_swap(entry1, entry2)交换两个条目的位置(include/linux/list.h#L315-L325):

static inline void list_swap(struct list_head *entry1, struct list_head *entry2) { struct list_head *pos = entry2->prev; list_del(entry2); list_replace(entry1, entry2); if (pos == entry1) pos = entry2; list_add(entry1, pos); }

初始状态 State 0:

.-----------------------------------------. v | .--------. .-------. .---------. .-----. | | clowns |-->| Grock |-->| Dimitri |-->| Pic |-' '--------' '-------' '---------' '-----'

执行list_swap(&dimitri->node, &pic->node)后,Dimitri 与 Pic 互换位置:

.-----------------------------------------. v | .--------. .-------. .-----. .---------. | | clowns |-->| Grock |-->| Pic |-->| Dimitri |-' '--------' '-------' '-----' '---------'

实现内部还顺带展示了list_replace()(把 old 条目整体替换为 new)的用法,以及针对pos == entry1的相邻交换特判。

11. 拼接两条链表:list_splice 与 list_splice_init

假设两班小丑在并购后需要合并,初始 State 0 有两条链表 "knie" 与 "stey":

.-----------------------------------------. | | v | .------. .-------. .---------. .-----. | | knie |-->| Grock |-->| Dimitri |-->| Pic |--' '------' '-------' '---------' '-----' .-----------------------------. v | .------. .---------. .-----. | | stey |-->| Alfredo |-->| Pio |--' '------' '---------' '-----'
static void circus_clowns_splice(void) { struct clown *grock, *dimitri, *pic, *alfredo, *pio; struct list_head knie = LIST_HEAD_INIT(knie); struct list_head stey = LIST_HEAD_INIT(stey); /* ... 小丑分配与初始化 ... */ list_add_tail(&grock->node, &knie); list_add_tail(&dimitri->node, &knie); list_add_tail(&pic->node, &knie); list_add_tail(&alfredo->node, &stey); list_add_tail(&pio->node, &stey); /* State 0 */ list_splice(&stey, &dimitri->node); /* State 1 */ }

list_splice(&stey, &dimitri->node)把 stey 的所有条目插入到 Dimitri 之后。结果如下(注意 stey 链表头仍指向已搬走的元素!):

.-----------------------------------------------------------------. | | v | .------. .-------. .---------. .---------. .-----. .-----. | | knie |-->| Grock |-->| Dimitri |-->| Alfredo |-->| Pio |-->| Pic |--' '------' '-------' '---------' '---------' '-----' '-----' ^ .-------------------------------' | .------. | | stey |--' '------'

陷阱:此时再遍历stey链表不会得到正确行为——list_for_each()遍历 stey 会陷入无限循环,因为它的指针指向了另一条链表。原因在于list_splice()没有重新初始化被取走元素的链表头(实现见 include/linux/list.h#L589-L594)。

解决办法是list_splice_init()(include/linux/list.h#L615-L622):完成移植后顺手把list(捐献方链表头)重新初始化。配套还有面向队列语义的list_splice_tail()/list_splice_tail_init()(把条目拼到目标链表尾部)。

12. 状态查询工具函数

  • list_empty(head):链表是否为空——判断head->next == head(include/linux/list.h#L417-L420);
  • list_empty_careful(head):判断链表为空没有其他 CPU 正在修改它(与list_del_init_careful()配对使用,保证内存操作顺序,见 include/linux/list.h#L433-L458);
  • list_is_first()/list_is_last()/list_is_head():判断条目位置(include/linux/list.h#L388-L411);
  • list_is_singular(head):链表是否恰好只有一个条目(include/linux/list.h#L496-L499);
  • list_count_nodes(head):统计节点数(include/linux/list.h#L808-L817)。

13. 并发访问:锁与 RCU

13.1 默认前提:需要外部锁

对链表的并发读写大多数情况下必须加锁保护。文档明确指出:并发访问与修改需要锁;在「读多写少」的场景下,更推荐使用 RCU 原语。

13.2 RCU 保护读多写少链表

RCU 的一大优势是:所需的内存顺序全部由链表宏提供。详见 Documentation/RCU/listRCU.rst,它给出了三类典型用例:

  • 用例 1:延迟销毁(Deferred Destruction)——无锁遍历系统中所有进程。task_struct::tasks把全部进程串成链表,读者在rcu_read_lock()内用for_each_process(p)遍历;写者release_task()tasklist_lock写锁下调用list_del_rcu(&p->tasks)摘除节点,并通过call_rcu()延迟到宽限期(grace period)结束后才释放task_struct,保证遍历者看到的next指针始终有效。这种模式被称为「存在性锁」(existence lock)。
  • 用例 2:锁外使用读侧计算结果——如系统调用审计的audit_filter_task():在auditsc_lock读锁内遍历audit_tsklist,拿到结果后提前释放锁再使用该值,即使链表随后被修改也无关紧要(审计多记几个系统调用无伤大雅)。
  • 用例 3:引用计数——读者遍历期间持有引用,防止对象被释放。

for_each_process的定义(Documentation/RCU/listRCU.rst):

#define next_task(p) \ list_entry_rcu((p)->tasks.next, struct task_struct, tasks) #define for_each_process(p) \ for (p = &init_task ; (p = next_task(p)) != &init_task ; )

对应的 RCU 变体链表宏(list_add_tail_rcu()list_del_rcu()list_replace_rcu()list_for_each_entry_rcu()等)也都定义在 include/linux/list.h 中,与普通宏共享同一套节点结构。

14. 内核中的真实应用

链表 API 在内核中无处不在。仅kernel/目录下就有大量文件在使用list_for_each_entry/list_add_tail/list_del系列宏,例如:

  • kernel/auditfilter.ckernel/auditsc.c:审计规则链表的维护(正是 Documentation/RCU/listRCU.rst 用例 2 的实例);
  • kernel/bpf/cgroup.ckernel/bpf/devmap.c:BPF 子系统中的对象挂接;
  • kernel/async.c:异步域中的 pending 链表管理。

这印证了文档的论断:环形双向链表是内核各子系统组织「同类对象集合」的通用底座。

15. 私有链表 API:list_private.h

struct list_head是结构体的私有(private)成员时,可使用 include/linux/list_private.h(2025 年新增,作者 Pasha Tatashin)提供的list_private_*原语。它与<linux/list.h>功能一一对应,区别在于:

  • 通过ACCESS_PRIVATE访问私有成员;
  • 偏移计算使用__list_private_offset(type, member),而非标准offsetof

核心宏包括list_private_entry()list_private_first_entry()/list_private_last_entry()list_private_next_entry()/list_private_prev_entry()list_private_for_each_entry()及其 reverse / continue / from / safe 全套遍历变体。适用场景是:链表头成员被结构体以私有方式封装、不希望外部直接通过成员名访问时的内核内部使用。

16. 单向哈希链表变体:hlist

在 include/linux/list.h#L989-L1270 中,还有为哈希表优化的单指针头链表hlist

struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; };

(定义见 include/linux/types.h#L210-L216。)它用一个指针的链表头(而不是两个),在哈希表条目数巨大、双指针链表头过于浪费内存的场景下节省空间;代价是失去 O(1) 访问队尾的能力。相关 API 有hlist_add_head()hlist_del_init()hlist_for_each_entry()hlist_for_each_entry_safe()hlist_count_nodes()等。

17. 调试与加固:CONFIG_DEBUG_LIST 与 CONFIG_LIST_HARDENED

include/linux/list.h#L57-L154 中的链表校验逻辑由两个内核配置驱动:

  • CONFIG_DEBUG_LIST:在list_add/list_del前执行全套链表损坏检查,损坏时报告 warning 并拒绝操作;
  • CONFIG_LIST_HARDENED:仅做最小化内联完整性检查,捕获「非故障型损坏」(如被覆写的指针),检测到问题再调用慢路径报告函数。

校验函数__list_add_valid()/__list_del_entry_valid()检查诸如next->prev == prev && prev->next == next这类双向一致性;报告函数__list_add_valid_or_report()__list_del_entry_valid_or_report()实现在 lib/list_debug.c(通过EXPORT_SYMBOL导出)。这套机制让「use-after-free 式链表误用」在开发期即可暴露,而不是静默破坏内存。

18. 完整 API 参考

文档末尾以 kernel-doc 方式内嵌了完整 API 清单:

  • Full List API:来自 include/linux/list.h 的:internal:全部注释——即前文提到的所有宏与内联函数,每一条都带参数说明与语义注释;
  • Private List API:来自 include/linux/list_private.h 的「Private List Primitives」文档块及其全部实现。

阅读这些头文件中的 kernel-doc 注释,是快速检索具体宏语义(参数含义、边界条件、并发前提)的第一手资料。

小结

Linux 内核的环形双向链表以「struct list_head内嵌 +container_of反推」的泛型设计,把增(list_add/list_add_tail)、删(list_del/list_del_init)、查(list_for_each_entry)、改(list_move/list_swap/list_rotate_*/list_splice/list_cut_*/list_bulk_move_tail)等全套操作收敛为一套与 payload 类型无关的宏。使用时牢记三条准则:

  1. 空链表头必须自指(LIST_HEAD_INIT/INIT_LIST_HEAD);
  2. 遍历中删除必须用_safe变体;
  3. 并发场景要么加锁,要么在「读多写少」时选择 RCU 变体,并在对象释放上配合宽限期延迟回收。

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询