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的鏈表,(寫好了再來貼上),有不對的地方望路過的大佬斧正。

 

 

 

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