跳到主要内容Linux 侵入式链表详解 | 极客日志C算法
Linux 侵入式链表详解
综述由AI生成Linux 内核中的侵入式双向链表。对比了传统链表与自包含链表,阐述了侵入式链表内存紧凑、性能高、支持多链表管理的优势。介绍了核心数据结构 list_head、初始化宏及增删遍历操作函数。重点解析了 container_of 宏原理,通过偏移量计算获取结构体指针。提供了基本使用、多链表管理及队列栈实现的示例代码,适用于系统编程、内核开发及高性能场景。
樱花落尽29 浏览 什么是侵入式链表
**侵入式链表(Intrusive Linked List)**是一种特殊的链表实现方式,它的特点是:链表节点直接嵌入到数据结构内部,而不是通过指针指向独立的数据节点。
在侵入式链表中,链表节点(list_head)是数据结构的一个成员,而不是独立存在的。这种设计使得链表操作更加高效,并且不需要额外的内存分配。
核心思想
数据结构包含 list_head 成员,list_head 嵌入在数据中,通过 container_of 获取完整数据。
与传统链表的对比
传统链表(非侵入式)
传统链表通常采用以下结构:
struct list_node {
void* data;
struct list_node* next;
struct list_node* prev;
};
特点:
- 链表节点和数据是分离的
- 需要额外的指针来存储数据
- 每次访问数据都需要解引用
- 内存布局不连续
说明: list_node中的 data字段是一个指针,指向实际的数据对象,数据对象和链表节点在内存中是分离的。
自包含链表(专用链表节点)
还有一种常见的链表实现方式,数据直接嵌入在链表节点中:
struct list_node {
int a;
struct list_node* next;
struct list_node* prev;
};
特点:
- 数据直接存储在链表节点中,不是指针
- 链表节点就是数据结构本身
不需要额外的指针来访问数据内存布局连续,但只适用于单一数据类型简单直接,适合特定场景说明: 数据字段(如 int a)直接嵌入在链表节点结构体中,链表节点本身就是数据容器。
- 数据类型简单且固定
- 不需要存储复杂数据结构
- 适合教学示例和简单应用
侵入式链表
struct list_head {
struct list_head* next;
struct list_head* prev;
};
struct my_data {
int value;
char name[32];
struct list_head list;
};
- 链表节点直接嵌入在数据结构中
- 不需要额外的指针存储数据
- 通过
container_of宏从节点指针获取完整数据结构
- 内存布局更紧凑
说明: list_head直接嵌入在 my_data结构体中,数据对象和链表节点在内存中是连续的,通过 list成员连接。
三种链表类型对比总结
| 特性 | 传统链表 | 自包含链表 | 侵入式链表 |
|---|
| 数据存储 | 外部对象(通过指针) | 节点内部 | 数据结构内部 |
| 灵活性 | 高(可存储任意类型) | 低(固定类型) | 高(任意类型) |
| 内存开销 | 中等(需要指针) | 低 | 最低 |
| 适用场景 | 通用场景 | 简单固定类型 | 系统编程、高性能场景 |
侵入式链表的优势
1. 内存效率高
- 无额外指针开销:不需要额外的指针来存储数据
- 内存布局紧凑:数据连续存储,缓存友好
2. 性能优势
- 减少内存分配:不需要为链表节点单独分配内存
- 减少指针解引用:数据访问路径更短
- 更好的缓存局部性:数据连续存储,提高缓存命中率
3. 灵活性
- 一个对象可以同时属于多个链表:只需在结构中包含多个
list_head成员
- 类型无关:链表操作不关心数据类型,通用性强
4. 适合系统编程
- 零开销抽象:编译后几乎没有额外开销
- 适合内核开发:Linux 内核广泛使用
Linux 内核中的实现
Linux 内核在 include/linux/list.h中实现了完整的侵入式双向链表。这是经过多年优化的工业级实现。
文件位置
linux-4.14.7/include/linux/list.h
核心数据结构
list_head 结构体
struct list_head {
struct list_head* next;
struct list_head* prev;
};
这是侵入式链表的核心数据结构,只包含两个指针,非常简洁。
初始化宏
#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);
list->prev = list;
}
next和 prev都指向自身
- 形成一个空的双向循环链表
核心操作函数
1. 添加节点
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_add - 内部插入函数
static 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);
}
2. 删除节点
list_del - 删除节点
static inline void list_del(struct list_head *entry) {
__list_del_entry(entry);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
static inline void __list_del_entry(struct list_head *entry) {
if (!__list_del_entry_valid(entry)) return;
__list_del(entry->prev, entry->next);
}
static inline void __list_del(struct list_head *prev, struct list_head *next) {
next->prev = prev;
WRITE_ONCE(prev->next, next);
}
list_del_init - 删除并重新初始化
static inline void list_del_init(struct list_head *entry) {
__list_del_entry(entry);
INIT_LIST_HEAD(entry);
}
3. 遍历链表
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 - 安全遍历(允许删除)
#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))
4. 其他常用操作
list_empty - 判断链表是否为空
static inline int list_empty(const struct list_head *head) {
return READ_ONCE(head->next) == head;
}
list_move - 移动节点
static inline void list_move(struct list_head *list, struct list_head *head) {
__list_del_entry(list);
list_add(list, head);
}
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,所以
&((TYPE *)0)->MEMBER就是偏移量
struct my_data {
int value;
char name[32];
struct list_head list;
};
2. container_of 计算过程
struct my_data {
int value;
struct list_head list;
};
struct my_data* data;
struct list_head* list_ptr = &data->list;
const typeof(((struct my_data*)0)->list) *__mptr = list_ptr;
char *__mptr_char = (char*)__mptr;
struct my_data* result = (struct my_data*)(__mptr_char - offsetof(struct my_data, list));
地址:0x1000 0x1024 [my_data] [list]
^ ^
| |
data list_ptr
offsetof = 0x1024 - 0x1000 = 0x24
从 list_ptr 获取 data: data = list_ptr - offsetof = 0x1024 - 0x24 = 0x1000 ✓
为什么需要 typeof
typeof用于类型检查,确保传入的 ptr确实是 member类型的指针,提高代码安全性。
使用示例
示例 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);
示例 3:实现队列
LIST_HEAD(queue);
void enqueue(struct list_head* item) {
list_add_tail(item, &queue);
}
struct list_head* dequeue(void) {
if (list_empty(&queue)) return NULL;
struct list_head* item = queue.next;
list_del(item);
return item;
}
示例 4:实现栈
LIST_HEAD(stack);
void push(struct list_head* item) {
list_add(item, &stack);
}
struct list_head* pop(void) {
if (list_empty(&stack)) return NULL;
struct list_head* item = stack.next;
list_del(item);
return item;
}
应用场景
1. Linux 内核
- 进程管理:进程链表、运行队列
- 内存管理:页框链表、伙伴系统
- 文件系统:inode 链表、dentry 缓存
- 设备驱动:设备链表
- 网络协议栈:sk_buff 链表
2. 高性能系统编程
- 嵌入式系统:资源受限环境
- 实时系统:低延迟要求
- 游戏引擎:性能敏感场景
3. 需要多链表管理的场景
当一个对象需要同时属于多个链表时,侵入式链表特别有用:
struct process {
int pid;
struct list_head runq;
struct list_head waitq;
struct list_head children;
struct list_head siblings;
};
总结
侵入式链表的优点
- 内存效率高:无额外指针开销
- 性能优秀:缓存友好,访问速度快
- 灵活性强:一个对象可属于多个链表
- 类型无关:通用性强,代码复用性好
侵入式链表的缺点
- 侵入性:需要修改数据结构,添加
list_head成员
- 学习曲线:
container_of宏的理解需要一定时间
- 调试困难:指针操作较多,调试相对复杂
适用场景
- ✅ 系统编程(内核、驱动)
- ✅ 性能敏感的应用
- ✅ 需要多链表管理的场景
- ✅ 内存受限的环境
- ❌ 简单的用户态应用(可能过度设计)
关键要点
- 理解
container_of宏:这是侵入式链表的精髓
- 正确使用遍历宏:区分普通遍历和安全遍历
- 注意内存管理:删除节点后要释放内存
- 理解循环链表:链表头指向自身表示空链表
相关免费在线工具
- 加密/解密文本
使用加密算法(如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