list.h之我见

 

      链表是一种常见的、组织有序数据的数据结构,它通过指针将一系列的数据节点连接成一条数据链,是线性表的一种实现方式。与数组相比,链表具有更好的动态性,建立联表示无须知道结点的总数,可以随机分配空间,可以高效的链表中的任意位置插入或者删除数据。按照结点指针域的组织以及各节点之间的联系方式,链表又可以分为单链表、循环链表、双向循环链表等多种类型。以下是常见的这几种链表的结点的数据结构。
单链表:每个结点仅有一个指针指向后继结点。

双链表:每个结点都有一个指向前驱结点的指针和一个指向后继结点的指针,但链表的首尾不相接,所以首结点的前驱为空,尾结点的后继为空。


双向循环链表:每个结点都有一个指向前驱结点的指针和一个指向后继结点的指针,且首尾相接。

在linux内核中使用了大量的链表结构来组织数据,那么内核链表是如何定义和操作的呢?我们现在就走进list.h文件,赶快学习一下吧!
首先看一下list_head结构体:

struct list_head是表示双向链表的结点的结构体,它里面没有定义数据域,只包含指针域。它将作为另一个结构体的成员,该结构体包含数据域。
双向链表的定义:

list字段,隐藏了链表的指针特性,但正是它,把我们要链接的数据组织成了链表。
接着,看看对链表可以进行那些操作。
1. 初始化链表

INIT_LIST_HEAD函数将链表初始化为一个空链表,当前链表只含有头结点,头结点的prev和next域均指向头结点,其值等于头指针list。

第二个宏是一个嵌套定义的宏,它在定义一个新的宏的时候调用了已定义的第一个宏。宏#define LIST_HEAD(name)生成了一个头结点,宏#define LIST_HEAD_INIT(name)是对头结点name所表示的链表初始化,即将name的地址直接分别赋给那name.prevname.next,形成一个空链表。因此,宏LIST_HEAD(name)的作用就是定义并初始化一个空的双向循环链表。
2. 链表结点的插入
在内核中有两个插入函数:

以上两个插入函数均调用了下面的函数:

下面我们来看一下list_add和list_add_tail函数:

可见,这两个函数通过巧妙地调用_list_add函数来实现元素的头插和尾插。
3. 链表节点的删除

该函数分别让欲删除结点的prev和next结点越级指向彼此。
但这个函数并不是正式的删除函数,它只是一个被调用的函数,调用函数即具体的删除结点函数如下,entry为要删除的节点的指针。

这个函数属于不安全的删除,想要安全的删除,那么请调用下面的函数,它最后会让entry的prev和next指针均指向自己。

4. 链表结点的替换
替换操作很简单,实质就是在old->prev和old->next之间插入new结点。

若要安全替换,可以调用:

5. 链表结点的移动
移动就是删除和增加的复合,将一个节点移动到链表中的指定位置。list_move函数最 终调用的是__list_add(list,head,head->next),实现将list移动到头结点之后;而list_move_tail 函数最终调用__list_add_tail(list,head->prev,head),实现将list节点移动到链表末尾。

6. 测试函数
接下来的几个测试函数,可以见名之意。
list_is_last函数是测试结点list是否为链表head的最后一个节点。

下面的两个函数是测试head链表是否为空链表。

list_empty_careful函数,他比list_empty函数"仔细"在那里呢?前者只 是认为只要一个结点的next指针指向头指针就算为空,但是后者还要去检查头节点的prev指针是否也指向头结点。另外,这种仔细也是有条件的,只有当其 他cpu的链表操作只list_del_init()时,否则仍然不能保证安全。

下面的函数list_is_singular是测试head链表是否只有一个结点:这个链表既不能是空而且head前后的两个结点都得是同一个结点。

7. 链表的分割(一分为二)
下面的函数是将原链表头结点head后至entry(包括entry结点)之间的所有结点与entry后面的其它结点切割开,使他们成为一个以list为头结点的新链表。但事实切割之前我们必须进行判断,如果head本身是一个空链表则失败;如果head是一个单节点链表也失败;如果entry恰好就是头结点head,那么直接初始化list,因为根据切割规则得到的链表将是一个空链表;如果这些条件都不符合,那么就放心的切割吧!

具体的切割代码请看下面的函数:

 

8. 链表的合并
下面是一个基本的合并函数,它的功能是将list链表(不包括头结点)插入到另一个链表的prev和next两结点之间。

 

理解了基本的合并函数,那么将它封装起来就得到了下面的两个合并函数。这里的合并类似于添加和删除功能,有头部合并和尾部合并。

合并两个链表后,list还指向原来的链表,因此应该初始化,在上述两个函数末尾添加一句INIT_LIST_HEAD(list);后,就安全了。
故安全的合并便是下面的两个函数:

9. 链表的遍历
下面我们来学习内核链表的遍历,在list.h文件中有许多的遍历链表的宏,开始看的时候可能有点头大,但从前面的分析的思想来看,我们只要抓住基本的函数,再对基本的函数进行封装便得到扩展函数,那么宏也是如此。
基本宏,pos为索引,head为链表头指针。

但这个宏只是不停的移动索引,得到新的结构体的指针,那么与该结构体组成的新结构体中的数据成员怎么得到呢?

           
要找到数据结点的位置,必须明白下面的宏:

其中ptr为list_head结构体指针,type为你所定义的结构体类型,member是结构体中list_head结构体成员变量的名。type的作用是为了强制转换,即宏中两次用到(type *)指针ptr指向结构体type中的成员member;通过指针ptr,返回结构体type的起始地址,如图:

           

为了方便大家阅读,我把上面的结构体写成这样

这个表达式最终的结果就是type 型的地址,((size_t) &(type *)0)->member)把0地址转化为type结构的指针,然后获取该结构中member成员的指针,并将其强制转换为size_t类型。于是,由于结构从0地址开始定义,因此,这样求出member的成员地址,实际上就是它在结构中的偏移量.然后ptr减去这个偏移量就得到了所要的结构体指针。
下面这个宏会得到链表中第一个结点的地址。

真正遍历的宏登场了,整个便利过程看起来很简单,可能你对prefetch()陌生,它的作用是预取节点,以提高速度。

我们再回头看一开始我们举例的那个基本的便利宏。注意它和上述便利宏的区别就是没有prefetch(),因为这个宏适合比较少结点的链表。
接下来这个遍历宏貌似长相和上面那几个稍有不同,不过理解起来也不困难,倒着(从最后一个结点)开始遍历链表。

下面两个宏是上述两个便利宏的安全版,我们看它安全在那里?它多了一个与pos同类型的n,每次将下一个结点的指针暂存起来,防止pos被释放时引起的链表断裂。

用在list_for_each宏进行遍历的时候,我们很容易得到pos,我们都知道pos存储的是当前结点前后两个结点的地址。而通过 list_entry宏可以获得当前结点的地址,进而得到这个结点中其他的成员变量。而下面两个宏则可以直接获得每个结点的地址,在for循环中,首先通过list_entry来获得第一个结点的地址;&pos->member != (head)其实就是&pos->list!=(head);它是用来检测当前list链表是否到头了;最后在利用list_entry宏 来获得下一个结点的地址。这样整个for循环就可以依次获得每个结点的地址,进而再去获得其他成员。理解了list_for_each_entry宏,那 么list_for_each_entry_reverse宏就显而易见了。

与上述宏不同的是,下面的宏是从当前pos结点开始遍历。

接下来几个宏又分别是上述几个宏的安全版。

好了,今天的学习到此结束!

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值