些小秘密之链表(三)"/>
C语言的那些小秘密之链表(三)
在开始写linux内核双向循环链表之前,我一直在想我要不要用长篇大论的文字来描述linux内核双向循环链表呢?经过认真的思考之后,我否决了用枯燥的文字向读者描述linux内核双向循环链表的想法,因为对于编程语言来说我相信大多数的读者都应该不喜欢面对枯燥的文字,更喜欢看到代码,同时那也是读者阅读文字后想要实现的东西,所以我决定在这里采用代码加上适当的文字描述的方法来进行讲解,这就使得我不可能用一篇的篇幅来讲解完,所以会写两篇文章来讲解这个知识点。希望读者能够坚持看完,学会以后在应用程序中写双向循环链表时,不用再自己去编写那些麻烦的操作函数,充分利用linux内核里已经提供的遍历链表的操作函数。特此说明:我会把我在文章中编写代码时候用到的头文件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))
#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结构的实现。
struct list_head {struct list_head *next, *prev;
};
在linux内核双向循环链表中我们用以上list_head类型定义一个变量,将其作为一个成员嵌入到宿主结构内。什么是宿主结构体呢?就是我们创建的双向循环链表的结构体。可以将链表结构放在宿主结构内的任何地方,当然也可以为链表结构取任何名字,从而我们就可以用list_head中的成员和相对应的处理函数来对链表进行遍历操作,如果想得到宿主结构的指针,使用我们可以使用list_entry计算出来,先别急着想知道list_entry什么,我们会在下面讲解,接着往下看。
在宿主结构体中定义了list_head之后接下来当然是要对我们定义的头结点进行初始化工作,初始化的实现方法可以有以下两种方式。
#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)宏即可完成定义和初始化的工作。头结点的初始化工作完成了之后接下来的工作当然是要添加节点了。
static 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()的实现。
static 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在应用层来写出我们的代码。
#include <stdio.h>
#include <stdlib.h>
#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;
}
运行结果为:
root@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()的理解现在可以参考运行结果和上面的文字描述部分。
我们接着往下看。
static inline void __list_del(struct list_head * prev, struct list_head * next)
{next->prev = prev;prev->next = next;
}
在prev和next指针所指向的结点之间,两者互相所指。其实也就是prev为待删除的结点的前面一个结点,next为待删除的结点的后面一个结点。
static 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。
static inline void list_del_init(struct list_head *entry)
{__list_del(entry->prev, entry->next);INIT_LIST_HEAD(entry);
}
以上函数的功能为删除entry所指向的结点,同时调用LIST_INIT_HEAD()把被删除结点为作为链表头构建一个新的空双循环链表。
#include <stdio.h>
#include <stdlib.h>
#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;
}
运行结果为:
root@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
看了代码的使用之后我们再去理解之前的讲解就要轻松多了。读者可以结合上面相应的文字描述再分析下代码,以加深印象。接着往下看,坚持看完本篇博客的最后两个函数。
static 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两个指针所指向的结点之间。如果读者觉得这样说不是太具体的话,那么我们来看看下面的代码。
#include <stdio.h>
#include <stdlib.h>
#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;
}
运行结果为:
root@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语言的那些小秘密之链表(三)
发布评论