- kernel version : v6.1.x (lts)
Linux 内核中的双向环形链表,巧妙的实现了将对象挂在节点上,而不是传统的将节点挂在对象上。
linux 在 include/linux/list.h
中提供了双向环形链表的操作函数,此代码可以移植到其他应用环境中,方便使用。
在内核编程环境中,需要
#include <linux/list.h>
来包含该头文件
需要注意的是:
- linux是通过将此链表嵌入在其他数据结构中, 从而将很多不同的数据结构链接起来!(而不是链表中包含数据)。
- 函数中的参数
head
代表的即为链表头! typeof
是GCC扩展的关键字, 所以不能在编译选项中添加-std=c99
,而是使用-std=gnu99
。- 为了兼容所有编译器,我在github中移植了通用版本。
认识环形链表
认识节点
通过阅读代码来理解实现,那么首先来看链表中的节点是如何定义的:
1 | struct list_head { |
list_head
就代表一个节点,节点包含指向上一个节点和下一个节点的前驱(prev)和后继(next)指针。
定义并初始化头节点
在链表中,首先需要创建的便是头节点:
1 |
宏LIST_HEAD
便完成了定义头节点,并对其完成初始化。初始化后的头节点其前驱和后继指针都指向自己,这便形成了最初始的环(也就空链表)。
除了上面的宏完成定义和初始化外,还有函数来单独初始化头节点,相当于将节点初始化为空链表了:
1 | static inline void INIT_LIST_HEAD(struct list_head *list) |
由于是在后期对节点的更改,这里使用了
WRITE_ONCE
原子操作来避免 data race。
插入节点
底层操作
1 | static inline void __list_add(struct list_head *new, |
这个函数的目的就是将节点new
插入到节点prev
和next
之间。
其中这里面的__list_add_valid
则是对参数的正确性做检查:
1 | static inline __must_check bool check_data_corruption(bool v) { return v; } |
上面这个函数就是为了检查以下几种异常:
prev 或 next 节点是空指针
prev 和 next 节点并不是相邻的关系,如果继续操作则会跳过中间的节点
new 等于 prev 或 next 节点
有了前面的底层操作函数,其它的操作便可以基于此来完成了。
用户接口
1 | // 将节点 new 插入到节点 head 的后面,就像是一个压栈的操作 |
1 | // 将节点 new 插入到节点 head 的前面,就像是插入到队列尾部 |
删除节点
底层操作
1 | static inline void __list_del(struct list_head * prev, struct list_head * next) |
删除prev
和next
之间的所有节点,这其实可以删除
0~N 个节点,但一般都用于删除一个节点。
1 | static inline void __list_del_clearprev(struct list_head *entry) |
这个函数相当于将节点 entry 从链表中删除了,且 entry 的 prev 指向了空。主要是用于网络代码中使用,以高效的删除一个节点。
1 | static inline void __list_del_entry(struct list_head *entry) |
这个函数就是删除entry
这个节点。
用户接口
1 | /* |
这个函数才应该是用户所使用的函数,删除节点后,将节点的前驱和后继指针都指向了一段特殊地址。如果有代码操作该特殊地址便会触发 pagefault。
其它用户操作接口
1 | // 将 old 节点替换为 new 节点 |
1 | // 交换 entry1 和 entry2 的位置 |
1 | // 从链表上删除 entry 并将 entry 初始化为空链表 |
1 | // 当当前节点的前驱节点是头节点时,那就意味着它是第一个节点 |
1 | // 内存安全形式的删除节点和置空一个节点 |
1 | // 将头节点右边的节点移动到头节点的左边 |
1 | // 当前链表仅有一个节点时,返回 true |
切割链表
底层操作
1 | // 将 head 之后一直到 entry(包含 entry)的节点切割到以 list 为头节点的链表中 |
用户接口
1 | // 将 head 之后一直到 entry(包含 entry)的节点切割到以 list 为头节点的链表中 |
宏操作
双向环形链表需要配合宏操作才能够发挥其威力。
反推节点的地址
1 | /* Are two types/vars the same type (ignoring qualifiers)? */ |
其实就是当前成员变量的地址减去其偏移地址,便可以得到结构体的首地址。
唯一需要注意的是,减去偏移时指针要转换为 char 型,以进行字节型的运算
示例代码如下:
1 |
|
相关操作函数
有了前面的认识,再来理解下面的宏就相对容易些了:
1 | /** |
链表的演化
以上是双向环形链表的实现,它还可以很方便的演化为以下几种数据结构:
去掉前驱指针,就是单向循环链表
对接口进行进一步封装,只允许头的取出和尾的插入操作,就是 FIFO
对接口进行进一步封装,只允许头的取出和插入操作,就是栈
由于上面的链表实现方式可以被插入到任意一种数据结构中,还有一种妙用:一个数据结构根据用途插入多个链表节点!
相当于是以不同的角度来看这个数据结构
示例
下面编写一个简单的内核模块,来简单的使用一下链表,模块代码如下:
1 |
|
对应的 Makefile 如下:
1 | KVERS = $(shell uname -r) |
需要注意的是:上面使用了 gnu99 编译选项。