Linux内核中链表介绍

链表是Linux内核中最简单、最普通的数据结构。链表是一种存放和操作可变数量元素(常称为节点)的数据结构。链表和静态数组的不同之处在于,它所包含的元素都是动态创建并插入链表的,在编译时不必知道具体需要创建多少个元素。另外也因为链表中每个元素的创建时间各不相同,所以它们在内存中无须占用连续内存区。正是因为元素不连续地存放,所以各元素需要通过某种方式被连接在一起。于是每个元素都包含一个指向下一个元素的指针,当有元素加入链表或从链表中删除元素时,简单调整指向下一个节点的指针就可以了。

单向链表和双向链表:

可以用一种最简单的数据结构来表示这样一个链表:
/* 一个链表中的一个元素 */
struct list_element{
void *data; /* 有效数据 */
struct list_element *next; /* 指向下一个元素的指针 */
}

下图描述了一个简单的单向链表;


图. 一个简单的链表

在有些链表中,每个元素还包含一个指向前一个元素的指针,因为它们可以同时向前和向后相互连接,所以这种链表被称作为双向链表。而上图
所示的那种只能向后连接的链表被称作单向链表。
表示双向链表的一种数据结构如下:
/* 一个链表中的一个元素 */
struct list_element{
void *data; /* 有效数据 */
struct list_element *next; /* 指向下一个元素的指针 */
struct list_element *prev; /* 指向前一个元素的指针 */
}

下图描述了一个双向链表;


图. 一个双向链表

环形链表:

通常情况下,因为链表中最后一个元素不再有下一个元素,所以将链表尾元素中的向后指针设置为NULL,以此表明它是链表中的最后一个元素。但在有些链表中,末尾元素并不指向特殊值,相反,它指回链表的首元素。这种链表因为首尾相连,所以被称为是环形链表。环形链表也存在双向链表和单向链表两种形式。在环形双向链表中,首节点的向前指针指向尾节点。下图1和图2分别表示单向和双向环形链表。


图1. 环形单向链表


图2. 环形双向链表

因为环形双向链表提供了最大的灵活性,所以Linux内核的标准链表就是采用环形双向链表形式实现。

沿链表移动:
沿链表移动只能是线性移动。先访问某个元素,然后沿该元素的向后指针访问下一个元素,不断重复这个过程,就可以沿链表向后移动了。这是一种最简单的沿链表移动方法,也是最适合访问链表的方法。如果需要随机访问数据,一般不使用链表。使用链表存放数据的理想情况是,需要遍历所有数据或需要动态加入和删除数据时。

有时,首元素会用一个特殊指针表示————该指针称为头指针,利用头指针可方便、快速地找到链表的"起始端"。在非环形链表里,向后指针指向NULL的元素是尾元素,而在环形链表里向后指针指向头元素的元素是尾元素。遍历一个链表需要线性地访问从第一个元素到最后一个元素之间的所有元素。对于双向链表来说,也可以反向遍历链表,可以从最后一个元素线性访问到第一个元素。当然还可以从链表中的指定元素开始向前和向后访问数个元素,并不一定要访问整个链表。

Linux内核中的实现:
相比普通的链表实现方式,Linux内核的实现可以说独树一帜。回忆早先提到的数据通过内部添加一个指向数据的next节点指针,才能串联在链表中。比如,假定我们有一个fox数据结构来描述犬科动物中的一员。
struct fox{
unsigned long tail_length; /* 尾巴长度,以厘米为单位 */
unsigned long weight; /* 重量,以千克为单位 */
bool is_fantastic; /* 这只狐狸奇妙吗? */
};
存储这个结构到链表里的通常方法是在数据结构中嵌入一个链表指针,比如:
struct fox{
unsigned long tail_length; /* 尾巴长度,以厘米为单位 */
unsigned long weight; /* 重量,以千克为单位 */
bool is_fantastic; /* 这只狐狸奇妙吗? */
struct fox *next; /* 指向下一个狐狸 */
struct fox *prev; /* 指向前一个狐狸 */
};
Linux内核方式与众不同,它不是将数据结构塞入链表,而是将链表节点塞入数据结构。
1.链表数据结构:
在过去,内核中有许多链表的实现,该选一个即简单、又高效的链表来统一它们了。在内核2.1开发系列中,首先引入了官方内核链表实现。从此内核中的所有链表现在都使用官方的链表实现了,千万不要再自己造轮子啦。
链表代码在头文件<linux/list.h>中声明,其数据结构很简单:
struct list_head {
struct list_head *next;
struct list_head *prev;
};
next指针指向下一个链表节点,prev指针指向前一个节点。然而,似乎这里还看不出他们有多大的作用。到底什么才是链表存储的具体内容呢?其实关键在于理解list_head结构是如何使用的。
struct fox{
unsigned long tail_length; /* 尾巴长度,以厘米为单位 */
unsigned long weight; /* 重量,以千克为单位 */
bool is_fantastic; /* 这只狐狸奇妙吗? */
struct list_head list; /* 所有fox结构体形参链表 */
};
上述结构中,fox中的list.next指向下一个元素,list.prev指向前一个元素。现在链表已经能用了,但是显然还不够方便。因此内核又提供了一组链表操作例程。比如list_add()方法加入一个新节点到链表中。但是,这些方法都有一个统一的特点:它们只接受list_add结构作为参数。使用宏container_of()我们可以很方便地从链表指针找到父结构中包含的任何变量。这时因为在C语言中,一个给定结构体中的变量偏移在编译时地址就被ABI固定下来了。
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
使用container_of宏,我们定义一个简单的函数便可返回包含list_head的父类型结构体:
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
依靠list_entry()方法,内核提供了创建、操作以及其他链表管理的各种例程,所有这些方法都不需要知道list_head所嵌入的对象数据结构。
2.定义一个链表:
正如看到的:list_head本身其实并没有意义——它需要被嵌入到你自己的数据结构中才能生效:
struct fox{
unsigned long tail_length; /* 尾巴长度,以厘米为单位 */
unsigned long weight; /* 重量,以千克为单位 */
bool is_fantastic; /* 这只狐狸奇妙吗? */
struct list_head list;        /* 所有fox结构体形参链表 */
};
链表需要在使用前初始化。因为多数元素都是动态创建的(也许这就是需要链表的原因),因此最常见的方式是在运行时初始化链表。
struct fox *red_fox;
red_fox = kmalloc(sizeof(struct fox), GFP_KERNEL);
red_fox->tail_length = 40;
red_fox->weight = 6;
red_fox->is_fantastic = false;
INIT_LIST_HEAD(&red_fox->list);
如果一个结构在编译期静态创建,而你需要在其中给出一个链表的直接引用,下面是最简单方式:
struct fox red_fox = {
.tail_length = 40,
.weight = 6,
.is_fantastic = false,
.list = LIST_HEAD_INIT(red_fox.list);

};

3.链表头
前面我们展示了如果把一个现有的数据结构(这里是我们的fox结构体)改造成链表。
简单修改上述代码,我们的结构便可以被内核链表例程管理。但是在可以使用这些例程前,需要一个标准的索引指针指向整个链表,即链表头指针。内核链表实现中最杰出的特性是:我们的fox节点都是无差别的————每一个都包含一个list_head指针,于是我们可以从任何一个节点起遍历链表,直到我们看到所有节点。这种方式确实很优美,不过有时确实也需要一个特殊指针索引到整个链表,而不从一个链表节点触发。有趣的是,这个特殊的索引节点事实上也就是一个常规的list_head:
static LIST_HEAD(fox_list);

该函数定义并初始化了一个名为fox_list的链表例程,这些例程中的大多数都只接受一个或者两个参数:头节点或者头节点加上一个特殊链表节点。下面我们就具体看看这些操作例程。

操作链表:

内核提供了一组函数来操作链表,这些函数都要使用一个或多个list_head结构体指针作为参数。因为函数都是使用C语言以内联函数形式实现的,所以它们的原型在文件<linux/list.h>中。
有趣的是,所有这些函数的复杂度都为O(1)。这意味着,无论这些函数操作的链表大小如何,无论它们得到的参数如何,它们都在恒定时间内完成。比如,不管是对于包含3个元素的链表还是对于包含3000个元素的链表,从链表中删除一项或加入一项花费的时间都是相同的。这点可能没什么让人惊奇的,但你最好还是搞清楚其中的原因。

1.向链表中增加一个节点
给链表增加一个节点:
void list_add(struct list_head *new, struct list_head *head);
该函数向指定链表的head节点后插入new节点。因为链表是循环的,而且通常没有首尾节点的概念,所以你可以把任何一个节点当成head。如果把"最后"一个节点当做head的话,那么该函数可以用来实现一个栈。
回到我们的例子,假定我们创建一个新的struct fox,并把它加入fox_list,那么我们这样做:
list_add(&f->list, &fox_list);
把节点增加到链表尾:
void list_add_tail(struct list_head *new, struct list_head *head);

该函数向指定链表的head节点前插入new节点。和list_add()函数类似,因为链表是环形的,所以可以把任何一个节点当做head。如果把"第一个"元素当做head的话,那么该函数可以用来实现一个队列。

待续,明天继续。

猜你喜欢

转载自blog.csdn.net/caihaitao2000/article/details/80556562