Linux内核源码list.h解读

#list.h源码阅读
此文章是我阅读list.h后的一些见解,有问题且理解不到位的地方希望大家批评指正。

  • 本次我们阅读的内核版本为4.18.7
    在Linux内核中,list.h是内核为了方便使用链表而自己建立的链表头文件。
    以__开头的是指内核函数。
    ##链表
    ##宏定义
  • 除了调用其它头文件外,首先看到的是两个宏定义。
#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)

其实可以写成一句

struct list_head name ={&(name),&(name)};

这个结构体在types.中定义了双向链表。
##初始化链表

  • 定义完当然就是初始化链表了。
static inline void INIT_LIST_HEAD(struct list_head *list)
{
	WRITE_ONCE(list->next, list);
	list->prev = list;
}

将双链表的头尾指向自己(初始化)。
这里有两个问题,1、inline的作用,2、WRITE_ONCE的使用。

  • inline是关键字是把后面的函数定义为内联函数,我们可以认为它是一个static函数加上了inline属性,它大部分表现与static一样,只不过在调用这种函数的时候,gcc会在其调用处将其汇编码展开编译而不为这个函数生成独立的汇编码。我们可以把它理解为在定义和使用函数在同一文件时(无函数指针对函数文件进行间接调用,以及函数递归进行自身调用),inline相当于做了宏定义的替换,这样表面看可能没有什么优势,但如果函数的定义较为简单,但调用的十分频繁,这样调用函数的开销会变的非常大影响性能。
  • 在阅读list.h的过程中我们会碰到WRITE_ONCE()与READ_ONCE(),他们同属于ACCESS_ONCE()宏,WRITE_ONCE (a,b)其实就等价于a=b,READ_ONCE(x)即读取x的值,但是为什么不直接使用呢?由于C语言本身没有并发的概念,但实际情况中并发是不可避免的,ACCESS_ONCE()就是为解决这一问题存在的,该宏确保作为参数传递的值仅由生成的代码访问一次。通俗的讲,如果直接赋值a=b;b的值随时可能被其他进程修改,这样得到的A值是不可靠的,那么ACCESS_ONCE()做了什么呢?先取x的地址,将其转换为x类型的指针,将其强制转换为x类型的volatile(在C中,提醒编译器后面定义的变量随时会改变,需要读取变量时直接从变量地址进行读取)指针,再指向该指针。即将相关变量的类型转为volatile类型,直接对内存地址进行访问。这样成功避免了多线程并发导致变量被修改的问题,在共享变量未上锁的情况下,ACCESS_ONCE()为我们提供了确保读取有效数据的方法。
    ##调试模块
    下面看到的是CONFIG_DEBUG_LIST,这是一个条件调试模块,仅在DEBUG下才会被编译,若未在调试模式下则定义链表增加或删除的可用性。不在DEBUG模式则定义了如下两个函数:
static inline bool __list_add_valid(struct list_head *new,
				struct list_head *prev,
				struct list_head *next)
{
	return true;
}
static inline bool __list_del_entry_valid(struct list_head *entry)
{
	return true;
}

而开启DEBUG模式下的两个函数与这里的区别是无返回值,这里定义他们的类型是bool型,而bool的默认返回值为FALUSE,也就是说DEBUG模块定义了__list_add_valid,与 __list_del_entry_valid,他们的返回值为FALUSE而非DEBUG下定义了__list_add_valid与__list_del_entry_valid,他们的返回值为TRUE。我们目前只要明确这一点就好,接着往下看。
##链表插入节点
内核代码真是精妙无比,我们来看它是怎么做的。

static inline void __list_add(struct list_head *new,
			      struct list_head *prev,
			      struct list_head *next)
{
	if (!__list_add_valid(new, prev, next))
		return;

	next->prev = new;
	new->next = next;
	new->prev = prev;
	WRITE_ONCE(prev->next, new);
}

首先回去检测我们之前定义的__list_add_valid,如果返回值为TRUE,则继续下面的增加节点,否则就跳出去。在这里我觉得有两种意思,第一,开启DEBUG模式下返回值为FAULSE,那么就跳出去,不会增加新节点。第二,若在定义新节点过程中有特殊情况发生导致定义不成功,那么也会跳出去,所以我觉得一方面是程序需要开启调试模式不需要增加节点,另一方面也保证了它的安全性。
接着进行双链表的增加节点工作可翻译如下:

又看到了熟悉的WRITE_ONCE(),前面讲过这里不多提了。接下来内核做了一件什么事呢?

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封装了起来,定义了头插和尾插,同时只给了两个接口,让使用者调用这两个类似于API即可实现双链表的头插和尾插。
##链表删除节点
删除的基本操作就比较简单啦,只需要让前节点的后指针指向后面,后节点的前指针指向前面即可,需要注意的是虽然改变了前后指针指向的地址,但是我们原中间节点的指针指向地址并没有改变,所以我们还可以用此节点做位置记录。

static inline void __list_del(struct list_head * prev, struct list_head * next)
{
	next->prev = prev;
	WRITE_ONCE(prev->next, next);
}
static inline void __list_del_entry(struct list_head *entry)
{
	if (!__list_del_entry_valid(entry))
		return; 

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

static inline void list_del(struct list_head *entry)
{
	__list_del_entry(entry);
	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;
}

从上面三步我们可以看出,层层封装,把普通的删除操作加以确认是否DEBUG模式封装为__list_del_entry,最终加以封装为list_del。

	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;

这两句的意思是prev、next指针分别被设为LIST_POSITION2和LIST_POSITION1两个特殊值,LIST_POISON1和LIST_POISON2这两个变量在是poison.h中被设置的,是为了保证不在链表中的节点项不可访问(对LIST_POSITION1和LIST_POSITION2的访问都将引起页故障),同时由于访问了这两个值,它也去掉了__的标志。
##替换与移动等
首先还是基础的替换操作:

static inline void list_replace(struct list_head *old,
				struct list_head *new)
{
	new->next = old->next;
	new->next->prev = new;
	new->prev = old->prev;
	new->prev->next = new;
}

替换
接着是封装为替换的整体操作:

static inline void list_del_init(struct list_head *entry)
{
   __list_del_entry(entry);
   INIT_LIST_HEAD(entry);
}

需要注意的是这里还调用了初始化节点的函数,让替换后的节点初始化为空节点。紧随其后的list_del_init也调用了这一函数,让删除后的节点初始化为空节点,这与前面删除后节点两指针指向固定位置不同,系统可分辨出这个节点是删除过后有没有初始化是否可用。

  • 移动则是通过调用删除与增加两个函数的操作来实现,并且同时定义了移动至头或尾
  • 通过list_is_last与list_empty判断链表的下一个是不是头指针与头指针的下一个是否还是自己可判断出链表是否走到头,链表是否为空(双链表为循环链表的特性)。
  • 有一点值得注意,紧随其后的ist_emplty_careful也是用来判断链表是否为空,只是判断的要求更为严格。
static inline int ist_emplty_careful(const struct list_head *head)
{
	struct list_head *next = head->next;
	return (next == head) && (next == head->prev);
}

它不但判断链表是否为空,还同时检查了有没有当前正在定义它前后指针的进程,增加了一步保险。在Linux内核代码中我们会经常看到这种写法,list.h中还有一个,我们稍后会看到。

  • list_rotate_left则是将head的next与head进行交换。
  • list_is_singular利用一个&&判断链表中是否只有一个节点。
    ##裁剪与合并
static inline void __list_cut_position(struct list_head *list,
		struct list_head *head, struct list_head *entry)
{
	struct list_head *new_first = entry->next;
	list->next = head->next;
	list->next->prev = list;
	list->prev = entry;
	entry->next = list;
	head->next = new_first;
	new_first->prev = head;
}

__list_cut_position是把一个链表裁剪成两段新的,具体操作如下:
裁剪
之后还是一样的封装。

  • 合并这个函数实现的结果是将list领头的这个链表合并到prev和next之间,不包括list这个结点。
static inline void __list_splice(const struct list_head *list,
				 struct list_head *prev,
				 struct list_head *next)
{
	struct list_head *first = list->next;
	struct list_head *last = list->prev;

	first->prev = prev;
	prev->next = first;

	last->next = next;
	next->prev = last;
}

合并
同样通过封装,它也有从头或者从尾合并的两种调用方法。而list_splice_init与list_splice_tail_init则是将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链。

static inline void list_splice_init(struct list_head *list,
				    struct list_head *head)
{
	if (!list_empty(list)) {
		__list_splice(list, head, head->next);
		INIT_LIST_HEAD(list);
	}
}

##大量的宏定义
接着我们会看到大量的宏定义,通过观察我们可以发现有许多是相同的,只是多了一个safe,那么到底safe在哪里呢?

#define list_for_each_safe(pos, n, head) 
    for (pos = (head)->next, n = pos->next; pos != (head); 
        pos = n, n = pos->next)  
#define list_for_each_prev_safe(pos, n, head) /  
    for (pos = (head)->prev, n = pos->prev;   
         prefetch(pos->prev), pos != (head);   
         pos = n, n = pos->prev) 

发现它总是多定义一个n,相当于一个暂存容器,用来保存当前数据,同时可以在遍历的过程中对节点进行操作并且不会导致数据的丢失。

  • 其实这些宏定义的方式也与上面函数相同,设置了最基本最主要的定义,剩下的都是靠调用它实现完成的,通过阅读list.h到现在,我对函数的可移植性有了更深层次的理解。
    ##哈希表
    我理解中,哈希表是一种折中的数据结构,它既避免了顺序存储结构的增删繁琐,又优化了链表的挨个遍历的复杂。它通过散列技术,将记录存储在一块连续的存储空间中,而我们最关键的就是确定这个散列技术中存储位置和它关键字的对应关系,即哈希函数,以及解决所带来的冲突问题。
    -定义哈希表头节点与节点的代码位于types.h中。
struct hlist_head {
	struct hlist_node *first;
};

struct hlist_node {
	struct hlist_node *next, **pprev;
};

我们会立即注意到定义节点的时候它使用了二级指针,它为什么要使用二级指针来定义呢,它在里面怎么用呢?我们接着往下走

###初始化

#define HLIST_HEAD_INIT { .first = NULL }
#define HLIST_HEAD(name) struct hlist_head name = {  .first = NULL }
#define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL)

这里用的宏方法与前面双链表类似不再多提。分别是只初始化头节点,声明并初始化头节点,使用指针初始化头节点,可在运行时初始化。

static inline void INIT_HLIST_NODE(struct hlist_node *h)
{
	h->next = NULL;
	h->pprev = NULL;
}

static inlinkjlke int hlist_unhashed(const struct hlist_node *h)
{
	return !h->pprev;
}

static inline int hlist_empty(const struct hlist_head *h)
{
	return !READ_ONCE(h->first);
}

这里初始化了哈希表节点,检测节点是否被哈希以及检测哈希表是否为空。
##删除哈希节点

static inline void hlist_del(struct hlist_node *n)
{
	__hlist_del(n);
	n->next = LIST_POISON1;
	n->pprev = LIST_POISON2;
}

static inline void hlist_del_init(struct hlist_node *n)
{
	if (!hlist_unhashed(n)) {
		__hlist_del(n);
		INIT_HLIST_NODE(n);
	}
}

在这里我们看到了定义的二级指针。那么为什么不用双链表解决这个问题呢?先看看哈希表的数据结构定义。

假如我们用双链表来实现会有什么问题呢?

  • 第一Hash与双链表的数据结构不同,从图上或者定义可以很清楚的知道,hlist头结点中没有prev变量,如果强行设计为双链表,相当于给了他一个无用指针,将空间占用提高了一倍,造成巨大浪费。
  • 第二假设用原来的双链表实现,节点的头指针总指向前一个节点,但是HASH头节点中放的是first指向的地址,那么每一次的操作都需要类型转换,十分麻烦。
    理解了使用双链表的弊端,那么就能理解二级指针的使用了,使用二级指针完美的避免了以上问题,来看看它怎么操作的。
    1、获取n的下一个结点next
    2、 n->pprev指向n的前一个结点的next指针的地址,这样*pprev就代表n前一个节点的下一个结点的地址(目前指向n本身)
    3、*pprev=next,即将n的前一个节点和n的下一个结点关联起来。
    4、如果n是链表的最后一个结点,那么n->next即为空,则无需任何操作;否则,next->pprev=pprev,将n的下一个结点的pprev指向n的pprev(既修改后结点的pprev数值)
    不得不说,这里的Hash设计实在精妙。
    hlist_del和hlist_del_init都是调用__hlist_dle来删除结点n。唯一不同的是对结点n的处理,前者是将n设置为不可用,后者是将其设置为一个空的结点。
    ###添加哈希节点
    先来看前三个常规的添加:
static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)
{
	struct hlist_node *first = h->first;
	n->next = first; 
	if (first)
		first->pprev = &n->next;
	WRITE_ONCE(h->first, n);
	n->pprev = &h->first;
}
static inline void hlist_add_before(struct hlist_node *n,
					struct hlist_node *next)
{
	n->pprev = next->pprev;
	n->next = next;
	next->pprev = &n->next;
	WRITE_ONCE(*(n->pprev), n);
}

static inline void hlist_add_behind(struct hlist_node *n,
				    struct hlist_node *prev)
{
	n->next = prev->next;
	WRITE_ONCE(prev->next, n);
	n->pprev = &prev->next;

	if (n->next)
		n->next->pprev  = &n->next;
}

hlist_add_head:将结点n插在头结点h之后。

  • first = h->first。获得当前链表的首个结点.
  • 将first赋值给n结点的next域。让n的next与first关联起来。
  • 如果first不为空,则将first的pprev指向n的next域。此时完成了first结点的关联。
    如果fist为空,则不进行操作。

hlist_add_before:将结点n插在next结点的前面,所以需要next节点一定不可以为NULL

  • n->pprev = next->prev;将next的pprev赋值给n->pprev。使n的pprev 指向next的前一个结点的next。
  • n->next = next;将n结点的next指向next,完成对n结点的关联。
  • next->pprev = &n->next;此时修改next结点的pprev,使其指向n的next的地址。此时完成next结点的关联。
  • (n->pprev) =n;此时(n->pprev)即n结点前面的next,使其指向n。完成对n结点的关联。

hlist_add_after:将结点next插在n之后(n在哈希链表中)

  • next->next = n->next; 将next->next指向结点n的下一个结点。
  • n->next = next; 修改n结点的next,使n指向next。
  • next->pprev = &n->next; 将next的pprev指向n的next
  • 判断next后的结点是否为空如果,为空则不进行操作,否则将next后结点的pprev指向自己的next 处。

最奇怪的是最后这个hlist_add_fake函数,它将ppev指针指向自己的next指针,称之为虚假添加,同时注释中写到,这样的结点会被hlist_unhashed判为已hash。所以也可以对它调用hlist_del函数,对此函数并未搞懂,可能是没有碰到实际应用的地方。

static inline void hlist_add_fake(struct hlist_node *n)
{
	n->pprev = &n->next;
}

###移动Hash等
对哈希表的移动与双链表很类似,这里将本来以old为头结点的链表移动成为以new为头结点的链表。

static inline void hlist_move_list(struct hlist_head *old,
				   struct hlist_head *new)
{
	new->first = old->first;
	if (new->first)
		new->first->pprev = &new->first;
	old->first = NULL;
}

当然Hash里也有检查是否只有头节点的函数:

hlist_is_singular_node(struct hlist_node *n, struct hlist_head *h)
{
	return !n->next && n->pprev == &h->first;
}

紧接着后面的大段是哈希表的遍历宏,和前面的双链表类似,也有很多safe的操作,为内核代码提供了强壮性。
##总结
通过阅读List.h掌握到一种很重要的思想就是封装,这让内核代码看起来十分简洁,不断地调用,让简单的功能实现复杂功能的同时给与调用函数的人方便的使用,这样的代码,怪不得说内核代码是宝库,名不虚传,我学到了很多。期待之后阅读带来的惊喜。

猜你喜欢

转载自blog.csdn.net/weixin_43122409/article/details/82831268