Linux内核深入学习(4)——内核常见的数据结构之链表
Linux内核深入学习(3)——内核常见的数据结构1链表
前言
我们的常用的数据结构,一个是我们的list,另一个是笔者打算之后介绍的rb_tree,也就是红黑树。这里我们先从链表开始说起。
内核的链表是双循环链表
双循环链表是一种常见的数据结构,它在计算机科学中扮演着灵活组织数据的角色。想象一条由许多环节连接而成的链条,每个环节不仅知道自己的下一个环节在哪里,还能记住前一个环节的位置。这种链条首尾相接,形成一个闭环,就像游乐场的环形小火车,无论从哪个车厢出发,都能沿着轨道绕回起点。这就是双循环链表最直观的样貌。
每个节点都像一个小型数据仓库,存储着有价值的信息,同时携带着两把钥匙:一把指向它的前驱节点,另一把指向后继节点。这种双向连接的特性让数据访问变得自由灵活。当我们需要查找某个数据时,既可以从头节点顺时针寻找,也能反向逆时针回溯。这种双向性在需要频繁前后移动的场景中尤其有用,比如音乐播放器的播放列表,既支持切到下一首歌,也能随时返回上一曲。
与传统链表不同,双循环链表的首尾节点通过指针紧紧相扣。当链表为空时,头节点的前后指针都指向自己,就像一个孤独的哨兵静静守护着空荡的城堡。随着数据节点的加入,这个闭环会逐渐扩展,但始终保持着循环的特性。这样的设计消除了链表头尾的边界感,使得所有节点都处于平等的位置,操作时无需特别处理首尾边界条件,这在某些算法实现中能显著简化代码逻辑。
插入和删除操作是双循环链表的拿手好戏。要新增一个节点,只需调整相邻节点的指针指向,就像在铁链中间插入新环节时重新焊接两端的连接点。删除节点时同样优雅,断开目标节点与邻居的连接后,前后节点会自然牵起手来,整个过程不需要大规模移动数据。这种高效的重构能力让它在需要频繁修改数据的场景中表现出色,比如实时更新的缓存系统或动态规划的解决方案。
这里,我们的内核采用的就是经典的双循环链表。很快,我们就来看看内核时如何抽象的。
list head是什么
struct list_head {struct list_head *next, *prev;
};
我们知道,数据结构本身跟数据自身的属性是没有关系的,换而言之,数据本身的组织不应当被采取的容器所影响。你看,C++的std::list<T>
是使用泛型这样做到的,那问题来了,C咋办呢?我们总不能强硬的将每一个我们关心成一条链的数据结构都整出来重复的链表代码吧!那太冗余!
为此,我们的做法是——希望让每一个使用链表组织的数据内部,侵入一个链表的节点结构体,想要找到链表上的存储的数据,只需要反推这个链表所在的位置对应的偏移头,偏移节点所在结构体的偏移量就能拿到原本结构的头
+===========================+ <===== get the struct itself
| |
| int xxx; |
| |
|---------------------------| <==== Offset of the struct list_head
| |
| struct list_head |
|---------------------------|
....
为此,只需要将list_head的地址剪掉list_head的offset就能拿到结构体了!就是这个原理!现在,我们就能将数据本身和链表解耦合开来了!
初始化
-
静态初始化
使用宏LIST_HEAD(name)
可在编译期声明并初始化一个空链表头:LIST_HEAD(my_list);
等价于:
struct list_head my_list = { &my_list, &my_list };
我们来看看实际上是如何做的:
#define LIST_HEAD_INIT(name) { &(name), &(name) }#define LIST_HEAD(name) \struct list_head name = LIST_HEAD_INIT(name)
嗯很,就是静态的初始化结构体嘛!没啥新鲜的!
-
动态初始化
对于已声明的struct list_head head;
,可在运行期调用:INIT_LIST_HEAD(&head);
将其
next
和prev
指向自身,实现空表状态static inline void INIT_LIST_HEAD(struct list_head *list) {WRITE_ONCE(list->next, list);WRITE_ONCE(list->prev, list); }
这里的write_once是一次赋值大小检查和一次真正的赋值行为封装宏。
核心操作函数
核心操作都在 include/linux/list.h
及 lib/list.c
(旧版中为 lib/list.c
,新版本都为宏实现)中定义,主要包括:
-
添加节点
list_add(new, head)
:将new
添加到head
之后,常用于栈(LIFO)场景。list_add_tail(new, head)
:将new
添加到head
之前,实现队列(FIFO)效果
/** Insert a new entry between two known consecutive entries.** This is only for internal list manipulation where we know* the prev/next entries already!*/ 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); }/*** list_add - add a new entry* @new: new entry to be added* @head: list head to add it after** Insert a new entry after the specified head.* This is good for implementing stacks.*/ static inline void list_add(struct list_head *new, struct list_head *head) {__list_add(new, head, head->next); }
我们的内核喜欢使用
__T
和T
隔离实现,这样的方式来保证API的稳定,内部函数__list_add
如同精密的手术刀,负责在已知的相邻节点prev
与next
之间嵌入新节点new
。它先通过__list_add_valid
进行安全性校验,随后像编织绳索般调整指针:先将新节点的next
与next
节点挂钩,再将新节点的prev
与prev
节点相连,最后用WRITE_ONCE
确保前驱节点的next
指针原子性地指向新节点,防止多线程环境下的数据竞争。外层函数
list_add
则是面向开发者的友好接口,它将新节点直接插入到链表头节点head
之后。 -
删除节点
list_del(entry)
:将entry
从所属链表中移除,内部调用__list_del(entry->prev, entry->next)
并将entry
指针置为 UNDEFINED。list_del_init(entry)
:移除后重新初始化该节点,使其成为单节点空链表。
/** Delete a list entry by making the prev/next entries* point to each other.** This is only for internal list manipulation where we know* the prev/next entries already!*/ static inline void __list_del(struct list_head * prev, struct list_head * next) {next->prev = prev;WRITE_ONCE(prev->next, next); }/** Delete a list entry and clear the 'prev' pointer.** This is a special-purpose list clearing method used in the networking code* for lists allocated as per-cpu, where we don't want to incur the extra* WRITE_ONCE() overhead of a regular list_del_init(). The code that uses this* needs to check the node 'prev' pointer instead of calling list_empty().*/ static inline void __list_del_clearprev(struct list_head *entry) {__list_del(entry->prev, entry->next);entry->prev = NULL; }static inline void __list_del_entry(struct list_head *entry) {if (!__list_del_entry_valid(entry))return;__list_del(entry->prev, entry->next); }/*** list_del - deletes entry from list.* @entry: the element to delete from the list.* Note: list_empty() on entry does not return true after this, the entry is* in an undefined state.*/ static inline void list_del(struct list_head *entry) {__list_del_entry(entry);entry->next = LIST_POISON1;entry->prev = LIST_POISON2; }
基础指针调整 (
__list_del
)
内部函数,通过直接操作相邻节点的指针完成删除:- 将后驱节点
next
的prev
指向当前节点的前驱prev
- 使用
WRITE_ONCE
宏原子性地更新前驱节点prev
的next
指向next
此操作仅解除目标节点与链表的连接,不修改目标节点自身指针,适用于已知前后节点的场景。
清除前向指针 (
__list_del_clearprev
)
在基础删除后,显式将目标节点的prev
置为NULL
。该设计针对网络模块的 per-cpu 链表优化,牺牲list_empty
的可用性来减少一次写操作,调用方需通过检测prev
是否为空判断节点状态。带校验的删除 (
__list_del_entry
)
对外部暴露的安全接口:- 先通过
__list_del_entry_valid
验证节点合法性(防止无效操作) - 再调用基础
__list_del
执行指针调整
提供基本的防错机制,确保节点确实存在于链表中。
完全节点隔离 (
list_del
)
标准删除接口:- 调用
__list_del_entry
解除节点与链表的关联 - 将节点的
next
/prev
赋值为LIST_POISON
(特殊标记值,通常为非法地址) - 使被删节点进入"毒化"状态:既无法通过常规方法访问链表,又能在调试时触发错误显式暴露悬空指针访问。
-
移动与拼接
-
list_move(entry, head)
、list_move_tail(entry, head)
:分别将单节点移到另一链表的 head 之后或之前。 -
list_splice(list, head)
、list_splice_tail(list, head)
:将整个链表拼接到目标链表中,原链表保留或变为空链表(视 API 而定)/*** list_move - delete from one list and add as another's head* @list: the entry to move* @head: the head that will precede our entry*/ static inline void list_move(struct list_head *list, struct list_head *head) {__list_del_entry(list);list_add(list, head); }/*** list_move_tail - delete from one list and add as another's tail* @list: the entry to move* @head: the head that will follow our entry*/ static inline void list_move_tail(struct list_head *list,struct list_head *head) {__list_del_entry(list);list_add_tail(list, head); }
list_move
将节点从原链表删除,并插入到目标头节点之后(成为新链表的第一个节点)。
内部调用__list_del_entry
删除节点,再通过list_add
将其添加到目标位置。list_move_tail
将节点从原链表删除,并插入到目标头节点之前(成为新链表的最后一个节点)。
同样先删除节点,再通过list_add_tail
将其添加到目标链表的末尾。static inline void __list_splice(const struct list_head *list,struct list_head *prev,struct list_head *next) {struct list_head *first = list->next;struct list_head *last = list->prev;first->prev = prev;prev->next = first;last->next = next;next->prev = last; }/*** list_splice - join two lists, this is designed for stacks* @list: the new list to add.* @head: the place to add it in the first list.*/ static inline void list_splice(const struct list_head *list,struct list_head *head) {if (!list_empty(list))__list_splice(list, head, head->next); }/*** list_splice_tail - join two lists, each list being a queue* @list: the new list to add.* @head: the place to add it in the first list.*/ static inline void list_splice_tail(struct list_head *list,struct list_head *head) {if (!list_empty(list))__list_splice(list, head->prev, head); }
__list_splice
(内部函数)
核心功能是将源链表list
的全部节点插入到目标位置prev
和next
之间。- 获取源链表的首节点
first
(list->next
) 和尾节点last
(list->prev
) - 将
first
的prev
指向目标前驱节点prev
,同时prev
的next
指向first
- 将
last
的next
指向目标后继节点next
,同时next
的prev
指向last
通过 4 次指针操作完成两个链表的无缝衔接。
list_splice
将源链表list
拼接到目标链表head
之后(插入到head
和head->next
之间)。- 先检查源链表是否非空 (
list_empty
判断) - 调用
__list_splice(list, head, head->next)
实现拼接
典型应用场景是栈的合并,新链表节点会出现在目标链表头部之后。
list_splice_tail
将源链表list
拼接到目标链表head
之前(插入到head->prev
和head
之间)。- 同样先检查源链表非空
- 调用
__list_splice(list, head->prev, head)
实现拼接
适用于队列操作,新链表节点会出现在目标链表尾部。
- 获取源链表的首节点
-
-
遍历宏
list_for_each(pos, head)
:以struct list_head *pos
形式遍历所有节点。list_for_each_entry(entry, head, member)
:直接以外层结构指针形式遍历,内置container_of
安全转换,推荐使用
/*** list_for_each - iterate over a list* @pos: the &struct list_head to use as a loop cursor.* @head: the head for your list.*/ #define list_for_each(pos, head) \for (pos = (head)->next; !list_is_head(pos, (head)); pos = pos->next)/*** list_for_each_reverse - iterate backwards over a list* @pos: the &struct list_head to use as a loop cursor.* @head: the head for your list.*/ #define list_for_each_reverse(pos, head) \for (pos = (head)->prev; pos != (head); pos = pos->prev) /*** list_for_each_entry - iterate over list of given type* @pos: the type * to use as a loop cursor.* @head: the head for your list.* @member: the name of the list_head within the struct.*/ #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))/*** list_for_each_entry_reverse - iterate backwards over list of given type.* @pos: the type * to use as a loop cursor.* @head: the head for your list.* @member: the name of the list_head within the struct.*/ #define list_for_each_entry_reverse(pos, head, member) \for (pos = list_last_entry(head, typeof(*pos), member); \!list_entry_is_head(pos, head, member); \pos = list_prev_entry(pos, member))
这些就是典型的使用for爬取链表,当然,还是很简单的,这里不做讲解