一、背景
初次接触linux内核,从最简单的数据结构,线性表结构开始学习。使用cd命令进入/usr/src/kernel/3.10.0-229.el7.x86_64/include/linux中,找到其中的list.h文件并分析它。
二、分析过程
1、初始化链表头
我们可以看到这里有两个宏定义。第一个是带有变量的宏定义。第二个宏定义使用了第一个宏定义LIST_HEAD_INIT(name)。
这样,当要申明并初始化自己的链表头如head时,就可以直接调用宏,LIST_HEAD(head)。
如此就有初始化struct list_head head = {&(head),&(head)}。
那么结构体类型struct list_head是在哪里定义的呢?
往回翻我们可以看到头文件
这里一看就会发现头文件types.h很像是定义struct list_head的文件。
进入这个文件里会找到这样一行:
看到这个定义我们也就知道了,head的前趋与后继指针都指向了head自己(prev and next 与head同为struct list_head结构)。
现在考虑函数,关键字static表明该函数是一个内部函数,限制了函数的作用域,使函数只能在文件内部被调用。
关键字inline告诉编译程序这是一个内联函数,当然此修饰符可以不加,但为了减少内存的占用,还是加入为好。
该函数的作用与宏定义的函数是相同的。均是初始化链表头。
2、添加新结点
后面两个函数通过调用第一个函数实现new结点插入head结点之前或之后。
首先来看第一个调用函数的实现过程:将指向结构体类型struct list_head的指针new与head传入函数中。
现在我假设head结点后跟着的是data结点。
那么
1.next->prev = new
这一步会将head的下一个结点data的prev指针的地址更改为new的地址。
这样data的上一个结点变为了new。
2.new->next = next
这一步会将new的next指针的地址设置成head的本来的下一个结点的地址,即data的地址。
这样new的下一个结点变为了data。
3.new->prev = prev
这一步会将new的prev指针设置成head的地址。
4.prev->next = new
这一步会将head的next指针的地址更改为new的地址。
好了,通过这四步,成功的将new结点插入到了head结点之后的位置。
接下来看第二个调用函数的实现过程:
现在我假设head结点前跟着的是data结点。
那么
1.next->prev = new
这一步会将head结点的prev更改为new的地址。
这样head的前趋结点变为了new。
2.new->next = next
这一步会将new的next指针的地址设置为head的地址。
这样new的后继结点变为了head。
3.new->prev = prev
这一步会将new的prev指针地址设置为head的原来的前趋data的地址。
这样new的前趋结点就是data。
4.prev->next = new
这一步会将data的next指针更改为new的地址。
这样new就成了data的后继结点。
通过这四步,成功的将new结点插入到了head结点之前的位置。
3.删除结点
类似上面添加结点,这里删除结点也是有两个函数来调用另一个函数来实现。
由于两个调用函数功能类似,其中第二个调用函数比第一个调用函数功能上更加完善,
所以这里只就第二个调用函数进行分析。
假设有三个结点data_1,entry,data_2,entry为data_1的后继结点,data_2为entry的后继结点。
将entry的地址传入函数后,entry->prev表示的是data_1的地址,entry->next表示的是data_2的地址。
这两个实参在传入函数__list_del后,就有
data_2->prev = &data_1
data_1->next = &data_2
这就显而易见了,data_1的next指针不再指向entry,而是指向data_2,
并且data_2的prev指针也不再指向entry,而是指向data_1。
接下来的两个赋值语句是将entry的next指针指向变为LIST_POISON1;
将entry的prev指针指向变为LIST_POISON2。这两个均为宏定义,在头文件poison中被定义。
4.结点的替换
以上这两个函数都可以对old结点与new结点进行替换。
但是第二个函数功能更完善,它除了替换外还将old的prev指针与next指针改为了old结点自己。
现进行分析:
假设有结点data_1,old,data_2,且old结点是data_1的后继结点,data_2是old的后继结点。
那么将old替换为new结点的方法很显然是将new的prev指针改为data_1的地址。next指针改为data_2的地址。
并且将data_1的next指针改为new的地址,将data_2的prev指针改为new的地址。
第一个被调用的函数即为如此算法。这里不再赘述。
三、总结
这里先分析了几种线性链表的操作,往下还有许多线性链表的操作值得去分析,但分析方法大致相同。
并且有许多函数是调用其它函数进行操作。
linux内核中源代码的营养价值很高,一是代码的写作思路十分清晰,值得我们学习与借鉴。二是代码的写作可读性非常高,
每个变量与函数的定义都是经过数次推敲写出的,写作习惯值得我们学习与借鉴。三是从linux内核的源代码中我们可以对
许多的基础知识进行检验与实践。