深入分析linux内核链表

上传人:j****9 文档编号:47065907 上传时间:2018-06-29 格式:DOC 页数:10 大小:100KB
返回 下载 相关 举报
深入分析linux内核链表_第1页
第1页 / 共10页
深入分析linux内核链表_第2页
第2页 / 共10页
深入分析linux内核链表_第3页
第3页 / 共10页
深入分析linux内核链表_第4页
第4页 / 共10页
深入分析linux内核链表_第5页
第5页 / 共10页
点击查看更多>>
资源描述

《深入分析linux内核链表》由会员分享,可在线阅读,更多相关《深入分析linux内核链表(10页珍藏版)》请在金锄头文库上搜索。

1、深入分析深入分析 Linux 内核链表内核链表 本文详细分析了 2.6.x 内核中链表结构的实现,并通过实例对每个链表操作接口进行了详尽的讲解。一、 链表数据结构简介链表是一种常用的组织有序数据的数据结构,它通过指针将一系列数据节点连接成一条数据链,是线性表的一种重要实现方式。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系

2、形式,链表又可以分为单链表、双链表、循环链表等多种类型,下面分别给出这几类常见链表类型的示意图:1 单链表单链表图图 1 单链表单链表单链表是最简单的一类链表,它的特点是仅有一个指针域指向后继节点(next),因此,对单链表的遍历只能从头至尾(通常是 NULL 空指针)顺序进行。2 双链表双链表图图 2 双链表双链表通过设计前驱和后继两个指针域,双链表可以从两个方向遍历,这是它区别于单链表的地方。如果打乱前驱、后继的依赖关系,就可以构成“二叉树“;如果再让首节点的前驱指向链表尾节点、尾节点的后继指向首节点(如图 2 中虚线部分),就构成了循环链表;如果设计更多的指针域,就可以构成各种复杂的树状

3、数据结构。3 循环链表循环链表循环链表的特点是尾节点的后继指向首节点。前面已经给出了双循环链表的示意图,它的特点是从任意一个节点出发,沿两个方向的任何一个,都能找到链表中的任意一个数据。如果去掉前驱指针,就是单循环链表。在 Linux 内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在include/linux/list.h实现的一个相当精彩的链表数据结构。本文的后继部分就将通过示例详细介绍这一数据结构的组织和使用。二、 Linux 2.6 内核链表数据结构的实现尽管这里使用 2.6 内核作为讲解的基础,但实际上 2.4 内核中的链表结构和 2.

4、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,由此可见,内核的链表具备双链表功能,实际上

5、,通常它都组织成双循环链表。和第一节介绍的双链表结构模型不同,这里的 list_head 没有数据域。在 Linux 内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。在数据结构课本中,链表的经典定义方式通常是这样的(以单链表为例):struct list_node struct list_node *next; ElemTypedata; ;因为 ElemType 的缘故,对每一种数据项类型都需要定义各自的链表结构。有经验的 C+程序员应该知道,标准模板库中的采用的是 C+ Template,利用模板抽象出和数据项类型无关的链表操作接口。在 Linux 内核链表中,需要用链

6、表组织起来的数据通常会包含一个 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)。从下图中我们可以看到,这种通用的链表结构避免了为每个数

7、据项类型定义自己的链表的麻烦。Linux 的简捷实用、不求完美和标准的风格,在这里体现得相当充分。图图 3 nf_sockopts 链表示意图链表示意图三、 链表操作接口1. 声明和初始化声明和初始化实际上 Linux 只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的呢?让我们来看看 LIST_HEAD()这个宏:#define LIST_HEAD_INIT(name) 除了用 LIST_HEAD()宏在声明的时候初始化一个链表以外,Linux 还提供了一个 INIT_LIST_HEAD 宏用于运行时初始化链表:#define INIT_LIST_HEAD(ptr) d

8、o (ptr)-next = (ptr); (ptr)-prev = (ptr); while (0)我们用 INIT_LIST_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, hea

9、d-prev, head);来实现两个接口,可见,在表头插入是插入在 head 之后,而在表尾插入是插入在 head-prev 之后。假设有一个新 nf_sockopt_ops 结构变量 new_sockopt 需要添加到 nf_sockopts 链表头,我们应当这样操作:list_add(从这里我们看出,nf_sockopts 链表中记录的并不是 new_sockopt 的地址,而是其中的 list 元素的地址。如何通过链表访问到 new_sockopt 呢?下面会有详细介绍。b) 删除删除 static inline void list_del(struct list_head *entr

10、y);当我们需要删除 nf_sockopts 链表中添加的 new_sockopt 项时,我们这么操作:list_del(被剔除下来的 new_sockopt.list,prev、next 指针分别被设为 LIST_POSITION2 和 LIST_POSITION1 两个特殊值,这样设置是为了保证不在链表中的节点项不可访问-对 LIST_POSITION1 和 LIST_POSITION2 的访问都将引起页故障。与之相对应,list_del_init()函数将节点从链表中解下来之后,调用 LIST_INIT_HEAD()将节点置为空链状态。c) 搬移搬移 Linux 提供了将原本属于一个链表

11、的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类: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(假设当前有两个链表,表头分别是 list1 和 list2(都是 struct list_head 变量),当调用 list_splice(该函数在将 list 合并到 head 链表的基础

12、上,调用 INIT_LIST_HEAD(list)将 list 设置为空链。3. 遍历遍历遍历是链表最经常的操作之一,为了方便核心应用遍历链表,Linux 链表将遍历操作抽象成几个宏。在介绍遍历宏之前,我们先看看如何从链表中访问到我们真正需要的数据项。a) 由链表节点到数据项变量由链表节点到数据项变量 我们知道,Linux 链表中仅保存了数据项结构中 list_head 成员变量的地址,那么我们如何通过这个 list_head 成员访问到作为它的所有者的节点数据呢?Linux 为此提供了一个 list_entry(ptr,type,member)宏,其中 ptr 是指向该数据中 list_he

13、ad 成员的指针,也就是存储在链表中的地址值,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) c

14、ontainer_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) list_f

15、or_each(i, 函数首先定义一个(struct list_head *)指针变量 i,然后调用 list_for_each(i, pos != (head); pos = pos-next, prefetch(pos-next)它实际上是一个 for 循环,利用传入的 pos 作为循环变量,从表头 head 开始,逐项向后(next 方向)移动 pos,直至又回到 head(prefetch()可以不考虑,用于预取以提高遍历速度)。那么在 nf_register_sockopt()中实际上就是遍历 nf_sockopts 链表。为什么能直接将获得的 list_head 成员变量地址当成

16、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()函数可

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 中学教育 > 初中教育

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号