如果说内核态helloworid是我们进入linux内核世界的第一步,那么双链表操作算是我们学习linux内核数据组织方式的敲门砖。
在这里,我们首先要搞清一个概念,我们为什么要用链表来组织数据? 这我们可以从我们如何操作链表和存储链表这个角度来理解:链表操作无外乎插入节点、删除节点、查找节点、遍历节点等等等等,而它的存储并不像我们的数组一样占据一段连续的存储空间,它是由我们动态分配的,这相当大程度的提高了内存的利用率。
可能讲的还是不够具体清楚。我们说linux内核的内存管理、文件管理、进程管理都是使用双链表组织的(其实就是我们list.h中定义的list_head),现在我们讨论进程管理中怎么使用这个双链表的。我们现在使用的PC机都是并发执行的,即同一个时间段多个进程同时运行。假设我们现在有7个进程在运行,现在我们又启动一个进程,这个进程必然携带着自己的进程控制块加到我们的运行过程中,即就是说我们现在有8个进程在运行了。那么这个加进来的进程就是用我们的链表的插入节点的操作加进来的,而这8个进程就是拿链表来组织的。假如我们现在删除一个进程,那很显然就是用链表的删除节点操作实现的。还有查找一个进程,遍历进程的操作等等,这里不再赘述,别忘了,我们要讨论的是list.h,这里只是想强调一下list.h的重要性。
0、基本结构体的定义: struct list_head
作为双链表,它必然有一个指针成员指向下一个节点,也必然有一个指针指向前一个节点,其他无外乎加上我们的数据部分,但linux内核中把指向下一个节点和指向前一个节点的成员封装起来,放在一个只有这两个成员的结构体,这就是著名的struct list_head,它的定义是这样的:
struct list_head {
struct list_head *next, *prev;
};
那如果我们想要加入自己的数据部分,那么我们的结构体大概要这么定义:
struct my_test_list {
void *my_data;
struct list_head list;
};
如果把数据部分换成进程控制块的话,这就是一个小型的进程管理结构体了。这种定义结构体的方式有一个特别大的好处,就是我们可以把这个结构体(list_head)嵌入到任何结构体中,以实现双链表结构。而我们遍历一个结构体的话也只用遍历其中的list_head成员,在这个优势的基础上,我们就可以定义出一系列公共的操作,因而,在list.h中定义一系列针对list_head的操作,以方便我们使用。接下来我们就开始介绍这些操作。
1、链表的初始化
list.h定义了两个带参数的宏来初始化一个链表,初始化链表即就是让链表的指针指向自己。
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
list.h还定义了一个函数来初始化一个链表:
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
这里我们附加一个概念:在linux内核中用到了大量的带参数的宏定义来实现一个功能,我们知道带参数的宏能完成和函数类似的功能,那linux内核为啥不用函数来初始化呢? 这是一个空间时间问题,编译器在编译一个宏时,会把这个宏在原地展开(顺便,内联函数的编译也是这样的),因而占据了内存,但这样速度就快了,而编译器在编译一个函数时(非内联函数)会有跳转指令和栈保护操作,大家知道,跳转指令是相当耗时的,但却节省了内存。所以大家权衡时间和空间的比率来选择使用宏还是使用函数来实现一个功能。linux内核中宏用的非常多,这以后大家会看到。
2、添加节点操作
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;
}
这个函数在prev和next两个节点之间插入一个节点new。
下面两个函数会调用这个函数在一个节点的前、后插入一个节点,即前插和后插。
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
该函数在一个节点前插入节点。由于双链表的循环结构,我们可以传递任何节点给head,如果我们传递最后一个元素给head,那么这个函数可以实现一个栈结构。
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
该函数在一个节点后插入节点。同样,如果我传递第一个元素给head,这个函数就可以实现一个队列的结构。
3、删除节点操作
删除节点的操作其实很简单,让被删除节点的前后两个元素相互指向,这个节点就被放空了,但注意,放空是个很严肃的概念,因为这个被删除的节点是我们动态申请的空间,这样简单的让它无所指是一个很不安全的做法。
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
下面这个和函数调用这个函数删除一个节点。
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
内核源码中自己也提到这个函数是一个不安全的删除元素的方式,它跟我们上边放空的概念差不多,只是让删除的节点的两个指针指向固定的位置。这样list_empty()函数确定不了它的状态,不知道返回true还是false。想要安全的删除可以用下边这个函数:
static inline void list_del_init(struct list_head *entry)
{
__list_del_entry(entry);
INIT_LIST_HEAD(entry);
}
这个函数让被删除的项的两个指针指向自己,那它当然是一个空节点了。
4、替换节点操作
static inline void list_replace(struct list_head *old,
struct list_head *new)
{
new->next = old->next;
new->next->prev = new;
new->prev = old->prev;
new->prev->next = new;
}
替换节点也很简单,就是让old节点的前一个节点的next分量指向new,后一个节点的prev分量指向new,再把new节点的两个分量分别指回去就行了。当然这个函数依然是一种不安全的替换方法,因为我们让old节点放空了。下边的函数是一种比较安全的替换节点的函数:
static inline void list_replace_init(struct list_head *old,
struct list_head *new)
{
list_replace(old, new);
INIT_LIST_HEAD(old);
}
5、移动节点操作
理解了删除和增加结点,那么将一个节点移动到链表中另一个位置,其实就很清晰了。list_move函数最终调用的是__list_add(list,head,head->next),实现将list移动到头结点之后;而list_move_tail函数最终调用__list_add_tail(list,head->prev,head),实现将list节点移动到链表末尾。
static inline void list_move(struct list_head *list, struct list_head *head)
{
__list_del_entry(list);
list_add(list, head);
}
static inline void list_move_tail(struct list_head *list,
struct list_head *head)
{
__list_del_entry(list);
list_add_tail(list, head);
}
6、一组测试函数
static inline int list_is_last(const struct list_head *list,
const struct list_head *head)
{
return list->next == head;
}
该函数测试一个节点list在链表中是否是最后一个节点,head是这个链表的头节点。
static inline int list_empty(const struct list_head *head)
{
return head->next == head;
}
该函数测试一个节点是不是空节点,若指向自己就是空节点。
static inline int list_empty_careful(const struct list_head *head)
{
struct list_head *next = head->next;
return (next == head) && (next == head->prev);
}
这个函数被认为是比list_empty()更安全的测试节点是否为空的函数,前者只是认为只要一个结点的next指针指向头指针就算为空,但是后者还要去检查头节点的prev指针是否也指向头结点。另外,这种仔细也是有条件的,只有在删除节点时用list_del_init(),才能确保检测成功。
node:这里我们只介绍一些链表的比较常用的操作,其他有些比如旋转链表、拆分链表、合并链表的操作等也都无非是节点指针的指向操作,大家有兴趣可以查源码。
今天就介绍到这里,如果能坚持下去的话,我会继续更。加油!努力!
本文深入探讨了Linux内核中的双链表数据结构及其核心操作,包括初始化、节点增删等,并介绍了其在内存管理、文件及进程管理中的应用。

被折叠的 条评论
为什么被折叠?



