跳到主要内容Linux 侵入式链表详解 | 极客日志C算法
Linux 侵入式链表详解
综述由AI生成侵入式链表通过将节点指针嵌入用户数据结构中,避免了额外内存分配。Linux 内核广泛采用此设计以提升缓存局部性和性能。核心在于 list_head 结构体与 container_of 宏的配合使用,允许从成员指针反推完整对象地址。相比传统链表,它支持一个对象加入多个链表,适用于进程管理、内存调度等系统级场景。掌握其初始化、遍历及安全删除操作是内核开发的基础。
鲜活5 浏览 侵入式链表详解
什么是侵入式链表
侵入式链表(Intrusive Linked List) 是一种特殊的链表实现方式,其核心特点在于:链表节点直接嵌入到数据结构内部,而不是通过指针指向独立的数据节点。
在传统的非侵入式链表中,我们需要单独分配一个节点结构体来存储指针。而在侵入式设计中,list_head 只是目标数据结构的一个成员变量。这种设计让链表操作更加高效,且省去了为节点单独分配内存的开销。
与传统链表的对比
理解侵入式链表,最好先看看其他两种常见形式:
传统链表(非侵入式)
struct list_node {
void *data;
struct list_node *next;
struct list_node *prev;
};
这种方式下,链表节点和数据是分离的。访问数据需要解引用 data 指针,且内存布局不连续,对缓存友好性稍差。
自包含链表
还有一种是将数据直接塞进节点里:
struct list_node {
int a;
struct list_node *next;
struct list_node *prev;
};
这适合固定类型的简单场景,但缺乏灵活性,无法处理复杂对象。
侵入式链表
struct list_head {
struct list_head *next;
struct *;
};
value;
name[];
};
list_head
prev
struct my_data {
int
char
32
struct list_head list;
这里 list_head 直接嵌入在 my_data 中。数据对象和链表节点在内存中是连续的,通过 container_of 宏从节点指针反推完整结构体地址。
| 特性 | 传统链表 | 自包含链表 | 侵入式链表 |
|---|
| 数据存储 | 外部对象(通过指针) | 节点内部 | 数据结构内部 |
| 灵活性 | 高(可存储任意类型) | 低(固定类型) | 高(任意类型) |
| 内存开销 | 中等 | 低 | 最低 |
| 适用场景 | 通用场景 | 简单固定类型 | 系统编程、高性能场景 |
侵入式链表的优势
为什么 Linux 内核偏爱这种设计?主要有四点原因:
- 内存效率高:不需要额外的指针来存储数据,内存布局紧凑,减少了碎片。
- 性能优势:减少了一次内存分配,且数据访问路径更短。更重要的是,由于数据与节点在一起,CPU 缓存命中率更高。
- 灵活性:一个对象可以同时属于多个链表。只需在结构体中包含多个
list_head 成员即可。
- 零开销抽象:编译后几乎没有额外开销,非常适合内核开发等对性能敏感的场景。
Linux 内核中的实现
Linux 内核在 include/linux/list.h 中实现了完整的侵入式双向链表。这是经过多年优化的工业级实现,所有内核开发者都需要熟悉它。
核心数据结构与初始化
list_head 结构体
struct list_head {
struct list_head *next;
struct list_head *prev;
};
初始化宏
#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);
list->prev = list;
}
初始化后的状态是 next 和 prev 都指向自身,形成一个空的双向循环链表。这意味着判断链表是否为空只需要检查 head->next == head。
核心操作函数
添加节点
list_add - 在头部添加
static inline void list_add(struct list_head *new, struct list_head *head) {
__list_add(new, head, head->next);
}
list_add_tail - 在尾部添加
static inline void list_add_tail(struct list_head *new, struct list_head *head) {
__list_add(new, head->prev, head);
}
删除节点
list_del - 删除节点
static inline void list_del(struct list_head *entry) {
__list_del_entry(entry);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
list_del_init - 删除并重新初始化
static inline void list_del_init(struct list_head *entry) {
__list_del_entry(entry);
INIT_LIST_HEAD(entry);
}
遍历链表
list_for_each - 遍历链表节点
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
list_for_each_entry - 遍历链表中的数据
#define list_for_each_entry(pos, head, member) \
for (pos = list_first_entry(head, typeof(*pos), member); \
&pos->member != (head); \
pos = list_next_entry(pos, member))
list_for_each_entry_safe - 安全遍历
如果在遍历时需要删除节点,必须使用这个带 _safe 后缀的版本,因为它会提前保存下一个节点的指针。
#define list_for_each_entry_safe(pos, n, head, member) \
for (pos = list_first_entry(head, typeof(*pos), member), \
n = list_next_entry(pos, member); \
&pos->member != (head); \
pos = n, n = list_next_entry(n, member))
container_of 宏详解
container_of 宏是侵入式链表的灵魂。它的功能是从结构体成员的指针获取整个结构体的指针。
宏定义
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) *__mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); \
})
工作原理
1. offsetof 宏
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
原理很简单:将 0 强制转换为 TYPE* 类型,然后取 MEMBER 的地址。因为基址是 0,所以得到的就是成员相对于结构体起始地址的偏移量。
2. 计算过程
struct my_data {
int value;
struct list_head list;
};
struct my_data *data;
struct list_head *list_ptr = &data->list;
- 获取成员类型:
typeof(((struct my_data*)0)->list) 确保类型匹配。
- 字节运算:将指针转为
char* 以便进行字节级减法。
- 减去偏移量:
result = (struct my_data*)(__mptr_char - offsetof(...))。
地址:0x1000 0x1024 [my_data] [list]
^ ^
| |
data list_ptr
offsetof = 0x1024 - 0x1000 = 0x24
从 list_ptr 获取 data: data = list_ptr - offsetof = 0x1024 - 0x24 = 0x1000 ✓
使用示例
示例 1:基本使用
#include <stdio.h>
#include <stdlib.h>
#include "list.h"
struct student {
int id;
char name[32];
int age;
struct list_head list;
};
int main(void) {
LIST_HEAD(student_list);
struct student *s1 = malloc(sizeof(struct student));
s1->id = 1;
strcpy(s1->name, "Alice");
s1->age = 20;
INIT_LIST_HEAD(&s1->list);
struct student *s2 = malloc(sizeof(struct student));
s2->id = 2;
strcpy(s2->name, "Bob");
s2->age = 21;
INIT_LIST_HEAD(&s2->list);
list_add(&s1->list, &student_list);
list_add(&s2->list, &student_list);
struct student *pos;
list_for_each_entry(pos, &student_list, list) {
printf("ID: %d, Name: %s, Age: %d\n", pos->id, pos->name, pos->age);
}
struct student *n;
list_for_each_entry_safe(pos, n, &student_list, list) {
list_del(&pos->list);
free(pos);
}
return 0;
}
示例 2:多链表管理
一个对象可以同时属于多个链表,这在进程调度中很常见:
struct task {
int pid;
char name[32];
struct list_head run_list;
struct list_head wait_list;
struct list_head all_tasks;
};
struct task *t = malloc(sizeof(struct task));
INIT_LIST_HEAD(&t->run_list);
INIT_LIST_HEAD(&t->wait_list);
INIT_LIST_HEAD(&t->all_tasks);
list_add(&t->run_list, &run_queue);
list_add(&t->all_tasks, &task_list);
应用场景
1. Linux 内核
- 进程管理:进程链表、运行队列
- 内存管理:页框链表、伙伴系统
- 文件系统:inode 链表、dentry 缓存
- 网络协议栈:sk_buff 链表
2. 高性能系统编程
- 嵌入式系统:资源受限环境
- 实时系统:低延迟要求
- 游戏引擎:性能敏感场景
总结
侵入式链表通过牺牲一点代码的可读性(需要修改结构体),换取了极高的内存效率和性能。掌握它的关键点在于:
- 理解 container_of:这是从成员指针反推结构体的核心。
- 正确使用遍历宏:区分普通遍历和安全遍历,防止删除节点时崩溃。
- 注意内存管理:删除节点后要记得释放内存,或者使用
list_del_init 保持节点可用。
- 理解循环链表:链表头指向自身表示空链表,这是判断结束条件的依据。
对于系统编程、驱动开发或任何对内存敏感的应用,这都是必备的基础知识。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online