转至繁体中文版     | 网站首页 | 图文教程 | 资源下载 | 站长博客 | 图片素材 | 武汉seo | 武汉网站优化 | 
最新公告:     敏韬网|教学资源学习资料永久免费分享站!  [mintao  2008年9月2日]        
您现在的位置: 学习笔记 >> 图文教程 >> 数据库 >> SyBase >> 正文
深入分析 Linux 内核链表         ★★★★

深入分析 Linux 内核链表

作者:闵涛 文章来源:闵涛的学习笔记 点击数:1342 更新时间:2009/4/22 23:07:44
级别: 中级

杨沙洲 (pubb@163.net
国防科技大学计算机学院

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

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

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

1. 单链表

图 1 单链表


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

2. 双链表

图 2 双链表


通过设计前驱和后继两个指针域,双链表可以从两个方向遍历,这是它区别于单链表的地方。如果打乱前驱、后继的依赖关系,就可以构成"二叉树";如果再让首节点的前驱指向链表尾节点、尾节点的后继指向首节点(如图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++ 程序员应该知道,标准模板库中的 采用的是 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 链表示意图


三、 链表操作接口

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)


当 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 结构中定义的用于链表操作的节点成员变量名。

 
#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() 宏的原理


对于给定一个结构,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() 宏是这么定义的:

 
#define list_for_each(pos, head) \
for (pos = (head)->next, prefetch(pos->next); pos != (head); \
pos = pos->next, prefetch(pos->next))


它实际上是一个 for 循环,利用传入的 pos 作为循环变量,从表头 head 开始,逐项向后(next 方向)移动 pos,直至又回到 head(prefetch() 可以不考虑,用于预取以提高遍历速度 )。

那么在 nf_register_sockopt() 中实际上就是遍历 nf_sockopts 链表。为什么能直接将获得的 list_head 成员变量地址当成 struct nf_sockopt_ops 数据项变量的地址呢?我们注意到在 struct nf_sockopt_ops 结构中,list是其中的第一项成员,因此,它的地址也就是结构变量的地址。更规范的获得数据变量地址的用法应该是:

 
struct nf_sockopt_ops *ops = list_entry(i, struct nf_sockopt_ops, list);


大多数情况下,遍历链表的时候都需要获得链表节点数据项,也就是说 list_for_each()和list_entry() 总是同时使用。对此 Linux 给出了一个 list_for_each_entry() 宏:

 
#define list_for_each_entry(pos, head, member) ……


与 list_for_each() 不同,这里的pos是数据项结构指针类型,而不是 (struct list_head *)。nf_register_sockopt() 函数可以利用这个宏而设计得更简单:

 
……
struct nf_sockopt_ops *ops;
list_for_each_entry(ops,&nf_sockopts,list){
……
}
……


某些应用需要反向遍历链表,Linux 提供了 list_for_each_prev() 和 list_for_each_entry_reverse() 来完成这一操作,使用方法和上面介绍的 list_for_each()、list_for_each_entry() 完全相同。

如果遍历不是从链表头开始,而是从已知的某个节点 pos 开始,则可以使用 list_for_each_entry_continue(pos,head,member)。有时还会出现这种需求,即经过一系列计算后,如果 pos 有值,则从 pos 开始遍历,如果没有,则从链表头开始,为此,Linux 专门提供了一个 list_prepare_entry(pos,head,member) 宏,将它的返回值作为 list_for_each_entry_continue() 的 pos 参数,就可以满足这一要求。

4. 安全性考虑
在并发执行的环境下,链表操作通常都应该考虑同步安全性问题,为了方便,Linux 将这一操作留给应用自己处理。Linux 链表自己考虑的安全性主要有两个方面:

a) list_empty() 判断

基本的 list_empty() 仅以头指针的 next 是否指向自己来判断链表是否为空,Linux 链表另行提供了一个 list_empty_careful() 宏,它同时判断头指针的 next 和 prev,仅当两者都指向自己时才返回真。这主要是为了应付另一个 cpu 正在处理同一个链表而造成 next、prev 不一致的情况。但代码注释也承认,这一安全保障能力有限:除非其他 cpu 的链表操作只有 list_del_init(),否则仍然不能保证安全,也就是说,还是需要加锁保护。

b) 遍历时节点删除

[1] [2]  下一页


[C语言系列]C# 和 Linux 时间戳转换  [Web开发]PHP flock文件锁介绍
[Web开发]flock() Linux下的文件锁  [电脑应用]Linux下的六个免费的虚拟主机管理系统介绍
[电脑应用]Linux数据库大比拚  [操作系统]在Windows中玩转Linux操作系统
[办公软件]在RedHat Linux 9里安装gaim0.80  [办公软件]掌握 Linux 调试技术
[办公软件]理解 Linux 配置文件  [聊天工具]Real10 & Xpdf installation on Linux Box
教程录入:mintao    责任编辑:mintao 
  • 上一篇教程:

  • 下一篇教程:
  • 【字体: 】【发表评论】【加入收藏】【告诉好友】【打印此文】【关闭窗口
      注:本站部分文章源于互联网,版权归原作者所有!如有侵权,请原作者与本站联系,本站将立即删除! 本站文章除特别注明外均可转载,但需注明出处! [MinTao学以致用网]
      网友评论:(只显示最新10条。评论内容只代表网友观点,与本站立场无关!)

    同类栏目
    · Sql Server  · MySql
    · Access  · ORACLE
    · SyBase  · 其他
    更多内容
    热门推荐 更多内容
  • 没有教程
  • 赞助链接
    更多内容
    闵涛博文 更多关于武汉SEO的内容
    500 - 内部服务器错误。

    500 - 内部服务器错误。

    您查找的资源存在问题,因而无法显示。

    | 设为首页 |加入收藏 | 联系站长 | 友情链接 | 版权申明 | 广告服务
    MinTao学以致用网

    Copyright @ 2007-2012 敏韬网(敏而好学,文韬武略--MinTao.Net)(学习笔记) Inc All Rights Reserved.
    闵涛 投放广告、内容合作请Q我! E_mail:admin@mintao.net(欢迎提供学习资源)

    站长:MinTao ICP备案号:鄂ICP备11006601号-18

    闵涛站盟:医药大全-武穴网A打造BCD……
    咸宁网络警察报警平台