在开始写linux内核双向循环链表之前,我一直在想我要不要用长篇大论的文字来描述linux内核双向循环链表呢?经过认真的思考之后,我否决了用枯燥的文字向读者描述linux内核双向循环链表的想法,因为对于编程语言来说我相信大多数的读者都应该不喜欢面对枯燥的文字,更喜欢看到代码,同时那也是读者阅读文字后想要实现的东西,所以我决定在这里采用代码加上适当的文字描述的方法来进行讲解,这就使得我不可能用一篇的篇幅来讲解完,所以会写两篇文章来讲解这个知识点。希望读者能够坚持看完,学会以后在应用程序中写双向循环链表时,不用再自己去编写那些麻烦的操作函数,充分利用linux内核里已经提供的遍历链表的操作函数。本文引用地址:http://www.eepw.com.cn/article/272927.htm

特此说明:我会把我在文章中编写代码时候用到的头文件list.h上传到我的空间,免积分下载,有需要的读者可以自己去下载,当然也可以自己上网下载或者从自己安装的linux系统中得到。

懂了linux内核里双向循环链表的实现方式之后我们不得不惊叹它的实现是如此的巧妙,为了读者能够顺利的和我一起走完这次linux内核双向循环链表之旅,在此之前我特地为之写了一篇《C语言的那些小秘密之字节对齐》的文章,如果你发现在本篇文章中有些地方不懂的时候,你可以回过去看看《C语言的那些小秘密之字节对齐》再来接着继续往下继续全文的阅读。

由于我们在linux内核中有大量的数据结构都需要用到双向循环链表。若再采用以往那种传统双向循环链表的实现方式,我们不得不为这些数据结构维护各自的链表,并且为每个链表都要设计插入、查找、删除等操作函数。这是因为我们在常规链表中用来维持链表的next和prev指针都是指向对应类型的对象,因此一种数据结构的链表操作函数不能用于操作其它数据结构的链表。为了解决这个问题,在Linux内核中采用了一种与类型无关的双向循环链表实现方式,它的实现使得我们不用再为每个链表都要设计插入、查找、删除等相关的操作函数。其实现方法就是将结构体中的指针prev和next从具体的数据结构中提取出来,构成一种通用的双向循环链表数据结构list_head。如果需要构造某类对象的特定链表,则只需要在其结构体中定义一个类型为list_head类型的成员,通过这个定义的list_head类型的成员将这类对象连接起来,形成所需的双向循环链表,进而通过通用链表函数对其进行操作。显而易见是我们只需编写通用链表函数,就可构造和操作不同对象的链表,而无需为每个创建的双向循环链表编写专用函数,从而大大的实现了代码的重用。

下面我们就真正的开始我们的linux内核双向循环链表之旅。读者可以从网上下载一个linux内核双向循环链表的list.h的头文件,值得注意的就是因为内核版本的不同可能下载的头文件有些差异,但是这个并不影响我们对于它的讲解。读者可以先看完全文后再动手也不迟,用list.h头文件来实现我们的双向循环链表。为了便于讲解,我们就按照list.h头文件中代码的先后顺序进行讲解。

补充一点:(注:如果读者看不懂下面这段代码,可以继续往下看,不会影响接下来的学习,在接下来的部分还会有讲解,这部分代码是我写完全文后添加的,因为一开始我使用的是#define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))而不是#define list_entry(ptr, type, member) container_of(ptr, type, member))

[cpp] view plaincopy#define container_of(ptr, type, member) ( { \

const typeof( ((type *)0)->member ) *__mptr = (ptr); \

(type *)( (char *)__mptr - offsetof(type,member) ); } )

通过typeof( ((type *)0)->member )得到member成员的类型,将指向member的指针ptr赋值给__mptr,__mptr指针的类型为member数据成员的类型。通过(char *)__mptr将__mptr强制转换为char指针,之后再减去offsetof(type,member),即可得到宿主结构体的指针。如果有对offsetof(type,member)不懂的可以参考我之前写的一篇《C语言的那些小秘密之字节对齐》。

首先看看list_head结构的实现。

[html] view plaincopystruct list_head {

struct list_head *next, *prev;

};

在linux内核双向循环链表中我们用以上list_head类型定义一个变量,将其作为一个成员嵌入到宿主结构内。什么是宿主结构体呢?就是我们创建的双向循环链表的结构体。可以将链表结构放在宿主结构内的任何地方,当然也可以为链表结构取任何名字,从而我们就可以用list_head中的成员和相对应的处理函数来对链表进行遍历操作,如果想得到宿主结构的指针,使用我们可以使用list_entry计算出来,先别急着想知道list_entry什么,我们会在下面讲解,接着往下看。

在宿主结构体中定义了list_head之后接下来当然是要对我们定义的头结点进行初始化工作,初始化的实现方法可以有以下两种方式。

[html] view plaincopy#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \

struct list_head name = LIST_HEAD_INIT(name)

#define INIT_LIST_HEAD(ptr) do { \

(ptr)->next = (ptr); (ptr)->prev = (ptr); \

} while (0)

分析上面的代码可知,我们在代码中使用list_head定义了一个头结点之后,就要对定义的头结点进行初始化工作,可以使用INIT_LIST_HEAD(ptr)宏进行初始化,或者我们无需自己定义直接使用LIST_HEAD(name)宏即可完成定义和初始化的工作。头结点的初始化工作完成了之后接下来的工作当然是要添加节点了。

[html] view plaincopystatic inline void __list_add(struct list_head *new,

struct list_head *prev,

struct list_head *next)

{

next->prev = new;

new->next = next;

new->prev = prev;

prev->next = new;

}

__list_add()的功能是在两个非空结点中插入一个结点,值得注意的是new、prev、next均不能为空值。当然prev可以等于next,此时表示在只含头节点的链表中插入新节点。知道了__list_add()函数的实现我们接下来当然也要看看list_add()和list_add_tail()的实现。

[html] view plaincopystatic inline void list_add(struct list_head *new, struct list_head *head)

{

__list_add(new, head, head->next);

}

static inline void list_add_tail(struct list_head *new, struct list_head *head)

{

__list_add(new, head->prev, head);

}

看了上面的实现方式我们知道他们都是调用底层的__list_add()来实现的。看看在__list_add()函数里面传递不同的参数我们就能实现不同的添加节点的方法。__list_add()函数前面的双划线通常表示这是一个底层函数,供其他的模块调用。

第一个list_add()传递的参数实现的是在head和head->next两指针所指向的结点之间插入new所指向的结点。即就是在head指针的后面插入一个new所指向的结点。Head并非一定为头结点。如果我们的链表只含有一个头节点时,上面的__list_add(new, head, head->next)仍然成立。

第二个list_add_tail()其功能是在结点指针head所指向结点的前面插入new所指向的结点。当如果head指向的是头结点,那么就相当于在尾结点后面增加一个new所指向的结点。在这个函数里值得注意的是head->prev不能为空,如果head为头结点,那么head->prev要指向一个数值,一般为指向尾结点,构成循环链表。

说到这儿可能有的读者已经迫不及待的想看看代码了,那我们就用linux内核里的list.h在应用层来写出我们的代码。

[html] view plaincopy#include

#include

#include "list.h"

typedef struct _stu

{

char name[20];

int num;

struct list_head list;

}stu;

int main()

{

stu *pstu;

stu *tmp_stu;

struct list_head stu_list;

struct list_head *pos;

int i = 0;

INIT_LIST_HEAD(&stu_list);

pstu = malloc(sizeof(stu)*5);

for(i=0;i<5;i++)

{

sprintf(pstu[i].name,"Stu%d",i+1);

pstu[i].num = i+1;

list_add( &(pstu[i].list), &stu_list);

}

list_for_each(pos,&stu_list)

{

tmp_stu = list_entry(pos, stu, list);

printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

}

free(pstu);

return 0;

}

运行结果为:

[html] view plaincopyroot@ubuntu:/home/paixu/dlist_node# ./a

student num: 5 student name: Stu5

student num: 4 student name: Stu4

student num: 3 student name: Stu3

student num: 2 student name: Stu2

student num: 1 student name: Stu1

看看上面的代码,我们做的基本工作都有那些呢?

1、定义了一个宿主结构体stu,并且在宿主结构体中我们定义了一个struct list_head 类型的list变量;

2、定义一个头结点并且对其进行初始化工作;

3、对定义的一个宿主结构体变量申请内存空间;

4、对申请的宿主结构体变量初始化和添加到以stu_list为头结点的链表中。

在上面值得注意的就是list_for_each()和list_entry(),我们会在接下来的部分讲解,读者在这儿只需要知道它们两个在此合在一起的作用就是打印出宿主结构stu中每个数据。sprintf()的使用就不在这里讲解了,很简单,相信读者猜都可以猜出它的功能。读者如果一开始对上面的文字描述部分有什么疑惑或者不解的现在看了代码的实现应该都懂了,list_add_tail()的使用和list_add()类似,读者可以自己修改代码实现。如果一开始对于list_add()不太理解的读者,现在对于list_add()的理解现在可以参考运行结果和上面的文字描述部分。

我们接着往下看。

[html] view plaincopystatic inline void __list_del(struct list_head * prev, struct list_head * next)

{

next->prev = prev;

prev->next = next;

}

在prev和next指针所指向的结点之间,两者互相所指。其实也就是prev为待删除的结点的前面一个结点,next为待删除的结点的后面一个结点。

[html] view plaincopystatic inline void list_del(struct list_head *entry)

{

__list_del(entry->prev, entry->next);

entry->next = LIST_POISON1;

entry->prev = LIST_POISON2;

}

删除entry所指的结点,同时将entry所指向的结点指针域封死。在这里值得注意的是LIST_POISON1、LIST_POISON2。它们在list.h中的宏定义如下:

#define LIST_POISON1 ((void *) 0x00100100)

#define LIST_POISON2 ((void *) 0x00200200)

对LIST_POISON1、LIST_POISON2的说明,Linux 内核中有这么一句话:These are non-NULL pointers that will result in page faults under normal circumstances,used to verify that nobody uses non-initialized list entries。也就是说它们并不是空指针,但是访问这样的指针在正常情况下是会导致出错的。其实按照我们一般的思路都是把entry->next 和entry->prev 赋值为NULL,使得不可以通过该节点进行访问。但是在这里使用了一种特殊的方法。注意:我在linux环境下以上宏的值不用修改是不会出错的,但是在vc下就会出错,不允许使用那两个值,所以要修改为NULL。

[html] view plaincopystatic inline void list_del_init(struct list_head *entry)

{

__list_del(entry->prev, entry->next);

INIT_LIST_HEAD(entry);

}

以上函数的功能为删除entry所指向的结点,同时调用LIST_INIT_HEAD()把被删除结点为作为链表头构建一个新的空双循环链表。

[html] view plaincopy#include

#include

#include "list.h"

typedef struct _stu

{

char name[20];

int num;

struct list_head list;

}stu;

int main()

{

stu *pstu;

stu *tmp_stu;

struct list_head stu_list;

struct list_head *pos;

int i = 0;

INIT_LIST_HEAD(&stu_list);

pstu = malloc(sizeof(stu)*5);

for(i=0;i<5;i++)

{

sprintf(pstu[i].name,"Stu%d",i+1);

pstu[i].num = i+1;

list_add( &(pstu[i].list), &stu_list);

}

list_del(&(pstu[3].list));

printf("使用list_del()删除pstu[3]\n");

list_for_each(pos,&stu_list)

{

tmp_stu = list_entry(pos, stu, list);

printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

}

list_del_init(&(pstu[2].list));

printf("使用list_del_init()删除pstu[2]\n");

list_for_each(pos,&stu_list)

{

tmp_stu = list_entry(pos, stu, list);

printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

}

free(pstu);

return 0;

}

运行结果为:

[cpp] view plaincopyroot@ubuntu:/home/paixu/dlist_node# ./a

使用list_del()删除pstu[3]

student num: 5 student name: Stu5

student num: 3 student name: Stu3

student num: 2 student name: Stu2

student num: 1 student name: Stu1

使用list_del_init()删除pstu[2]

student num: 5 student name: Stu5

student num: 2 student name: Stu2

student num: 1 student name: Stu1

看了代码的使用之后我们再去理解之前的讲解就要轻松多了。读者可以结合上面相应的文字描述再分析下代码,以加深印象。接着往下看,坚持看完本篇博客的最后两个函数。

[html] view plaincopystatic inline void list_move(struct list_head *list, struct list_head *head)

{

__list_del(list->prev, list->next);

list_add(list, head);

}

static inline void list_move_tail(struct list_head *list,

struct list_head *head)

{

__list_del(list->prev, list->next);

list_add_tail(list, head);

}

看看上面两个函数list_move()和list_move_tail(),第一个list_move()函数的功能是把list移至head和head->next两个指针所指向的结点之间。而第二个list_move_tail()函数的功能是把list移至head和head->prev两个指针所指向的结点之间。如果读者觉得这样说不是太具体的话,那么我们来看看下面的代码。

[cpp] view plaincopy#include

#include

#include "list.h"

typedef struct _stu

{

char name[20];

int num;

struct list_head list;

}stu;

int main()

{

stu *pstu;

stu *tmp_stu;

struct list_head stu_list;

struct list_head *pos;

int i = 0;

INIT_LIST_HEAD(&stu_list);

pstu = malloc(sizeof(stu)*5);

for(i=0;i<5;i++)

{

sprintf(pstu[i].name,"Stu%d",i+1);

pstu[i].num = i+1;

list_add( &(pstu[i].list), &stu_list);

}

list_move(&(pstu[3].list),&stu_list);

printf("把pstu[3]移至head和head->next两个指针所指向的结点之间\n");

list_for_each(pos,&stu_list)

{

tmp_stu = list_entry(pos, stu, list);

printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

}

list_move_tail(&(pstu[2].list),&stu_list);

printf("把pstu[2]移至head和head->prev两个指针所指向的结点之间\n");

list_for_each(pos,&stu_list)

{

tmp_stu = list_entry(pos, stu, list);

printf("student num: %d\tstudent name: %s\n",tmp_stu->num,tmp_stu->name);

}

free(pstu);

return 0;

}

运行结果为:

[cpp] view plaincopyroot@ubuntu:/home/paixu/dlist_node# ./a

把pstu[3]移至head和head->next两个指针所指向的结点之间

student num: 4 student name: Stu4

student num: 5 student name: Stu5

student num: 3 student name: Stu3

student num: 2 student name: Stu2

student num: 1 student name: Stu1

把pstu[2]移至head和head->prev两个指针所指向的结点之间

student num: 4 student name: Stu4

student num: 5 student name: Stu5

student num: 2 student name: Stu2

student num: 1 student name: Stu1

student num: 3 student name: Stu3

在此之前先说一个注意点,以免部分读者以为结果有误,pstu[]中的下标是从0开始的,所以pstu[3]对应的是stu4。

这篇先讲到这里,余下的我们在下面一篇《C语言的那些小秘密之链表(四)》中继续讲。由于本人水平有限,博客中的不妥或错误之处在所难免,殷切希望读者批评指正。同时也欢迎读者共同探讨相关的内容,如果乐意交流的话请留下你宝贵的意见。

c语言链表head的作用,C语言的那些小秘密之链表(三)相关推荐

  1. c语言链表head的作用,c语言链表的用法

    c语言链表的用法 链表是数据结构中比较基础也是比较重要的类型之一,那么有了数组,为什么我们还需要链表呢!或者说设计链表这种数据结构的初衷在哪里?下面小编就为大家介绍下c语言链表的用法. c语言枚举的用 ...

  2. c语言程序头文件作用,C语言头文件

    C语言头文件教程 C 语言的头文件一般都是 .h 做为结尾的. C语言头文件详解 语法 #include 参数 参数 描述 filename 我们需要引入的头文件的名称. 说明 C 语言的头文件一般都 ...

  3. c语言对编程对作用,c语言编程心得体会

    c语言编程心得体会 c语言是在国内外广泛使用的一种计算机语言.以下是小编整理的c语言编程心得体会,欢迎大家阅读! c语言编程心得体会1 说到我学习c语言时,真是用千言万语呀!记得刚开始学的时候,我的c ...

  4. c语言的应用与作用,C语言主要应用在什么地方?

    C语言是一种计算机程序设计语言.它既有高级语言的特点,又具有汇编语言的特点.它可以作为系统设计语言,编写工作系统应用程序,也可以作为应用程序设计语言,编写不依赖计算机硬件的应用程序.因此,它的应用范围 ...

  5. c语言eof不起作用,c语言程序设计 怎么以输入EOF结束

    EOF(EndOfFile)是C语言中的文件结束标志符.当C语言输入函数获取到文件结尾标志时,会通过返回值的方式体现该值.所以要以输入EOF为结束,就需要判断输入函数的返回值.EOF定义在stdio. ...

  6. c语言(cn)括号的作用,c语言小括号的用法

    c语言小括号的用法 C语言的小括号里面表示一条语句,返回值是多条语句中最靠右的语句的返回值,比如(a=2,b=3,c=a+b),返回值就是c的值.下面小编就为大家介绍c语言小括号的用法. c语言小括号 ...

  7. c语言位与运算的作用,c语言位运算符的用法

    c语言位运算符的用法 C语言中位运算的运算分量只能是整型或字符型数据,位运算把运算对象看作是由二进位组成的位串信息,按位完成指定的运算,得到位串信息的结果.下面小编就为大家介绍下c语言位运算符的用法. ...

  8. c语言文件有什么作用,C语言文件的使用方法

    C语言文件的使用方法 C语言文件的使用方法:所谓"文件"是指一组相关数据的有序集合. 这个数据集有一个名称,叫做文件名. 实际上在前面的各章中我们已经多次使用了文件,例如源程序文件 ...

  9. C语言分支结构的作用,C语言丨用switch语句实现多分支选择结构

    C语言永远不会过时 其实学编程关键是学习其思想,如果你精通了一门,再去学其他的时候也很容易上手.C不会过时的,尤其是在unix.linux操作平台上,学好C是必须的. C跟C++在很多方面也是兼容的, ...

  10. C语言结构标记的作用,C语言基础知识之词法符号

    任何高级语言都有自定义的词法符号和支持的数据类型. 词法符号是语言的基本组成单位,数据类型是数据的基本属性.这里我将先对C语言的C法符号做一个小的总结,既是对我自己C语言的一个系统的整理与提高,也希望 ...

最新文章

  1. python打印乘法表口诀-用Python打印九九乘法表正三角和倒三角。
  2. Activity生命周期的学习和验证
  3. 调用系统函数向进程发信号core
  4. Qt::WA_QuitOnClose用法
  5. 物联网技术渐趋成熟 车联网应用或成市场主驱力
  6. 模板—tarjan求割边
  7. Java学习笔记(7)——输入输出
  8. JSP的JavaBean前的知识点
  9. 26、Eternal框架v2-框架设计 前置分发器 1、代码
  10. snmp的oid查询方法
  11. HTML基础开头代码
  12. Spyder设置字体
  13. 广告的术语和简称大全
  14. Python爬虫学习笔记
  15. python网易云爬虫网络技术的意义_Python3爬虫实战之网易云音乐
  16. cadence xbl封装转AD
  17. 使用Visual Studio Code编写markdown文件以及转成PDF格式和markdown常用语法
  18. Android Vibrator(震动服务)
  19. c语言魔塔编程,魔塔V1.0 用C语言写的魔塔小游戏 - 下载 - 搜珍网
  20. AdminEAP框架-基于AdminLTE的权限管理

热门文章

  1. 《三国演义·挂机版》隐私政策
  2. 支持ps4html5,PS5支持PS4游戏吗 PS5可以玩PS4买过的游戏吗
  3. 高楼大厦素材高清图片
  4. 求全年最高温度(通过mapreduce)
  5. android蓝牙通信和Hex文件解析升级
  6. 程序员的“墨菲定理”
  7. 24-.equals的引入
  8. 八种提高电脑运行速度方法让你的电脑飞起来
  9. 信息渠道-市场敏锐-反应速度-从王建硕的文章,我想知道,是否有个市场情报团队在关注?渠道!!!
  10. amp sqlserver中 什么意思_SQL Server 缓存命中和缓存未命中amp;缓存与缓冲间的差异...