前言
一、Linux内核链表源码分析
1.链表的初始化
在 Linux 内核开发中,链表的初始化非常常见,链表初始化通常有两种方式:静态初始化和动态初始化。你给出的代码片段展示了这两种初始化方式。让我们来详细讲解一下这些宏和函数是如何工作的。
1. 静态初始化
静态初始化是在编译时就完成的初始化,它不需要在运行时调用函数。静态初始化主要通过宏 LIST_HEAD_INIT
和 LIST_HEAD
来完成。
宏 LIST_HEAD_INIT(name)
#define LIST_HEAD_INIT(name) { &(name), &(name) }
LIST_HEAD_INIT(name)
是一个宏,用于静态地初始化一个 list_head
结构体变量。它将链表头的 next
和 prev
指针都指向链表头本身,也就是 &(name)
。这表示链表是空的,但已经初始化好了。
&(name)
获取了链表头name
的地址。{ &(name), &(name) }
是一个结构体初始化列表,将next
和prev
都指向name
。
宏 LIST_HEAD(name)
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
LIST_HEAD(name)
是一个宏,用于定义并初始化一个名为 name
的 list_head
结构体变量。
struct list_head name
定义了一个list_head
结构体。= LIST_HEAD_INIT(name)
通过前面的宏LIST_HEAD_INIT
来初始化这个结构体,使name
的next
和prev
都指向name
本身。
总结:
- 静态初始化是通过
LIST_HEAD_INIT
和LIST_HEAD
在编译时完成的,它们使链表头部的next
和prev
都指向链表头本身,这样就表示链表是空的。
2. 动态初始化
动态初始化是在程序运行时调用函数来完成的。在链表的动态初始化中,通常通过调用 INIT_LIST_HEAD
函数来完成。
函数 INIT_LIST_HEAD(struct list_head *list)
static inline void INIT_LIST_HEAD(struct list_head *list)
{
WRITE_ONCE(list->next, list);
list->prev = list;
}
INIT_LIST_HEAD
是一个内联函数,用于动态地初始化一个 list_head
结构体。
WRITE_ONCE(list->next, list);
将list
的next
指针指向list
本身。list->prev = list;
将list
的prev
指针指向list
本身。
总结:
- 动态初始化是通过调用
INIT_LIST_HEAD
函数在程序运行时完成的。它的效果与静态初始化相同,即将链表头的next
和prev
指针都指向链表头本身,表示链表为空。
对比总结
-
静态初始化:在编译时就完成了初始化,通过
LIST_HEAD_INIT
和LIST_HEAD
宏来实现。适用于编译时即可确定链表头的情况。 -
动态初始化:在运行时通过调用
INIT_LIST_HEAD
函数来完成初始化。适用于运行时需要初始化链表头的情况。
无论使用静态还是动态初始化,最终的效果都是将链表头的 next
和 prev
指针都指向链表头本身,表示链表为空并已初始化。
2.链表的添加
list_add
是 Linux 内核中用于在双向链表中插入新节点的函数。双向链表是一个常用的数据结构,在 Linux 内核中被广泛应用于各种任务,如管理进程、设备列表、文件系统结构等。
list_add
函数的定义
在 Linux 内核中,list_add
函数的原型通常定义在 include/linux/list.h
头文件中:
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
list_add
的主要功能是将一个新节点插入到链表的头部。具体来说,它将节点插入到链表的某个节点(通常是头节点 head
)之后,使得新节点成为链表的第一个节点。
函数参数
new
: 指向将要插入的新节点的指针,类型为struct list_head *
。head
: 指向链表头节点的指针,类型为struct list_head *
。
内部实现
list_add
实际上调用了一个内部函数 __list_add
来完成插入操作。__list_add
函数负责处理链表中节点的插入操作。
__list_add
函数
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
__list_add
函数执行的步骤如下:
-
next->prev = new;
将next
节点的prev
指针指向新节点new
。 -
new->next = next;
将新节点new
的next
指针指向原本的next
节点。 -
new->prev = prev;
将新节点new
的prev
指针指向原本的prev
节点。 -
prev->next = new;
将prev
节点的next
指针指向新节点new
。
通过这四个步骤,新节点 new
就被正确地插入到了 prev
和 next
之间,维护了链表的双向链接结构。
list_add
的功能总结
list_add
的功能是在链表中插入一个新节点,使得新节点成为链表中的第一个节点(紧跟在头节点 head
之后)。在调用 list_add
之后,链表的结构会被调整,以包含这个新的节点。
例如,假设我们有以下链表结构:
head <-> A <-> B <-> C
我们希望将一个新节点 new
插入到 head
之后(即链表的头部)。调用 list_add(new, &head);
后,链表的结构变为:
head <-> new <-> A <-> B <-> C
使用场景
list_add
通常用于将新节点插入到链表头部,用于维护栈结构或优先级较高的元素。在 Linux 内核中,它被广泛用于各类内核数据结构的管理,如设备列表、文件系统结构、进程链表等。
小结
- 功能:
list_add
在双向链表的头部插入一个新节点。 - 操作: 调用
__list_add
函数,调整前后节点的next
和prev
指针,将新节点插入到链表中。 - 应用: 常用于需要在链表头部插入新元素的场景,广泛应用于 Linux 内核的各种链表管理。
3.链表删除
list_del
是 Linux 内核中用于从双向链表中删除节点的函数。这个函数在内核链表操作中非常重要,因为它提供了一种安全且高效的方式来从链表中移除节点。
list_del
函数的定义
在 Linux 内核的链表实现中,list_del
函数通常定义在 include/linux/list.h
头文件中:
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
函数参数
entry
: 指向要从链表中删除的节点,类型为struct list_head *
。
内部实现
list_del
函数通过以下步骤来删除链表中的节点:
-
调用
__list_del(entry->prev, entry->next);
这一行代码调用了__list_del
函数,将当前节点entry
的前驱节点和后继节点连接起来,从而从链表中移除了entry
。 -
将
entry->next
设置为LIST_POISON1
将被删除节点的next
指针设置为LIST_POISON1
。这是一个特殊的宏定义,通常用于在调试中检测非法的指针操作。 -
将
entry->prev
设置为LIST_POISON2
将被删除节点的prev
指针设置为LIST_POISON2
,同样是为了调试和防止使用已经删除的节点。
__list_del
函数
static inline void __list_del(struct list_head *prev, struct list_head *next)
{
next->prev = prev;
prev->next = next;
}
__list_del
函数负责断开链表中当前节点 entry
的前驱节点 prev
和后继节点 next
,并将它们直接连接起来。
-
next->prev = prev;
将entry
的后继节点的prev
指针指向entry
的前驱节点。 -
prev->next = next;
将entry
的前驱节点的next
指针指向entry
的后继节点。
通过这两个步骤,链表中的 entry
节点被移除,链表结构保持完整。
函数的功能总结
- 删除节点:
list_del
函数从双向链表中移除指定的节点,并将它的前驱节点和后继节点连接起来。 - 安全性: 被删除节点的
next
和prev
指针被设置为特殊的值 (LIST_POISON1
和LIST_POISON2
),防止后续的非法访问和使用被删除的节点。
调用示例
假设我们有一个链表:
A <-> B <-> C
如果我们希望删除节点 B
,可以调用:
list_del(&B);
调用后,链表变为:
A <-> C
B
的前驱节点 A
和后继节点 C
被直接连接起来,而 B
的指针被设置为毒性值,以防止后续误用。
LIST_POISON1
和 LIST_POISON2
这两个宏定义通常是在调试模式下使用的。它们的值通常被定义为不可能是有效指针的值,以便在使用非法指针时快速发现错误。
#define LIST_POISON1 ((void *) 0x00100100)
#define LIST_POISON2 ((void *) 0x00200200)
这些值帮助开发人员检测到链表节点在删除后被错误地访问。
小结
- 功能:
list_del
用于从双向链表中删除节点,确保链表的前后节点正确链接。 - 实现: 通过调用
__list_del
函数实现节点删除,并将删除节点的指针设置为毒性值,防止非法访问。 - 应用: 在需要从链表中删除节点时使用,是 Linux 内核链表操作中的基础函数。
二、Linux内核链表和普通链表对比
Linux 内核链表是一种通用的双向链表实现,它在设计上与普通链表有一些显著的区别。以下是对 Linux 内核链表和普通链表的对比分析,重点讲解为什么内核链表更通用,而普通链表往往需要重新实现。
1. 内核链表是通用链表
通用性
-
Linux 内核链表:
- 设计: Linux 内核链表被设计为一个通用的、可重用的双向链表实现,可以用于各种不同类型的数据结构。它的核心在于
struct list_head
,这个结构体仅包含两个指针,分别指向链表的前一个节点和后一个节点。 - 使用方式: 使用者只需要将
struct list_head
嵌入到自己的数据结构中,然后利用链表操作函数(如list_add
,list_del
等)进行操作,无需每次重新实现链表逻辑。
- 设计: Linux 内核链表被设计为一个通用的、可重用的双向链表实现,可以用于各种不同类型的数据结构。它的核心在于
-
普通链表:
- 设计: 普通链表通常是在应用程序或驱动中为特定数据结构而编写的。每次需要使用链表时,开发者需要从头实现链表的数据结构和操作方法。
- 使用方式: 由于链表的节点结构往往包含具体的数据,链表操作函数(如添加、删除、遍历等)都需要针对具体的数据类型进行重新编写,缺乏通用性。
示例对比
-
Linux 内核链表示例:
struct my_data { int value; struct list_head list; // 内核链表结构 }; struct my_data item1, item2; INIT_LIST_HEAD(&item1.list); // 初始化链表头 list_add(&item2.list, &item1.list); // 将 item2 添加到链表中
-
普通链表示例:
struct node { int value; struct node *next; }; struct node *head = NULL; struct node *item1 = malloc(sizeof(struct node)); struct node *item2 = malloc(sizeof(struct node)); // 添加 item2 到链表 item2->next = head; head = item2;
2. 内核链表是双向链表
结构设计
-
Linux 内核链表:
- 双向链表: Linux 内核链表是双向链表,每个节点都包含两个指针,一个指向前一个节点 (
prev
),另一个指向后一个节点 (next
)。双向链表的优势在于可以轻松地进行向前和向后的遍历以及高效地进行节点删除操作。
- 双向链表: Linux 内核链表是双向链表,每个节点都包含两个指针,一个指向前一个节点 (
-
普通链表:
- 单向链表: 普通链表在大多数情况下是单向链表,即每个节点只包含一个指向下一个节点的指针。单向链表的节点删除操作通常较复杂,因为在删除一个节点时,必须要知道前一个节点的指针。
- 双向链表: 虽然普通链表也可以实现为双向链表,但这种实现通常需要开发者手动设计双向指针结构,并为插入、删除、遍历等操作编写相应的代码。
操作简便性
-
Linux 内核链表:
- 删除操作: 由于是双向链表,删除操作只需调整前后两个节点的指针,而无需查找前驱节点。例如,在内核链表中,
list_del
函数能够直接删除当前节点而不影响其他节点的链接。 - 插入操作: 在双向链表中插入节点相对简单,只需调整几个指针,即可将新节点插入到任意位置。
- 删除操作: 由于是双向链表,删除操作只需调整前后两个节点的指针,而无需查找前驱节点。例如,在内核链表中,
-
普通链表:
- 删除操作: 在单向链表中删除节点往往需要先遍历到要删除节点的前一个节点,然后才能修改其
next
指针。这使得操作更复杂且容易出错。 - 插入操作: 插入操作也较为复杂,尤其是在指定位置插入时,单向链表需要遍历到目标位置。
- 删除操作: 在单向链表中删除节点往往需要先遍历到要删除节点的前一个节点,然后才能修改其
3. 内核链表的灵活性和复用性
灵活性
-
Linux 内核链表:
- 复用性强: 由于内核链表的设计与具体的数据结构解耦,因此可以在内核中的各种数据结构之间轻松复用。同一个链表操作函数可以处理不同的数据结构,只需确保数据结构中嵌入了
struct list_head
。 - 内核链表工具函数: Linux 内核提供了大量的工具函数来简化链表操作,如
list_for_each
(遍历)、list_empty
(检查链表是否为空)、list_move
(移动节点)等,这些函数进一步增强了链表的复用性和简便性。
- 复用性强: 由于内核链表的设计与具体的数据结构解耦,因此可以在内核中的各种数据结构之间轻松复用。同一个链表操作函数可以处理不同的数据结构,只需确保数据结构中嵌入了
-
普通链表:
- 特定性强: 普通链表往往是针对具体的应用场景而设计的,其复用性较差。如果另一个模块或应用需要使用链表,通常需要重新设计和实现。
- 缺乏工具函数: 普通链表通常没有像内核链表那样丰富的工具函数,这意味着开发者需要手动实现各种常见操作,增加了开发复杂性。
总结
- Linux 内核链表 是一个高度通用的双向链表实现,提供了丰富的工具函数,能够在内核中的各种数据结构之间复用,极大地提高了代码的灵活性和可维护性。
- 普通链表 通常是单向链表,针对特定的数据结构和应用场景设计,虽然可以根据需要扩展为双向链表,但复用性和灵活性较差,每次使用时都可能需要重新实现链表操作逻辑。
通过内核链表的设计,Linux 内核能够在不牺牲性能的情况下,实现复杂数据结构的高效管理,而普通链表的实现则更适合于简单和特定的应用场景。