linux内核链表最细讲解

目录

1:简介内核链表

2.链表的初始化

2.1宏定义初始化链表

2.2接口初始化链表

3.添加节点__list_add

3.1头部插入list__add

3.2尾部插入list_add_tail

4.删除操作__list_del

5.替换 list_replace

 6.移动

6.1头部移动 list_move

6.2尾部移动

8.list_entry宏


1:简介内核链表

       顾名思义内核链表也是链表的一种,即其在物理储存单元上是非连续的结构,但在逻辑上是成线性关系而形成的顺序链,以指针链接。内核链表也是如此,内核链表为双循环链表模式,但相较于寻常双循环链表不同的是,内核线性链表将其中的两个指针单独拿出创建了小结构体,再将小结构体放入大结构体当中形成了十分精彩的链表结构。

2.链表的初始化

      在linux内核中使用了大量的链表来组织数据,因此内核也提供了多种方式来初始化链表。

2.1宏定义初始化链表

       我们熟知的带参数的宏定义如 #define <宏名> (参数表)    <宏体>                                                 宏名是我们自己定的一个标识符如以上两个宏举例宏名分别为LIST_HEAD_INIT 和 LIST_HEAD 而参数表中的参数可以是一个也可是多个,宏体这是被宏名代替的表达式或字符串。

       第一个宏我们可以将其理解为我们定义了一个结构体其结构体内部包含了两个同类型结构体的地址且都指向自己,第二个宏本质是赋予了name (struct list_hand)的属性,由于list_hand不包含数据域,并而LIST_HEAD__ INT  和  LIST_HEAD这两个宏本身不包含任何数据类型,使得整个链表十分灵活,具有很强的可维护性和通用性十分精妙,但就宏定义平常调用而言并不好用,故有些人称其狗都不用。

2.2接口初始化链表

 如上所示接口的初始化更容易被我们看懂,其在静态区定义了内联函数INIT_LIST_HEAD其目的和宏定义一样都是都是将链表的前驱和后驱两个指针都指向自己。因为list_hand其只有指针域而没有数据域故我们应该创造一个结构体其即包含数据域也包含指针域,如下所示。

3.添加节点__list_add

Linux内核还提供给我们了非常精彩的添加节点的函数

 上图表示的是添加新节点的操作部分具体操作如下

       新添加了一个new节点将prev和next分别和new链接到了一起,prev下一个节点是new,new的下一个节点是next。                                                                                                                                   正是因为有了这个函数我们之后无论在哪里添加新的节点我们只需要将新节点以及新节点的前面后面的地址传过来即可,十分方便。

3.1头部插入list__add

 

通过上面的分析这里我们可以清晰的知道我们将hand作为了prve,将head->next即hand后面的解点作为了next,而我们将新建出来的new插入其中,完成头插操作。

3.2尾部插入list_add_tail

 这里和头部插入相同同样都是调用了_list_add函数。

我们这里将头的前驱hand->prev即链表的最后一位当作prve,将头hand当做next,由于这里是双循环链表,因此将new插入头尾之间完成尾插操作。

 小结

总结由于是双向循环链表其本质是一个顺时针和一个逆时针的两个环所以说无论我们是在头部插入还是尾部插入亦或是中间插入,我们都可以将其看作在中间任何地方插入一个新的节间,于是我们便可以将单独拿出来封装函数,我们只需要把握好我们所插入位置的上一位和下一位即可。

4.删除操作__list_del

同添加操作一样我们也可以将删除操作看作我们在任何一个位置删掉一个节点,故我们可以将删除操作单独拿出封装一个函数如上图所示。

 我们这里的操作直接将prev和next 链接到一起,prev的下一个节点为next,next的上一个节点为prev。

将entry的前节点和后节点链接,entry掉出链表。

       这里LIST_POISON1 和 LIST_POISON2分别表示((void *) 0x00100100 + 0)和 ((void *) 0x00200200 + 0)这两个特殊的值这样设置表明了链表中的节点不可访问,访问会引起页故障(页故障:是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。)我们这里简单的将其理解为置NULL即可。              以上操作便是删除操作,但我们要记得如果是自己写内核链表时一定要在删除操作中释放掉你malloc或者realloc出来的既包含数据域也包含指针域的空间,如果不释放则会导致内存泄露造成巨大风险。

5.替换 list_replace

 

 new节点完全的继承了old的前驱和后驱 ,同时让old前面的结点的后驱指向new,让old后面节点的前驱指向new,完美的替换了old节点。

这里new节点 替换 old节点 ,之后对old节点做初始化操作即  自己--->指自己。

 6.移动

6.1头部移动 list_move

我们第一步将list节点截切出来即用删除操作令其掉出链表,之后我们将list节点插在head的后面完成了对list节点的移动。

6.2尾部移动

 同理我们第一步将list节点截切出来,即用删除操作令其掉出链表,之后我们将list节点插在head的前面完成了对list节点的移动。

 7.遍历

Linux内核还为我们提供了遍历操作

 判断某个节点是否为最后一个节点,如果是则返回1   如果不是则返回0

8.list_entry宏

遍历的关键就是list_entry这个宏,同时也是container_of宏。

 由此可知两个宏是相同的

 

 我们接下来挨个分析,但在分析前我们先介绍一下typeof()和一些名称分别表示什么,typeof() 是gcc的扩展宏,给定一个参数或者变量名,能够自动推导数据类型。

我们接下来介绍第一个分号前的内容((type*)0)这里是将0转换成大结构体指针类型         typeof(((type*)0)->member)是通过大结构体指针类型指向小结构体通过typeof获得小结构体的类型, typeof(((type*)0)->member)*_mptr=(ptr)这是通过获得的小结构体类型创建的_mptr小结构体指针并且将ptr的地址给其赋初值。

之后是第二个分号宏定义的内容 (char*)_mptr很好理解就是将我们刚刚得到的小结构体指针类型强转成char*类型让其每次只能移动一个字节,((TYPE*)0)和上文一样这里是将0转换成大结构体类型,而((TYPE*)0)->MEMBER是将大结构体指针指向小结构体。                        ((size_t)&((TYPE*)0)->MEMBER)是取到指向小结构体的地址,将这个地址强转成无符号整型变量,从而获得了小结构体和大结构体之间的偏移量。由于很多人不理解我们画图说明。

 由于知道了大小结构体之间的偏移量我们又将小结构体转变成(char*)类型使其一个字节一个字节移动故(char*)_mptr-((size_t)&((TYPE*)0)->MEMBER)是将我们小结构体的地址减去其偏移量得到大结构体变量的首地址,(type*)(char*)_mptr-((size_t)&((TYPE*)0)->MEMBER)之后在将其强转为大结构体指针类型。

总结

list_entry(ptr, type, member) 
 通过小结构体指针类型ptr  得到 大结构体指针类型

第一次写博客写的不是太好,如果有错误欢迎大家指正。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值