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的话,那么该函数可以用来实现一个队列。

2.从链表中删除一个节点:
在链表中增加一个节点后,从中删除一个节点是另一个重要的操作。从链表中删除一个节点,调用list_del()函数:
void list_del(struct list_head *entry);
该函数从链表中删除entry元素。注意,该操作并不会释放entry或释放包含entry的数据结构所占用的内存;该函数仅仅是将entry元素从链表中移走,所以该函数被调用后,通常还需要再撤销包含entry的数据结构体和其中的entry项。
例如,为了删除for节点,我们回到前面增加节点的fox_list:
list_del(&f->list);
注意,该函数并没有接受fox_list作为输入函数。它只是接受一个特定的节点,并修改其前后的指针,这样给定的节点就从链表中删除。代码的实现颇具有启发性:
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);
}
从链表中删除一个节点并对其重新初始化:
void list_del_init(struct list_head *entry);

该函数除了还需要再次初始化entry以外,其他和list_del()函数类似。这样做是因为:虽然链表不再需要entry项,但是还可以再次使用包含entry的数据结构体。

3.移动和合并链表节点:
把节点从一个链表移到另一个链表:
void list_move(struct list_head *list, struct list_head *head);
该函数从一个链表中移除list项,然后将其加入到另一链表的head节点后面。
把节点从一个链表移到另一个链表的末尾:
void list_move_tail(struct list_head *list,struct list_head *head);
该函数和list_move()函数一样,唯一的不同是将list项插入到head项前。
检查链表是否为空:
static inline int list_empty(const struct list_head *head)
{
return head->next == head;
}
如果指定的链表为空,该函数返回非0值;否则返回0。
把两个未连接的链表合并在一起:
void list_splice(struct list_head *list, struct list_head *head);
该函数合并两个链表,它将list指向的链表插入到指定链表的head元素后面。
把两个未连接的链表合并在一起,并重新初始化原来的链表:
void list_splice_init(struct list_head * list, struct list_head * head);
该函数和list_splice()函数一样,唯一的不同是由list指向的链表要被重新初始化。

遍历链表:
现在我们已经知道了如何在内核中声明、初始化和操作一个链表。这很了不起,但如果无法访问自己的数据,这些没有任何意义。链表仅仅是个能够包含重要数据的容器;我们必须利用链表移动并访问包含我们的数据的结构体。幸好,内核为我们提供了一组非常棒的接口,可以用来遍历链表和引用链表中的数据结构体。
注意:和链表操作函数不同,遍历链表的复杂度为O(n),n是链表所包含的元素数目。
1.基本方法:
遍历链表最简单的方法是使用list_for_each宏,该宏使用两个list_head类型的参数,第一个参数用来指向当前项,这是一个你必须要提供的临时变量,第二个参数是需要遍历的链表的以头节点形式存在的list_head。每次遍历中,第一个参数在链表中不断移动指向下一个元素,直到链表中所有的元素都被访问为止。用法如下:
struct list_head *p;
list_for_each(p, list){
/* p指向链表中的元素 */
}
好了,实话实说,其实一个指向链表结构的指针通常是无用的;我们所需要的是一个指向包含list_head的结构体的指针。比如前面fox结构体的例子,我们需要的是指向每个fox的指针,而不需要指向结构体中list成员的指针。我们可以使用前面讨论的list_entry()宏,来获得包含给定list_head的数据结构。比如:
struct list_head *p;
struct fox *f;
list_for_each(p, &fox_list){
/* p指向链表中的元素 */
f = list_entry(p, struct fox, list);

}

2.可用方法:
前面的方法虽然确实展示了list_head节点的功效,但并不优美,而且也不灵活。所以多数内核代码采用list_for_each_entry宏遍历链表。该宏内部也是用list_entry()宏,但简化了遍历过程:
list_for_each_entry(pos, head, member);
这里pos是一个指向包含list_head节点对象的指针,可将它看做是list_entry宏的返回值。head是一个指向头节点的指针,即遍历开始位置,在我们前面例子中,fox_list.member是pos中list_head结构的变量名。这听起来令人迷惑,但是简单实用。下面的代码片断展示了如何重写前面的list_for_each(),来遍历所有fox节点:
struct fox *f;
list_for_each_entry(f,&fox_list,list){

}
现在来看看实际例子吧。它来自inotify————内核文件系统的更新机制:
static struct inotify_watch *inode_find_handle(struct inode *inode,struct inotify_handle *ih)       
{
struct inotify_watch *watch;
list_for_each_entry(watch, &inode->inotify_watches, i_list) {
if (watch->ih == ih)
return watch;
}
return NULL;
}

该函数遍历了inode->inotify_watches链表中的所有项,每个项的类型都是struct inotify_watch,list_head在结构体中被命名为i_list。循环中的每一个遍历,watch都指向链表的新节点。该函数的目的在于:在inode结构串联起来的inotify_watches链表中,搜寻其inotify_handle与所提供的句柄相匹配的inotify_watch项。

3.反向遍历链表:
宏list_for_each_entry_reverse()的工作和list_for_each_entry()类似,不同点在于它是反向遍历链表的。也就是说,不再沿着next指针
向前遍历,而是沿着prev指针向后遍历。其用法和list_for_each_entry()相同:
list_for_each_entry_reverse(pos, head, member);
很多原因会需要反向遍历链表。其中一个是性能原因——如果你知道你要寻找的节点是最可能在你搜索的起始点前面,那么反向搜索岂不更快。
第二个原因是如果顺序很重要,比如,如果你使用链表实现堆栈,那么你需要从尾部向前遍历才能达到先进先出原则。如果你没有确切的反向
遍历的原因,就老实点,用list_for_each_entry()宏吧。
4.遍历同时删除:
标准的链表遍历方法在你遍历链表的同时要想删除节点时是不行的。因为标准的链表方法建立在你的操作不会改变链表项这一假设上,所以如果当前项在遍历循环中被删除,那么接下来的遍历就无法获得next或prev指针了。这其实是循环处理中的一个常见范式,开发人员通过在潜在的删除操作之前存储next或prev指针到一个临时变量中,以便能执行删除操作。好在Linux内核提供了例程处理这种情况:
list_for_each_entry_safe(pos, n, head, member);
你可以按照list_for_each_entry()宏的方式使用上述例程,只是需要提供next指针,next指针和pos是同样的类型。list_for_each_entry_safe()启动next指针来将下一项存进表中,以使得能安全删除当前项。我们再次看看inotify的例子:
void inotify_inode_is_dead(struct inode *inode)
{
struct inotify_watch *watch, *next;
mutex_lock(&inode->inotify_mutex);
list_for_each_entry_safe(watch, next, &inode->inotify_watches, i_list) {
struct inotify_handle *ih = watch->ih;
mutex_lock(&ih->mutex);
inotify_remove_watch_locked(ih, watch);
mutex_unlock(&ih->mutex);
}
mutex_unlock(&inode->inotify_mutex);
}
该函数遍历并删除inotify_watches链表中的所有项。如果使用了表中的list_for_each_entry(),那么上述代码会造成"使用——在——释放后"的错误,因为在移项链表中下一项时,需要访问watch,但这时它已经被撤销。
5.其他链表方法:
Linux提供了很多链表操作方法——几乎是你所能想到的所有访问和操作链表方法,所有这些方法都可在头文件<linux/list.h>中找到。

  • 13
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值