Linux内核数据结构1(基于Linux6.6)---链表介绍
一、链表概述
在 Linux 内核中,链表(Linked List)是一种广泛使用的数据结构。它是一种灵活的、动态的内存分配结构,可以在不需要连续内存空间的情况下存储数据。链表通过将数据元素(节点)通过指针连接起来,使得元素的插入和删除操作可以非常高效地完成。Linux 内核使用链表来管理各种资源,如任务调度、内存管理、文件系统等。
链表由一系列节点(Node)组成,每个节点包含数据和指向下一个节点的指针。链表的基本结构通常如下:
struct list_head {
struct list_head *next, *prev;
};
next
:指向链表中的下一个节点。prev
:指向链表中的前一个节点。
通过这两个指针,链表可以方便地实现双向遍历
二、链表的种类
2.1、单向链表
只使用 next
指针,忽略 prev
指针。单向链表的优点是节省空间和内存,因为每个节点只需要一个指针。
特点:
- 单向链表:每个节点只有指向下一个节点的
next
指针。 - 内存节省:不使用
prev
指针,适合只需要向前遍历的场景。
/* 一个链表中的一个元素 */
struct list_element {
void *data; /* 有效数据 */
struct list_element *next; /* 指向下一个元素的指针 */
};
2.2、双向链表
最常见的 Linux 链表类型是双向链表,它使用 struct list_head
作为链表节点,每个节点有两个指针:next
和 prev
,分别指向下一个节点和前一个节点。这使得链表可以支持双向遍历。
特点:
- 双向链表:每个节点可以通过
next
和prev
指针向前向后遍历。 - 嵌入式链表:用户可以将
list_head
嵌入到自己的结构体中,这使得链表非常灵活。
/* 一个链表中的一个元素 */
struct list_element {
void *data; /* 有效数据 */
struct list_element *next; /* 指向下一个元素的指针 */
struct list_element *prev; /* 指向前一个元素的指针 */
};
2.3、环形链表
循环链表是将链表的最后一个节点的 next
指针指向链表的第一个节点,从而形成一个闭环。在 Linux 内核中,链表通常不会直接使用循环链表,但在一些特定应用中,如任务调度队列、环形缓冲区等,循环链表是非常有用的。
特点:
- 循环结构:链表的尾部指向头部,形成一个闭环。
- 无终止节点:循环链表没有“终止”的标志,遍历时需要特定的结束条件。
2.4、哈希链表(Hash List)
在某些情况下,链表与哈希表结合使用,形成哈希链表。每个哈希桶内都包含一个链表,这样可以将不同哈希值的数据组织在一起,并利用链表来处理哈希冲突。Linux 内核的 struct hlist_head
就是为了解决这一问题设计的。
特点:
- 哈希桶:每个桶中包含一个链表,用于存储哈希冲突的元素。
- 适用于哈希表:通过链表管理哈希冲突。
示例:
struct hlist_node {
struct hlist_node *next, *pprev;
};
struct hlist_head {
struct hlist_node *first;
};
2.5、环形链表(Ring Buffer)
环形链表有时被用于实现缓冲区,特别是在需要高效、连续地管理内存区域时。例如,环形缓冲区常用于数据流处理、日志系统等。与普通链表不同,环形链表的尾部指针指向头部,形成一个“圆环”结构。
特点:
- 环形结构:尾部与头部连接形成闭环。
- 固定大小:环形链表通常用于固定大小的缓冲区。
2.6、内核队列(List Queue)
内核队列(例如任务调度中的任务队列)常常使用链表作为底层数据结构。队列遵循先进先出(FIFO)原则,链表通过操作 head
和 tail
指针来实现对元素的添加和删除。
特点:
- FIFO:链表的插入和删除操作遵循先进先出的原则。
- 多用途:内核中常用于任务调度、I/O 调度等。
2.7、任务链表(Task List)
在 Linux 内核中,任务(进程或线程)也通常通过链表来管理。例如,调度器通过链表维护所有进程的队列,如就绪队列、睡眠队列等。
特点:
- 进程调度:链表用于实现就绪队列、睡眠队列、等待队列等。
- 状态管理:可以很方便地将任务状态(就绪、阻塞、运行中等)与链表节点绑定。
2.8、延迟队列(Delayed Queue)
延迟队列是一种特殊的链表类型,用于管理需要延迟执行的任务。任务按照到期时间排序,内核会按顺序执行这些任务。延迟队列通常使用 struct timer_list
等数据结构来实现,但它也基于链表。
特点:
- 延迟执行:链表中的任务会根据预定时间排序,并按时间顺序执行。
- 定时任务:常用于定时器和延迟处理。
三、Linux内核链表的实现
- 相比普遍的链表实现方式,Linux内核的实现可以说独树一帜。
- Linux内核方式与众不同,它不是将数据结构塞入链表 ,而是将链表节点塞入数据结构。
3.1、Linux的链表数据结构(list_head)
- 链表代码定义于list.h头文件中,格式如下:
- next:指向下一个链表节点。
- prev:指向前一个链表节点。
include/linux/types.h
struct list_head {
struct list_head *next, *prev;
};
3.2、链表在内核中如何使用
普通的应用程序来表示一个链表中的节点,则其格式如下:
struct fox {
unsigned long tail_length;
unsigned long_weight;
bool is_fantastic;
struct fox *next; //指向后一个节点
struct fox *prev; //指向前一个节点
};
但是Linux表示一个节点,则用下面的格式:其中list.next:指向下一个元素。其中list.prev:指向前一个元素。
struct fox {
unsigned long tail_length;
unsigned long_weight;
bool is_fantastic;
struct list_head list; //所有fox结构体形成地链表
};
3.3、container_of()宏
使用宏container_of()可以很方便地从链表指针找到父结构中包含的任何变量。这是因为在C语言中,一个给定结构中的变置偏移在编译时地址就被ABI固定下来了 .
tools/include/linux/kernel.h
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
#endif
3.4、list_entry()宏
使用container_of()宏,我们定义一个简单的函数便可返回包含list_head的父类型结构体。
依靠list_entry()方法,内核提供了创建、操作以及其他链表管理的各种例程——所有这些方法都不需要知道list_head所嵌入对象的数据结构。
include/linux/list.h
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
3.5、定义/创建一个链表(LIST_HEAD_INIT)
链表需要在使用前初始化。因为多数元素都是动态创建的(也许这就是需要链表的原因),因此最常见的方式是在运行时初始化链表。
include/linux/list.h
#define LIST_HEAD_INIT(name) { &(name), &(name) }
3.6、链表头(LIST_HEAD宏)
内核链表最突出的特点就是:每一个链表节点中都包含一个list_head指针,于是我们可以从任何一个节点起遍历链表,直到我们看到所有节点。
以上方式确实很优美,不过有时确实也需要一个特殊指针索引到整个链表,而不从一个链表节点触发。有趣的是,这个特殊的索引节点事实上也就是一个常规的list_head。
LIST_HEAD:该函数定义并初始化了一个链表例程,这些例程中的大多数都只接受一个或者两个参数:头节点或者头节点加上一个特殊链表节点。
include/linux/list.h
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
3.7、常用链表宏和函数总结
操作 | 宏/函数名 | 描述 |
---|---|---|
初始化链表 | INIT_LIST_HEAD | 初始化一个空链表 |
插入到头部 | list_add | 将节点插入到链表头部 |
插入到尾部 | list_add_tail | 将节点插入到链表尾部 |
删除节点 | list_del | 删除链表中的节点 |
删除并初始化节点 | list_del_init | 删除节点并初始化节点 |
遍历链表 | list_for_each | 遍历链表中的每个节点 |
反向遍历链表 | list_for_each_reverse | 反向遍历链表中的每个节点 |
判断链表是否为空 | list_empty | 判断链表是否为空 |
获取第一个元素 | list_first_entry | 获取链表的第一个元素 |
获取最后一个元素 | list_last_entry | 获取链表的最后一个元素 |
四、操作链表(增加、删除、移动)
内核提供了一组函数来操作链表,这些函数都要使用一个或多个list_head结构体指针作参数。因为函数都是用C语言以内联函数形式实现的,所以它们的原型在文件 include/linux/list.h中。
下面介绍的函数复杂度都为O(1)。这意味着,无论这些函数操作的链表大小如何,无论它们得到的参数如何,它们都在恒定时间内完成。
4.1、检查链表是否为空(list_empty)
检查指定的链表是否为空,为空的话返回非0值;不为空返回0 。
/**
* list_empty - tests whether a list is empty
* @head: the list to test.
*/
static inline int list_empty(const struct list_head *head)
{
return READ_ONCE(head->next) == head;
}
4.2、增加节点(list_add)
/**
* 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);
}
功能:该函数向指定链表的head节点后插入new节点。
4.3、增加尾节点(list_add_tail)
/**
* list_add_tail - add a new entry
* @new: new entry to be added
* @head: list head to add it before
*
* Insert a new entry before the specified head.
* This is useful for implementing queues.
*/
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
功能:该函数向指定链表的head令点前插入new节点。
/*
* 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);
}
4.4、删除节点(list_del)
/**
* 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_init - deletes entry from list and reinitialize it.
* @entry: the element to delete from the list.
*/
static inline void list_del_init(struct list_head *entry)
{
__list_del_entry(entry);
INIT_LIST_HEAD(entry);
}
__list_del()函数
/*
* 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);
}
4.5、节点的移动(list_move)
从一个链表中移除list项,然后将其加入到另一个链表的head节点后面。
/**
* 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项,然后将其加入到另一个链表的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);
}
4.6、链表的合并(list_splice)
该函数合并两个链表,将list所指的链表插入到指定链表的head元素后面。
/**
* 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链表。
/**
* 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);
}
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;
}
五、遍历链表
5.1、基本方法(list_for_each)
该宏使用2个参数:
参数1:一个临时变量,遍历时用来指向当前项。
参数2:需要遍历的链表的以头节点形式存在的list_head。
在遍历链表时,第一个参数在链表中不断移动指向下一个元素,直到链表中的所有元素都被访问为止。
/**
* 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)
- 演示案例:例如我们遍历前面的fox_list链表,则可以定义以下的代码
struct list_head *p;
struct fox *f;
list_for_each(p,&fox_list){
//每次返回一个struct fox结构体
//list_entry见上面介绍
f=list_entry(p,struct fox,list);
}
5.2、list_for_each_entry()
多数内核代码采用list_for_each_entry()宏遍历链表。该宏内部也使用list_entry()宏,但简化了遍历过程参数:
参数1:指向包含list_head节点对象的指针,可将它看做是list_entry宏的返回值。
参数2:head是一个指向头节点的指针,即遍历开始的位置。
参数3:list_head在结构中的名称。
/**
* list_for_each_rcu - Iterate over a list in an RCU-safe fashion
* @pos: the &struct list_head to use as a loop cursor.
* @head: the head for your list.
*/
#define list_for_each_rcu(pos, head) \
for (pos = rcu_dereference((head)->next); \
!list_is_head(pos, (head)); \
pos = rcu_dereference(pos->next))
5.3、反向遍历链表(list_for_each_entry_reverse)
其和list_for_each_entry类似,不同点在于它是反向遍历链表的。也就是说,不再是沿着next指针向前遍历,而是沿着prev指针向后遍历。
很多原因会需要反向遍历链表。其中一个是性能原因——如果你知道你要寻找的节点最可能在你搜索的起始点的前面,那么反向搜索岂不更快。第二个原因是如果顺序很重要,比如,如果你使用链表实现堆栈,那么你需要从尾部向前遍历才能达到先进/先 出(LIFO)原则。如果你没有确切的反向遍历的原因,就老实点,用list_for_each_entry()宏吧。
/**
* 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))
5.4、正向遍历的同时删除(list_for_each_entry_safe)
标准的链表遍历方法在你遍历链表的同时要想删除节点时是不行的。因为标准的链表方法建 立在你的操作不会改变链表项这一假设上,所以如果当前项在遍历循环中被刪除,那么接下来的遍历就无法获取next(或prev)指针了。这其实是循环处理中的一个常见范式,开发人员通过在潜在的删除操作之前存储next(或者previous)指针到一个临时变量中,以便能执行删除操作。好在Linux内核提供了例程处理这种情况:
/**
* list_for_each_entry_safe - iterate over list of given type safe against removal of list entry
* @pos: the type * to use as a loop cursor.
* @n: another type * to use as temporary storage
* @head: the head for your list.
* @member: the name of the list_head within the struct.
*/
#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); \
!list_entry_is_head(pos, head, member); \
pos = n, n = list_next_entry(n, member))
5.5、反向遍历的同时删除(list_for_each_entry_safe_reverse)
该宏与list_for_each_entry_safe相反,是在反向遍历链表的同时删除它 。
/**
* list_for_each_entry_safe_reverse - iterate backwards over list safe against removal
* @pos: the type * to use as a loop cursor.
* @n: another type * to use as temporary storage
* @head: the head for your list.
* @member: the name of the list_head within the struct.
*
* Iterate backwards over list of given type, safe against removal
* of list entry.
*/
#define list_for_each_entry_safe_reverse(pos, n, head, member) \
for (pos = list_last_entry(head, typeof(*pos), member), \
n = list_prev_entry(pos, member); \
!list_entry_is_head(pos, head, member); \
pos = n, n = list_prev_entry(n, member))
六、举例应用
在 Linux 内核中,链表是一种非常重要的数据结构,广泛应用于任务调度、资源管理、I/O 系统、文件系统等多个领域。下面我会通过几个实际的例子详细说明 Linux 链表的应用。
1. 任务调度(任务队列)
Linux 内核中的任务调度是链表应用的一个经典场景。内核使用链表来管理进程(任务)的状态。比如,就绪队列(Ready Queue)和睡眠队列(Sleep Queue)等。
就绪队列
每当进程被调度执行时,内核会将其从就绪队列中取出,并执行。进程可以处于不同的状态,比如运行、就绪、阻塞等。Linux 使用链表来维护这些状态之间的转换。
代码示例:
内核的就绪队列使用双向链表来管理进程。task_struct
结构体代表每个进程,其中包含一个 list_head
类型的链表节点,用于将该进程加入到链表中。
struct task_struct {
struct list_head tasks; // 用于进程链表
...
};
// 初始化链表
INIT_LIST_HEAD(&task->tasks);
// 将进程插入就绪队列
list_add_tail(&task->tasks, &ready_queue);
list_add_tail()
用来将任务插入到链表的尾部。INIT_LIST_HEAD()
是用来初始化链表头的宏。
睡眠队列
当一个进程在等待某个条件(如 I/O 完成)时,它将进入睡眠队列。在满足条件时,内核会将进程从睡眠队列中移除并放入就绪队列中。睡眠队列也是通过链表来管理的。
代码示例:
struct sleep_queue {
struct list_head list;
...
};
// 将进程插入睡眠队列
list_add_tail(&task->tasks, &sleep_queue);
2. 内存管理(伙伴系统和空闲内存块管理)
在内核的内存管理中,链表用于管理空闲的内存块。Linux 内核使用伙伴系统(Buddy System)来分配和释放内存块,这个系统是基于链表实现的。
空闲内存块链表
当内存块被释放时,它们会被放入空闲内存块链表中,等待再次分配。Linux 内核使用多个链表来管理不同大小的内存块。每个空闲块的链表通过 list_head
来组织。
代码示例:
struct free_area {
struct list_head free_list; // 存储空闲内存块的链表
...
};
struct page {
struct list_head lru; // 用于空闲页面的链表
...
};
3. I/O 调度(I/O 请求队列)
Linux 内核使用链表来管理 I/O 请求队列。每当一个 I/O 请求(如磁盘读写)被发起时,内核会将它插入到一个链表中,调度器随后会根据某些策略处理这些请求。
块设备 I/O 请求链表
Linux 使用 struct bio
来表示块设备的 I/O 请求,bio
结构体中包含一个 list_head
类型的链表,表示 I/O 请求的链表。
代码示例:
struct bio {
struct list_head bi_list; // 用于存储 I/O 请求的链表
...
};
void add_bio_to_queue(struct bio *bio, struct bio_queue *queue) {
list_add_tail(&bio->bi_list, &queue->bi_list);
}
4. 延迟执行(延迟任务队列)
内核中有很多需要延迟执行的任务,例如定时器、延迟信号、延迟处理任务等。这些任务通常会被插入到一个延迟任务队列中,等待到期后执行。延迟任务队列也是通过链表来管理的。
定时器链表
内核定时器(如 timer_list
)通常会被放入一个链表中,在定时器到期时,内核会遍历链表并执行相应的定时任务。
代码示例:
struct timer_list {
struct list_head entry; // 定时器的链表节点
...
};
void add_timer_to_list(struct timer_list *timer, struct list_head *queue) {
list_add_tail(&timer->entry, queue);
}
5. 文件系统(dentry 缓存和 inode 链表)
Linux 文件系统通过链表来管理文件和目录的相关信息。dentry
(目录项)和 inode
(索引节点)是文件系统中两个非常重要的数据结构,它们通常通过链表来管理。
dentry 缓存
dentry
缓存用于存储文件路径的解析结果。在内核中,每个目录项(如文件或目录)都有一个 dentry
对象,dentry
结构体包含了一个链表,用于管理同一目录下的所有目录项。
代码示例:
struct dentry {
struct list_head d_u; // 用于目录项的链表
...
};
void add_dentry_to_cache(struct dentry *dentry) {
list_add_tail(&dentry->d_u, &parent_dentry->d_u);
}
inode 链表
inode
结构体用来存储文件的元数据,如文件权限、大小、块地址等。内核通过链表来管理每个文件系统的 inode
对象。
6. 网络协议栈(网络连接管理)
在 Linux 网络协议栈中,链表被用来管理网络连接。每个网络连接(如 TCP 连接)都有一个 sock
结构体,sock
结构体通过链表管理。
TCP 连接链表
在 TCP 协议中,每个连接会维护一个链表来管理连接的状态。内核会根据链表中的信息来调度和管理网络连接。
代码示例:
struct sock {
struct list_head sk_node; // 用于管理网络连接的链表
...
};
void add_sock_to_list(struct sock *sk, struct list_head *list) {
list_add_tail(&sk->sk_node, list);
}