list_head 内核

list_head 内核
本文详细分析了 2.6.x 内核中链表结构的实现,并通过实例对每个链表操作接口进行了详尽的讲解。

一、链表数据结构简介
链表是一种常用的组织有序数据的数据结构,它通过指针将一系列数据节点连接成一条数据链,是线性表的一种重要实现方式。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。

通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型,下面分别给出这几类常见链表类型的示意图:

1.单链表

图1 单链表
图1 单链表 

单链表是最简单的一类链表,它的特点是仅有一个指针域指向后继节点(next),因此,对单链表的遍历只能从头至尾(通常是NULL空指针)顺序进行。

2.双链表

图2 双链表
 list_head <wbr>内核

通过设计前驱和后继两个指针域,双链表可以从两个方向遍历,这是它区别于单链表的地方。如果打乱前驱、后继的依赖关系,就可以构成"二叉树";如果再让首节点的前驱指向链表尾节点、尾节点的后继指向首节点(如图2中虚线部分),就构成了循环链表;如果设计更多的指针域,就可以构成各种复杂的树状数据结构。

3.循环链表
循环链表的特点是尾节点的后继指向首节点。前面已经给出了双循环链表的示意图,它的特点是从任意一个节点出发,沿两个方向的任何一个,都能找到链表中的任意一个数据。如果去掉前驱指针,就是单循环链表。

在Linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在[include/linux/list.h]实现的一个相当精彩的链表数据结构。本文的后继部分就将通过示例详细介绍这一数据结构的组织和使用。

二、 Linux 2.6内核链表数据结构的实现
尽管这里使用2.6内核作为讲解的基础,但实际上2.4内核中的链表结构和2.6并没有什么区别。不同之处在于2.6扩充了两种链表数据结构:链表的读拷贝更新(rcu)和HASH链表(hlist)。这两种扩展都是基于最基本的list结构,因此,本文主要介绍基本链表结构,然后再简要介绍一下rcu和 hlist。

链表数据结构的定义很简单(节选自[include/linux/list.h],以下所有代码,除非加以说明,其余均取自该文件):


struct list_head {
 struct list_head *next, *prev;
};
 

list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。

和第一节介绍的双链表结构模型不同,这里的list_head没有数据域。在Linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。

在数据结构课本中,链表的经典定义方式通常是这样的(以单链表为例):


struct list_node {
 struct list_node *next;
ElemType data;
};
 

因为ElemType的缘故,对每一种数据项类型都需要定义各自的链表结构。有经验的C++程序员应该知道,标准模板库中的<list>采用的是C++ Template,利用模板抽象出和数据项类型无关的链表操作接口。

在Linux内核链表中,需要用链表组织起来的数据通常会包含一个struct list_head成员,例如在[include/linux/netfilter.h]中定义了一个nf_sockopt_ops结构来描述Netfilter为某一协议族准备的getsockopt/setsockopt接口,其中就有一个(struct list_head list)成员,各个协议族的nf_sockopt_ops结构都通过这个list成员组织在一个链表中,表头是定义在[net/core/netfilter.c]中的nf_sockopts(struct list_head)。从下图中我们可以看到,这种通用的链表结构避免了为每个数据项类型定义自己的链表的麻烦。Linux的简捷实用、不求完美和标准的风格,在这里体现得相当充分。

图3 nf_sockopts链表示意图

 list_head <wbr>内核

 

 

三、链表操作接口

1. 声明和初始化
实际上Linux只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的呢?让我们来看看LIST_HEAD()这个宏:


#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)
 

当我们用LIST_HEAD(nf_sockopts)声明一个名为nf_sockopts的链表头时,它的next、prev指针都初始化为指向自己,这样,我们就有了一个空链表,因为Linux用头指针的next是否指向自己来判断链表是否为空:


static inline int list_empty(const struct list_head *head)
{
return head->next == head;
}
 

除了用LIST_HEAD()宏在声明的时候初始化一个链表以外,Linux还提供了一个INIT_LIST_HEAD宏用于运行时初始化链表:


#define INIT_LIST_HEAD(ptr) do { \
(ptr)->next = (ptr); (ptr)->prev = (ptr); \
} while (0)
 

我们用INIT_LIST_HEAD(&nf_sockopts)来使用它。

2. 插入/删除/合并
a) 插入

对链表的插入操作有两种:在表头插入和在表尾插入。Linux为此提供了两个接口:


static inline void list_add(struct list_head *new, struct list_head *head);
static inline void list_add_tail(struct list_head *new, struct list_head *head);
 

因为Linux链表是循环表,且表头的next、prev分别指向链表中的第一个和最末一个节点,所以,list_add和list_add_tail的区别并不大,实际上,Linux分别用


__list_add(new, head, head->next);
 


__list_add(new, head->prev, head);
 

来实现两个接口,可见,在表头插入是插入在head之后,而在表尾插入是插入在head->prev之后。

假设有一个新nf_sockopt_ops结构变量new_sockopt需要添加到nf_sockopts链表头,我们应当这样操作:


list_add(&new_sockopt.list, &nf_sockopts);
 

从这里我们看出,nf_sockopts链表中记录的并不是new_sockopt的地址,而是其中的list元素的地址。如何通过链表访问到new_sockopt呢?下面会有详细介绍。

b) 删除


static inline void list_del(struct list_head *entry);
 

当我们需要删除nf_sockopts链表中添加的new_sockopt项时,我们这么操作:


list_del(&new_sockopt.list);
 

被剔除下来的new_sockopt.list,prev、next指针分别被设为 LIST_POSITION2和LIST_POSITION1两个特殊值,这样设置是为了保证不在链表中的节点项不可访问--对 LIST_POSITION1和LIST_POSITION2的访问都将引起页故障。与之相对应,list_del_init()函数将节点从链表中解下来之后,调用LIST_INIT_HEAD()将节点置为空链状态。

c) 搬移

Linux提供了将原本属于一个链表的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类:


static inline void list_move(struct list_head *list, struct list_head *head);
static inline void list_move_tail(struct list_head *list, struct list_head *head);
 

例如list_move(&new_sockopt.list,&nf_sockopts)会把new_sockopt从它所在的链表上删除,并将其再链入nf_sockopts的表头。

d) 合并

除了针对节点的插入、删除操作,Linux链表还提供了整个链表的插入功能:


static inline void list_splice(struct list_head *list, struct list_head *head);
 

假设当前有两个链表,表头分别是list1和list2(都是struct list_head变量),当调用list_splice(&list1,&list2)时,只要list1非空,list1链表的内容将被挂接在list2 链表上,位于list2和list2.next(原list2表的第一个节点)之间。新list2链表将以原list1表的第一个节点为首节点,而尾节点不变。如图(虚箭头为next指针):

图4 链表合并list_splice(&list1,&list2)
 list_head <wbr>内核

当list1被挂接到list2之后,作为原表头指针的list1的next、prev仍然指向原来的节点,为了避免引起混乱,Linux提供了一个list_splice_init()函数:


 static inline void list_splice_init(struct list_head *list, struct list_head *head);
 

该函数在将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链。

3. 遍历
遍历是链表最经常的操作之一,为了方便核心应用遍历链表,Linux链表将遍历操作抽象成几个宏。在介绍遍历宏之前,我们先看看如何从链表中访问到我们真正需要的数据项。

a) 由链表节点到数据项变量

我们知道,Linux链表中仅保存了数据项结构中list_head成员变量的地址,那么我们如何通过这个list_head成员访问到作为它的所有者的节点数据呢?Linux为此提供了一个list_entry(ptr,type,member)宏,其中ptr是指向该数据中list_head成员的指针,也就是存储在链表中的地址值,type是数据项的类型,member则是数据项类型定义中list_head成员的变量名,例如,我们要访问nf_sockopts链表中首个nf_sockopt_ops变量,则如此调用:


list_entry(nf_sockopts->next, struct nf_sockopt_ops, list);
 

这里"list"正是nf_sockopt_ops结构中定义的用于链表操作的节点成员变量名。

list_entry的使用相当简单,相比之下,它的实现则有一些难懂:


#define list_entry(ptr, type, member) container_of(ptr, type, member)
container_of宏定义在[include/linux/kernel.h]中:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
offsetof宏定义在[include/linux/stddef.h]中:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
 

size_t最终定义为unsigned int(i386)。

这里使用的是一个利用编译器技术的小技巧,即先求得结构成员在与结构中的偏移量,然后根据成员变量的地址反过来得出属主结构变量的地址。

container_of()和offsetof()并不仅用于链表操作,这里最有趣的地方是 ((type *)0)->member,它将0地址强制"转换"为type结构的指针,再访问到type结构中的member成员。在container_of 宏中,它用来给typeof()提供参数(typeof()是gcc的扩展,和sizeof()类似),以获得member成员的数据类型;在 offsetof()中,这个member成员的地址实际上就是type数据结构中member成员相对于结构变量的偏移量。

如果这么说还不好理解的话,不妨看看下面这张图:

图5 offsetof()宏的原理
图5 offsetof()宏的原理 

对于给定一个结构,offsetof(type,member)是一个常量,list_entry()正是利用这个不变的偏移量来求得链表数据项的变量地址。

b) 遍历宏

在[net/core/netfilter.c]的nf_register_sockopt()函数中有这么一段话:


 ……
 struct list_head *i;
……
list_for_each(i, &nf_sockopts) {
 struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i;
……
}
……
 

函数首先定义一个(struct list_head *)指针变量i,然后调用list_for_each(i,&nf_sockopts)进行遍历。在[include/linux/list.h]中,list_for_each()宏是这么定义的:

list_head 一般用法以及例子

list_head结构的介绍
list_head结构定义在里,它是一个double linked list的结构。底下是它的结构宣告:

struct list_head {
struct list_head *next, *prev;
};
 

有的人可能看到这样的结构会觉得很奇怪这样的结构可以存放资料吗? 当然是不行的棉,因为这个结构根本是拿来让人当资料存的。首先, 我们先来看看两个macro,

 #define LIST_HEAD(name) \
struct list_head name &name, &name }
#define INIT_LIST_HEAD(ptr) do \
(ptr)->next (ptr); (ptr)->prev (ptr); \
while (0)
 

这两个macro在Kernel里也算蛮常出现的,是用来将list_head做初始化的,它的初始化就是将next和prev这两个栏位设为跟结构的地址相同。所以, 如果我们在程序里看到这样的程序,它的意思就是宣告一个list_head结构的变数hello,并将prev和next都设成hello的地址。

 LIST_HEAD(hello) 

因此, 如果要检查这个list是否是空的,只要检查hello.next是否等于&hello就可以了。事实上, Linux也提供了一个叫list_empty()的函式来检查list是否为空的。

 static __inline__ int list_empty(struct list_head *head)
{
return head->next == head;
}
 

现在我们来介绍如何加入或删除list_head到上面的hello串行里。 Linux提供二个函式来做这些事,分别是list_add()和lis_del()。这两个函式的定义都放在 里,而且其程序码也都很简单,只是单纯double linked list的串接和删除而已,因此我们不对它们做介绍。 有关于这个结构,其实最重要的应该是它提供的这个macro。

 #define list_entry(ptr, type, member) \
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
 

 list_head <wbr>一般用法以及例子500){this.resized=true;this.style.width=500;}">

我们现在来做个实验,相信各位会更容易了解这个macro的。请看一下下面这段程序码。

 struct HelloWorld {
   int x, y;
   struct list_head list;
hello;
 

假设int是4个byte。 那么以下这一行会得到8,如图5所示

 (unsigned long) (&((struct HelloWorld *)0)->list) 

有的人会对这一行程序感到奇怪, (struct HelloWorld*)0不就是一个NULL的指标吗? 怎么可以用0->list去参考list这个栏位呢? 难道不怕造成segmentation fault吗? 请注意一下,我们在0->list的前面还加上了一个&。如果没有&,那上面这一行就会segmentation fault了。如果你加上了&, 那就没问题棉。 Segmentation fault通常是去参考到不合法的记忆体地址内容所造成的,如果我们加上了&就表示我们没有要去参考这个不合法地址的内容,我们只是要那个栏位的地址而已,因此, 不会造成segmentation fault。 其实,结构的配置在记忆体里是连续的。 所以,如果我们去读取某个栏位时,像&hello->list。会先取得hello变数的地址,再然后再计算HelloWorld结构里list栏位所在的offset,再将hello的地址加上list栏位的offset,求得list栏位真正的地址。然后再去读list栏位的内容。这是compiler帮我们做的。那我们现在就来看看上面那一行究竟是什么意思。首先,我们先把上面那一行想象成下面这个样子。

 ptr 0;
(unsigned long) (&((struct HelloWorld *)ptr)->list)
 

这样是不是容易懂了吗,就是要取得&ptr->list的地址而已。所以,如果ptr是100的话, 那会得到100+8=108。因为前面有二个int, 每一个int是4个byte。经过转型, 就得到了(unsigned long)型态的108。如果ptr是0的话, 那同理, 我们会得到0+8=8。也就是这个栏位在HelloWorld结构里的offset。

现在,如果我们已经知道了list在HelloWorld结构中的offset,而且我们现在也知道hello这个变数里list的地址的话,那有没有办法得到hello本身的地址呢? 可以的,就像图6一样, 如果我们知道list的地址,只要将list的地址减8就可以知道了hello的地址了嘛。

 struct list_head *plist &hello.list;
printf( "&hello %x\n", (char*)plist (unsigned long) ));
 

而这种方式就是list_head的用法,它是专门用来当作别的结构的栏位,只要我们得到这个栏位的位置和包含这个栏位的结构是那一种,我们可以很轻易的算出包含此栏位的结构地址,图6就是super block在使用list_head所得到的结果。只要我们知道s_list的地址,只要呼叫

 list_entry( &sb1.s_list, struct super_block, s_list) 

就可以得到其sb1这个super_block结构的地址。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值