编写函数:把两个链表函数合并成一个链表函数,并返回新链表函数的表头(每个节点定义为职工信息,包括姓名和工资)

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

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

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

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

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

在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],以下所有代码除非加以说明,其余均取自该文件):

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

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

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

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

list)成员各个协议族的nf_sockopt_ops结构都通过这个list成员组織在一个链表函数中,表头是定义在[net/core/netfilter.c]中的nf_sockopts(struct list_head)从下图中我们可以看到,这种通用的链表函数结构避免了为每个数据项类型定义自己的链表函数的麻烦Linux的简捷实用、不求完美和标准的风格,在这里体现得相当充分

实际上Linux只定义了链表函数节点,并没有专门定义链表函数頭那么一个链表函数结构是如何建立起来的呢?让我们来看看LIST_HEAD()这个宏:

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

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

2. 插入/删除/合并

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

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

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

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

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

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

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

假设当前有两个链表函数,表头分别是list1和list2(都是struct

遍历是链表函数最经常的操莋之一为了方便核心应用遍历链表函数,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"正是nf_sockopt_ops结构中定义的鼡于链表函数操作的节点成员变量名。

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

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

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

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


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

nf_sockopt_ops结构中,list是其中的第一项成员因此,它的地址也就是结构变量的地址更规范的获得数据变量地址的用法应该是:


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

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

前面介绍了用于链表函数遍历的几个宏,它们都是通过移动pos指针来达到遍历的目的但如果遍历的操作中包含删除pos指针所指向的节点,pos指针的移动就会被中断因为list_del(pos)将把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。

member)它們要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址避免因pos节点被释放而造成的断链。

精益求精的Linux链表函数设計者(因为list.h没有署名所以很可能就是Linus Torvalds)认为双头(next、prev)的双链表函数对于HASH表来说"过于浪费",因而另行设计了一套用于HASH表应用的hlist数据结构--單指针表头双循环链表函数从上图可以看出,hlist的表头仅有一个指向首节点的指针而没有指向尾节点的指针,这样在可能是海量的HASH表中存储的表头就能减少一半的空间消耗

因为表头和节点的数据结构不同,插入操作如果发生在表头和首节点之间以往的方法就行不通了:表头的first指针必须修改指向新插入的节点,却不能使用类似list_add()这样统一的描述为此,hlist节点的prev不再是指向前一个节点的指针而是指向前一個节点(可能是表头)中的next(对于表头则是first)指针(struct list_head

在Linux链表函数功能接口中还有一系列以"_rcu"结尾的宏,与以上介绍的很多函数一一对应RCU(Read-Copy Update)是2.5/2.6内核中引入的新技术,它通过延迟写操作来提高同步性能

我们知道,系统中数据读取操作远多于写操作而rwlock机制在smp环境下随着处理機增多性能会迅速下降(见参考资料4)。针对这一应用背景IBM Linux技术中心的Paul E. McKenney提出了"读拷贝更新"的技术,并将其应用于Linux内核中RCU技术的核心是寫操作分为写-更新两步,允许读操作在任何时候无阻访问当系统有写操作时,更新动作一直延迟到对该数据的所有读操作完成为止Linux链表函数中的RCU功能只是Linux RCU的很小一部分,对于RCU的实现分析已超出了本文所及有兴趣的读者可以自行参阅本文的参考资料;而对RCU链表函数的使鼡和基本链表函数的使用方法基本相同。

版权声明:本文为博主原创文章遵循 版权协议,转载请附上原文出处链接和本声明

本题要求实现一个合并两个有序链表函数的简单函数。链表函数结点定义如下:


      


      

其Φlist1list2是用户传入的两个按data升序链接的链表函数的头指针;函数mergelists将两个链表函数合并成一个按data升序链接的链表函数并返回结果链表函数的頭指针。

 
/* 你的代码将被嵌在这里 */

      

      

    

我要回帖

更多关于 链表函数 的文章

 

随机推荐