Linux内核源码学习(第一天)——Linux内核中的链表

0x00 前言

linux中的双向链表和传统的双向链表不太一样,是注入了抽象封装灵魂的链表,

他不是把将数据结构塞入链表,而是将链表节点塞入数据

之前在windows里就见识过_ETHREAD结构.....的双向链表,但苦于没有源码,所以在linux里好好康一康。

链表的头文件是list.h(include\linux\list.h),我们重此来探究

参考文章:

https://blog.csdn.net/xu_ya_fei/article/details/49744699

https://www.cnblogs.com/wangzahngjun/p/5556448.html

https://wenku.baidu.com/view/5df5736fa0116c175e0e4817.html

https://blog.csdn.net/wanshilun/article/details/79747710

https://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html

0x01 传统双向链表

详见此文http://data.biancheng.net/view/8.html

就不在此赘述了。

0x02 基础数据结构

1. 内核链表的定义

struct list_head {
    struct list_head *next, *prev;
};

可以看出来,linux内核里并没有数据域,这正是体现抽象思想的表现,把双向链表的共性(前驱指针,后继指针)作为一个基本数据结构。

所以我们就可以如下使用

struct My_List{
    void* My_Data;
    struct list_head My_List;
};

1) list_head 是一种侵入式链表,数据附加在链表之上,使得 list_head 数据结构是通用的,使用起来就不需要考虑节点数据的类型。

2)list_head结构体可以添加到结构体的任何位置

3)可以为 list_head 命名

4)可以添加多个list_head链表

2.是否有效

#ifdef CONFIG_DEBUG_LIST
extern bool __list_add_valid(struct list_head *new,
			      struct list_head *prev,
			      struct list_head *next);
extern bool __list_del_entry_valid(struct list_head *entry);
#else
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;
}
#endif

通常在进行插入和删除时会首先判断是否有效

 

3.WRITE_ONCE && READ_ONCE

在某些情况下CPU对内存中变量读写并不是一次完成的,这可能会出现竞争。而READ_ONCE和WRITE_ONCE实现对变量一次性读取和一次性写入。

详情可以看这篇文章:https://blog.csdn.net/cloudblaze/article/details/51676139

 

4. offsetof  &&  container_of

在下文 "遍历" 时详细说明。

 

0x03 基础函数

1. 声明与初始化

上一段描述了链表的链节点,在LIST_HEAD这个宏中描述了头节点

在源码中可以看到俩个宏:

​#define LIST_HEAD_INIT(name) { &(name), &(name) }

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

第一个:&(name)  前驱指针,后继指针都是指向自己的指针

第二个:调用了第一个的初始化,使用宏 LIST_HEAD_INIT 进行初始化,这会使用变量name 的地址来填充prev和next 结构体的两个变量

两者的使用的不同:第一个宏是初始化,第二个宏是声明+初始化

除了宏以外还有Linux还提供了一个INIT_LIST_HEAD宏用于运行时初始化链表

static inline void
INIT_LIST_HEAD(struct list_head *list)
{
    list->next = list->prev = list;
}

2. 插入

在linux内核中一共有俩个插入的函数

static inline void list_add()
static inline void list_add_tail()

list_add在链表头插入,实现栈的功能

list_add_tail在链表尾插入,实现队列的功能

在传统的双向链表中的插入:

前插

int DlinkIns(DoubleList L,int i,ElemType e)
{
 DNode *s,*p;
 …/*先检查待插入的位置i是否合法(实现方法同单链表的前插操作)*/
 …/*若位置i合法,则让指针p指向它*/
 s=(DNode *)malloc(sizeof(DNode));
 if(s)
   {
    s->data=e;
    s->prior=p->prior;p->prior->next=s;
    s->next=p;p->prior=s;
    return TRUE;
   }
 else
    teturn fALSE;
}

后插也是一堆,这里略

咱们来看linux内核如何实现:

先设置了一个基础: __list_add

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:

static inline void list_add(struct list_head *new, struct list_head *head)
{
	__list_add(new, head, head->next);
}

list_add_tail: 

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,并确定位置,插在head和head->next之间,实现栈的操作;

list_add_tail调用__list_add,并确定位置,插在head->prev和head之间,实现队列的操作。

那如何找得将要插入的位置呢?在下文遍历中会详细说明。

3. 删除

同样贯彻封装的思想,先封装删除操作:

static inline void __list_del(struct list_head * prev, struct list_head * next)
{
	next->prev = prev;
	WRITE_ONCE(prev->next, next);
}

其中的WRITE_ONCE:把 next写入prev->next ,在某些情况下CPU对内存中变量读写并不是一次完成的,这可能会出现竞争。WRITE_ONCE实现对变量一次性写入。

list_del:

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;
}

注意:其中在使用list_del时,删除操作会把所有要删除的节点(point)的 next 和 prev都指向一个固定的值(LIST_POSITION1和LIST_POSITION2),因为LIST_POSITION1和LIST_POSITION2的宏定义了一个不可访问的地址,所以当使用 

point = point->next时会因不可访问而发生页错误。

那为什么不把next 和 prev直接free掉呢?

LIST_POSITION1和LIST_POSITION2指向的是内核中的位置,在用户态给0,这样list.h可以直接移植到用户态来用,笔者这里觉得没有直接释放掉,和便于移植有关。

另外,删除还有另一种情况:删除列表项并清除'prev'指针

static inline void __list_del_clearprev(struct list_head *entry)
{
	__list_del(entry->prev, entry->next);
	entry->prev = NULL;
}

。这是网络代码中使用的一种特殊用途的列表清除方法,用于按cpu分配的列表,我们不希望产生常规list_del_init()的额外*WRITE ONCE()开销。使用这个*的代码需要检查节点'prev'指针,而不是调用list_empty()。

安全删除

static inline void list_del_init(struct list_head *entry)
{
	__list_del_entry(entry);
	INIT_LIST_HEAD(entry);
}
static inline void __list_del_entry(struct list_head *entry)
{
	if (!__list_del_entry_valid(entry))
		return;

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

相对于list_del的简单调用,list_del_init,不止调用__list_del,之后还会将它初始化(将entry节点的前继节点和后继节点都指向entry本身)。

4.空 && 第一 && 最后 && 是否单链

这个不多说,看源码,有两种

static inline int list_empty(const struct list_head *head)
{
	return READ_ONCE(head->next) == head;
}

上面这种直接检测 后继 是否指向自己 下面这种检查的更多

static inline int list_empty_careful(const struct list_head *head)
{
	struct list_head *next = head->next;
	return (next == head) && (next == head->prev);
}

不止判断 后继 是否是自己,还判断 前驱 本身和 后继 一不一样。

linux还设置了判断是否是第一个节点的函数:

/**
 * list_is_first -- tests whether @list is the first entry in list @head
 * @list: the entry to test
 * @head: the head of the list
 */
static inline int list_is_first(const struct list_head *list,
					const struct list_head *head)
{
	return list->prev == head;
}

对应的,判断是否是最后一个节点:

static inline int list_is_last(const struct list_head *list,
				const struct list_head *head)
{
	return list->next == head;
}

判断是否是单个链:

static inline int list_is_singular(const struct list_head *head)
{
	return !list_empty(head) && (head->next == head->prev);
}

 

5. 移动

把链表A的某节点插入链表B(前插,后插),说白了就是利用了删除与插入:

前插:

static inline void list_move(struct list_head *list, struct list_head *head)
{
	__list_del_entry(list);
	list_add(list, head);
}

后插:

static inline void list_move_tail(struct list_head *list,
				  struct list_head *head)
{
	__list_del_entry(list);
	list_add_tail(list, head);
}

linux也提供了 在一个同一个链表中的移动:把链表的前一部分,放到链表的后部:

static inline void list_bulk_move_tail(struct list_head *head,
				       struct list_head *first,
				       struct list_head *last)
{
	first->prev->next = last->next;
	last->next->prev = first->prev;

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

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

1)head 参数 指向链表头,

2)first 指向需要移动的多个连续节点的第一个节点,

3)last 指向需要移动的多个 连续节点的最后一个节点。

(环的一部分)

6.替换

list_replace:只把old替换成new 

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;
}

list_replace_init:把old替换成new,并初始化old(要被替换的元素)

static inline void list_replace_init(struct list_head *old,
				     struct list_head *new)
{
	list_replace(old, new);
	INIT_LIST_HEAD(old);
}

7.交换(两个链表间)

用entry2替换entry1,并在entry2的位置重新添加entry1:

list_swap:

static inline void list_swap(struct list_head *entry1,
			     struct list_head *entry2)
{
	struct list_head *pos = entry2->prev;

	list_del(entry2);
	list_replace(entry1, entry2);
	if (pos == entry1)
		pos = entry2;
	list_add(entry1, pos);
}

8.反转 (同一个链表内)

不同于上述的交换,反转是同一个链表内,前后节点的交换:

list_rotate_left:(前后相连)

static inline void list_rotate_left(struct list_head *head)
{
	struct list_head *first;

	if (!list_empty(head)) {
		first = head->next;
		list_move_tail(first, head);
	}
}

list_rotate_to_front:(将链节点提到最前面)

static inline void list_rotate_to_front(struct list_head *list,
					struct list_head *head)
{
	/*
	 * Deletes the list head from the list denoted by @head and
	 * places it as the tail of @list, this effectively rotates the
	 * list so that @list is at the front.
	 */
	list_move_tail(head, list);
}

9. 剪切

贯彻抽象思想,暂时不管位置,先实现剪切操作

 __list_cut_position:(在entry处剪切,并把entry也剪掉)

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,通过head将第一个节点到entry节点(包括它本身),转到新链表list里,再将head 链表重新指向 entry 的下一个节点。

 

list_cut position:

static inline void list_cut_position(struct list_head *list,
		struct list_head *head, struct list_head *entry)
{
	if (list_empty(head))
		return;
	if (list_is_singular(head) &&
		(head->next != entry && head != entry))
		return;
	if (entry == head)
		INIT_LIST_HEAD(list);
	else
		__list_cut_position(list, head, entry);
}

list_cut_position() 函数用于将一个链表切成两个链表。参数 list 指向一个新的链表, 参数 head 指向被拆开的链表 (原始链表),entry 参数指向拆开的位置。函数首先调用 list_empty() 函数确定 head 链表是不是空链表,如果是则直接返回;如果不是空链表, 则调用 list_is_singular() 函数确定 head 链表是不是单一节点的链表,如果是则判断 head 链表的 next 不指向 entry 参数,并且 head 不是 entry,那么直接返回;反之 不是,则判断 entry 与 head 的关系,如果 entry 就是 head,那么代表新链表是空链表, 那么直接调用 INIT_LIST_HEAD() 函数初始化 list 链表;反之调用 __list_cut_position() 将 head 链表拆成两段,第一个节点到 entry 节点的链表使用 list 指定,entry 到最后一个 节点通过 head 指向。

 

list_cut_before: (在entry处剪切,不把entry剪掉)

static inline void list_cut_before(struct list_head *list,
				   struct list_head *head,
				   struct list_head *entry)
{
	if (head->next == entry) {
		INIT_LIST_HEAD(list);
		return;
	}
	list->next = head->next;
	list->next->prev = list;
	list->prev = entry->prev;
	list->prev->next = list;
	head->next = entry;
	entry->prev = head;
}

list_cut_before() 函数用于将 head 拆分成两个链表,新链表从 head 的第一个 节点到 entry 的前一个节点,拆分之后的 head 链表的第一个节点变成了 entry 节点。 函数首先检查 head->next 与 entry 之间的关系,如果相等,代表 list 链表是一个 空链表,因此调用 INIT_LIST_HEAD() 初始化 list 链表;如果不相等,就将 entry 之前的链表拆分给 list 链表,head 链表则维护从 entry 之后的链表。

10. 合并

说白了就是把整个链表插入:

基本插入操作(封装的思想)

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:前插

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

list_splice_tail:后插

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

图解:(一位大佬的图)

 

 

当list1被挂接到list2之后,作为原表头指针的list1的next、prev仍然指向原来的节点,为了避免引起混乱,Linux提供了一个list_splice_init()函数

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);
	}
}

该函数在将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链

对应的还有 list_splice_tail_init ,除了变成了后插,没有别的区别。

11. 遍历

对于链表,遍历是很重要的。在上文中,在添加操作时,如何找到将要插入的位置?在这里得到解答,linux中简单的宏实现遍历:

以下是向后遍历


#define list_for_each(pos, head) \

	for (pos = (head)->next; pos != (head); pos = pos->next)

当然也可以反向遍历


#define list_for_each_prev(pos, head) \

	for (pos = (head)->prev; pos != (head); pos = pos->prev)

pos: 指向list_head的指针。

head :需要遍历的链表的链表头。

那我们可以找到list_head的位置,但怎么找到对应的数据呢?我们需要另一个宏list_entry

list_entry:

#define list_entry(ptr, type, member) \
	container_of(ptr, type, member)

其中

ptr是指向该数据中list_head成员的指针,也就是存储在链表中的地址值,

type是数据项的类型,

member则是数据项类型定义中list_head成员的变量名


#define container_of(ptr, type, member) ({	    \

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

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

其实它的语法很简单,只是一些指针的灵活应用,它分两步:
    第一步,首先定义一个临时的数据类型(通过typeof( ((type *)0)->member )获得)与ptr相同的指针变量__mptr,然后用它来保存ptr的值。
    第二步,用(char *)__mptr减去member在结构体中的偏移量,得到的值就是整个结构体变量的首地址(整个宏的返回值就是这个首地址)

图解:

ok,那问题来了,这个偏移咋算的,我们跟进offseto().

include\linux\stddef.h

那么我们可以假设在0地址分配了一个结构体变量struct TYPE a,然后定义结构体指针变量p并指向a(struct TYPE *p = &a),如此我们就可以通过  &(p->MEMBER)  获得成员MEMBER的地址。由于a的首地址为0x0,所以获取的MEMBER的地址就是在结构体中的偏移,最后在强制转换成(size_t)类型。这样就拿到了list_head真正的偏移。

这样我们一路逆向回去,就可以找到节表的位置(data域开始的位置)。

我们拿代码验证一下:

#include<stdio.h>

#define offsetof(TYPE, MEMBER)	((size_t)&((TYPE *)0)->MEMBER)

struct My_Struct {
	int data;
	char My_list1;
	char My_list2;
};

int main()
{
	printf("My_list1相对My_Struct的位移:%d",offsetof(struct My_Struct, My_list1));

	printf("My_list2相对My_Struct的位移:%d", offsetof(struct My_Struct, My_list2));

	return 0;
}

运行结果:

 4和5 没问题。

 

 

0x04 总结

第一次看内核的源码,感觉好爽,等有时间自己实现一下linux的链表,(写好了再来贴上),有不对的地方望路过的大佬斧正。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章