内核链表源码分析
Linux 内核链表不是“数据节点里保存下一个数据节点”,而是“数据结构里嵌入一个通用链表节点”
- 先理解普通链表为什么不够用
- 再理解 Linux 内核链表到底反过来了什么
- 重点讲
list_head、链表头、节点之间怎么连 - 重点讲
container_of为什么能从小结构找到大结构
一、普通链表怎么写
最容易想到的链表是这样的:
1 | struct student { |
也就是说:
1 | struct student *next; |
指向下一个学生节点。
所以普通链表大概是:
1 | student1 ---> student2 ---> student3 ---> NULL |
这种写法很直观,但是问题也很明显。
如果现在不是学生,而是进程:
1 | struct task { |
那又要给 task 写一套链表操作。
如果又有文件结构体:
1 | struct file { |
又要写一套。
所以普通链表的问题是:
1 | 链表操作和具体数据类型绑定太死。 |
student 的链表只能操作 student,task 的链表只能操作 task。
二、Linux 内核链表的思路
Linux 内核不这样设计。
它先定义一个最通用的链表节点:
1 | struct list_head { |
这个结构体里面没有 id,没有 name,没有 pid,没有任何业务数据。
它只负责一件事:
1 | 把节点前后连接起来。 |
所以 struct list_head 可以理解成一个“挂钩”。
三、Linux 链表不是“数据里指向数据”,而是“数据里嵌入挂钩”
比如我们定义学生:
1 | struct student { |
注意这里:
1 | struct list_head list; |
不是指针,而是一个成员。
也就是说,每一个 student 对象里面,都自带一个链表挂钩。
可以想象成:
1 | +-----------------------------+ |
如果有三个学生:
1 | struct student s1; |
它们内部都有自己的 list:
1 | s1.list |
Linux 链表真正连接的是:
1 | s1.list <--> s2.list <--> s3.list |
不是直接连接:
1 | s1 <--> s2 <--> s3 |
这一点非常重要。
四、Linux 内核链表是双向循环链表
struct list_head 里面有两个指针:
1 | struct list_head { |
所以它是双向链表。
1 | next 指向后一个节点 |
而且 Linux 内核链表通常是循环链表。
也就是说,它不是这样:
1 | head <--> node1 <--> node2 <--> node3 --> NULL |
而是这样:
1 | +-----------------------------+ |
更简单地写:
1 | head <--> node1 <--> node2 <--> node3 <--> head |
链表尾部不会指向 NULL,而是重新指回 head。
五、链表头 head 是什么
Linux 内核链表通常会先定义一个链表头:
1 | LIST_HEAD(student_list); |
这个 student_list 本身也是一个:
1 | struct list_head |
但是它不是某个学生的数据,它只是一个头节点。
空链表时:
1 | student_list.next = &student_list |
也就是:
1 | +----------------+ |
空链表可以理解为:
1 | head 自己指向自己 |
所以空链表不是 NULL,而是:
1 | head->next == head |
这是 Linux 内核链表很重要的特点。
六、加入第一个节点
假设有一个学生:
1 | struct student s1; |
它里面有:
1 | s1.list |
现在执行:
1 | list_add_tail(&s1.list, &student_list); |
意思是:
1 | 把 s1.list 插入到 student_list 这个链表的尾部。 |
插入之后:
1 | student_list <--> s1.list <--> student_list |
也就是:
1 | head.next = &s1.list |
可以这样记:
1 | 链表头 head 不存数据; |
七、加入多个节点
继续加入 s2、s3:
1 | list_add_tail(&s2.list, &student_list); |
最终结构是:
1 | student_list <--> s1.list <--> s2.list <--> s3.list <--> student_list |
但是在内存里,每个 list 又藏在自己的结构体里面:
1 | +--------------------+ +--------------------+ +--------------------+ |
所以一定要记住:
1 | 链表看到的是 list_head; |
这就引出了 container_of。
八、为什么需要 container_of
遍历链表时,我们拿到的是:
1 | struct list_head *pos; |
比如 pos 指向的是:
1 | &s1.list |
但是我们真正想访问的是:
1 | s1.id |
也就是说,我们需要从:
1 | &s1.list |
反推出:
1 | &s1 |
这就是 container_of 的作用。
九、container_of 的本质
看这个结构体:
1 | struct student { |
假设整个 student 对象的起始地址是:
1 | 0x1000 |
里面成员大概这样分布:
1 | 0x1000 id |
那么:
1 | &s1.list |
可能是:
1 | 0x1018 |
但是 s1 的起始地址是:
1 | 0x1000 |
所以:
1 | s1 的地址 = s1.list 的地址 - list 在 student 中的偏移量 |
也就是:
1 | 0x1000 = 0x1018 - 0x18 |
这就是 container_of 的核心思想。
它就是地址计算。
十、container_of 怎么用
它的形式是:
1 | container_of(ptr, type, member) |
含义是:
1 | ptr:某个成员的地址 |
例如:
1 | struct student *stu; |
意思是:
1 | pos 是 struct student 里面 list 成员的地址; |
所以:
1 | container_of(pos, struct student, list) |
可以从:
1 | &s1.list |
得到:
1 | &s1 |
十一、list_entry 是什么
Linux 内核里还有一个宏:
1 | list_entry(ptr, type, member) |
它本质上就是对 container_of 的包装。
大概可以理解为:
1 | #define list_entry(ptr, type, member) \ |
所以:
1 | struct student *stu; |
意思是:
1 | 通过 pos 这个 list_head 指针,找到它所在的 struct student。 |
十二、遍历链表怎么理解
最常用的遍历是:
1 | struct student *stu; |
这个宏看起来复杂,但是可以按中文理解:
1 | list_for_each_entry(stu, &student_list, list) |
意思是:
1 | 遍历 student_list 这条链表; |
所以参数分别是:
1 | stu 遍历出来的结构体指针 |
公式模板:
1 | struct xxx *pos; |
比如学生:
1 | struct student *stu; |
比如进程:
1 | struct my_task *task; |
十三、list_add 和 list_add_tail 的区别
1. list_add
1 | list_add(&new->list, &head); |
表示把新节点插入到链表头后面。
也就是头插法。
原来:
1 | head <--> A <--> B <--> C <--> head |
插入 new 后:
1 | head <--> new <--> A <--> B <--> C <--> head |
所以 list_add 会让新节点出现在最前面。
2. list_add_tail
1 | list_add_tail(&new->list, &head); |
表示把新节点插入到链表尾部。
原来:
1 | head <--> A <--> B <--> C <--> head |
插入 new 后:
1 | head <--> A <--> B <--> C <--> new <--> head |
所以 list_add_tail 保持插入顺序。
可以这样记:
1 | list_add 头插,越新的越靠前 |
如果只是学习和写测试代码,建议先用:
1 | list_add_tail |
因为它更符合直觉。
十四、删除节点怎么理解
删除节点:
1 | list_del(&stu->list); |
假设链表是:
1 | head <--> A <--> B <--> C <--> head |
现在删除 B:
1 | list_del(&B->list); |
删除之后:
1 | head <--> A <--> C <--> head |
它本质上就是让:
1 | A.next = C |
也就是说,跳过 B。
但是注意:
1 | list_del(&stu->list); |
只是把 stu->list 从链表里摘掉。
如果 stu 是 malloc 出来的,还需要:
1 | free(stu); |
也就是说:
1 | list_del 只负责从链表断开; |
十五、删除时最容易出错的地方
如果一边遍历一边删除,不能随便用:
1 | list_for_each_entry(pos, &head, list) { |
这样可能出问题。
因为遍历宏后面还需要通过当前节点找到下一个节点。把当前节点释放了,它可能就找不到下一个了。
这种情况应该用安全版本:
1 | struct student *pos; |
记忆方式:
1 | 只遍历,不删除:list_for_each_entry |
这个非常重要。
十六、完整例子:学生链表
示例代码
1 |
|
这段代码的逻辑是:
1 | 1. 定义一个链表头 student_list |
测试脚本
1 | # 编译:gcc -g3 -O0 -Wall -Wextra -std=gnu11 list_dbg.c -o list_dbg |
编译
1 | gcc -g3 -O0 -Wall -Wextra -std=gnu11 list_dbg.c -o list_dbg |
执行
1 | gdb -x list_dbg_fixed.gdb ./list_dbg |
输出结果
1 | dku@dku:~/Desktop/list_head$ gdb -x list_dbg_fixed.gdb ./list_dbg |
初始化后内存中的关系为:
1 | s1 对象 |
十七、可以这样背 Linux 链表的使用步骤
以后看到 Linux 内核链表,就按这五步走。
第一步:定义数据结构
1 | struct student { |
记住:
1 | 数据结构里必须嵌入 struct list_head。 |
第二步:定义链表头
1 | LIST_HEAD(student_list); |
记住:
1 | 链表头不存数据,只是入口。 |
第三步:创建节点并初始化
1 | struct student *stu = malloc(sizeof(struct student)); |
记住:
1 | 初始化的是 stu->list,不是 stu。 |
第四步:插入链表
1 | list_add_tail(&stu->list, &student_list); |
记住:
1 | 插入的是 &stu->list,不是 stu。 |
这是很多初学者最容易忘的。
错误写法:
1 | list_add_tail(stu, &student_list); |
正确写法:
1 | list_add_tail(&stu->list, &student_list); |
因为链表操作函数只认识 struct list_head。
第五步:遍历链表
1 | struct student *pos; |
记住:
1 | 遍历出来的是 struct student *,不是 struct list_head *。 |
因为 list_for_each_entry 内部已经帮用了 container_of。
十八、最容易混淆的三个对象
一定要区分这三个东西。
1. 链表头
1 | student_list |
它的类型是:
1 | struct list_head |
它是链表入口,不保存学生数据。
2. 链表成员
1 | stu->list |
它的类型是:
1 | struct list_head |
它是嵌入在学生结构体里的挂钩。
3. 数据节点
1 | stu |
它的类型是:
1 | struct student * |
它才是真正保存数据的对象。
可以画成:
1 | student_list |
更完整一点:
1 | 链表头: student_list |
十九、一句话记住 Linux 内核链表
1 | Linux 内核链表把 list_head 嵌入到用户结构体中, |
这句话就是核心。
二十、几个常用宏的记忆表
| 宏/函数 | 作用 | 记忆方式 |
|---|---|---|
LIST_HEAD(name) |
定义并初始化链表头 | 创建一个空链表 |
INIT_LIST_HEAD(ptr) |
初始化一个 list_head |
让它自己指向自己 |
list_add(new, head) |
头插 | 插到 head 后面 |
list_add_tail(new, head) |
尾插 | 插到 head 前面 |
list_del(entry) |
删除节点 | 从链表摘掉 |
list_del_init(entry) |
删除并重新初始化 | 摘掉后恢复单独状态 |
list_entry(ptr, type, member) |
找外层结构体 | 从小成员找大结构 |
list_for_each_entry(pos, head, member) |
遍历 | 遍历每个外层结构体 |
list_for_each_entry_safe(pos, tmp, head, member) |
安全遍历 | 遍历时可以删除 |
二十一、代码模板
以后写 Linux 链表,就套这个。
1 | struct my_node { |
把它抽象成:
1 | struct 类型名 { |
二十二、口诀记忆
1 | 结构体里放 list, |
再压缩一点:
1 | 嵌入 list,挂到 head; |
二十三、最重要的理解图
脑子里形成这张图:
1 | struct student |
链表关系是:
1 | head <--> s1.list <--> s2.list <--> s3.list <--> head |
但访问数据时要变成:
1 | s1.list ----container_of----> s1 |
所以可以看到:
1 | list_for_each_entry(stu, &student_list, list) |
翻译成中文:
1 | 沿着 student_list 这条链上的每一个 list, |
二十四、总结
第一,Linux 内核链表是双向循环链表。
第二,链表头 head 本身不保存数据,只是一个哨兵节点。
第三,真正挂到链表上的是结构体里面的:
1 | struct list_head list; |
第四,遍历时拿到的是 list_head,但最终要通过 container_of 找回外层结构体。
第五,Linux 内核链表的强大之处在于它是通用链表,任何结构体只要嵌入 struct list_head,就可以使用同一套链表操作函数。
1 | struct list_head 负责连接 |
Linux 链表通过把通用的 list_head 嵌入到具体结构体中,实现了面向各种数据类型的通用链表操作。