1. 程式人生 > >Linux核心連結串列深度分析

Linux核心連結串列深度分析

連結串列簡介:

連結串列是一種常用的資料結構,它通過指標將一系列資料節點連線成一條資料鏈。相對於陣列,連結串列具有更好的動態性,建立連結串列時無需預先知道資料總量,可以隨機分配空間,可以高效地在連結串列中的任意位置實時插入或者刪除資料。連結串列的開銷主要是訪問的順序性和組織鏈的空間損失。

核心連結串列的好主要體現為兩點,1是可擴充套件性,2是封裝。可擴充套件性肯定是必須的,核心一直都是在發展中的,所以程式碼都不能寫成死程式碼,要方便修改和追加。將連結串列常見的操作都進行封裝,使用者只關注介面,不需關注實現。分析核心中的連結串列我們 可以做些什麼呢?我覺得可以將其複用到使用者態程式設計中,以後在使用者態下程式設計
就不需要寫一些關於連結串列的程式碼了,直接將核心中list.h中的程式碼拷貝過來用。也可以整理出my_list.h,在以後的使用者態程式設計中直接將其包含到C檔案中。

1. 連結串列對比

傳統連結串列和核心連結串列

傳統連結串列:一般指的是單向連結串列

struct List

{

struct list *next;//連結串列結點指標域

};

核心連結串列:雙向迴圈連結串列 設計初衷是設計出一個通用統一的雙向連結串列!

struct list_head

{

 struct list_head    *head, *prev;

};

list_head結構包含兩個指向list_head結構體的指標

prev和next,由此可見,核心的連結串列具備雙鏈表功能,實際上,通常它都組織成雙向迴圈連結串列

2. 核心連結串列使用

1. INIT_LIST_HEAD:建立連結串列

2. list_add:在連結串列頭插入節點

3. list_add_tail:在連結串列尾插入節點

4. list_del:刪除節點

5. list_entry:取出節點

6. list_for_each:遍歷連結串列

(如果我們不知道這些函式的引數以及函式內部實現,學習查閱這些函式的引數或者實現程式碼最好的方法還是直接檢視核心原始碼,結和前面的用sourceInsight工具直接搜尋這些函式的名字)


下面舉個例子:比如查閱INIT_LIST_HEAD函式,


這個是先將核心原始碼匯入sourceInsight工程裡面!原始碼可以在官網上下載,然後在Linux下解壓(檔名Linux分大小寫,windows不分大小寫),然後通過Samba和對映網路驅動器功能(前面的sourceInsight博文有講到),點選R圖示左邊的那個圖示(像一個開啟的一本書)


這樣可以很快的檢視到程式碼實現部分:在核心Mkregtale.c檔案中

/*
 * This is a simple doubly linked list implementation that matches the
 * way the Linux kernel doubly linked list implementation works.
 */

struct list_head {
	struct list_head *next; /* next in chain */
	struct list_head *prev; /* previous in chain */
};

這個不含資料域的連結串列,可以嵌入到任何資料結構中,例如可按如下方式定義含有資料域的連結串列:

struct score
{
	int num;
	int English;
	int math;
	struct list_head list;//連結串列連結域
};

struct list_head score_head;//所建立連結串列的連結串列頭
INIT_LIST_HEAD(&score_head);//初始化連結串列頭 完成一個雙向迴圈連結串列的建立
上面的紅色部分初始化一個已經存在的list_head物件,score_head為一個結構體的指標,這樣可以初始化堆疊以及全域性區定義的score_head物件。呼叫INIT_LIST_HEAD()巨集初始化連結串列節點,將next和prev指標都指向其自身,我們就構造了一個空的雙迴圈連結串列。

初始化一個空連結串列:其實就是連結串列頭,用來指向第一個結點!定義結點並且初始化!然後雙向迴圈連結串列就誕生了

static 加在函式前,表示這個函式是靜態函式,其實際上是對作用域的限制,指該函式作用域僅侷限於本檔案。所以說,static 具有資訊隱蔽的作用。而函式前加 inline 關鍵字的函式,叫行內函數,表 示編譯程式在呼叫這個函式時,立即將該函式展開。
/* Initialise a list head to an empty list */
static inline void INIT_LIST_HEAD(struct list_head *list)
{
        list->next = list;
	list->prev = list;
}

list_add:在連結串列頭插入節點

/**
 * list_add - add a new entry
 * @new: new entry to be added
 * @head: list head to add it after
 *
 * Insert a new entry after the specified head.
 * This is good for implementing stacks.
 */
static inline void list_add(struct list_head *new, struct list_head *head)
{
  __list_add(new, head, head->next);
}

/*
 * Insert a new entry between two known consecutive entries.
 *
 * This is only for internal list manipulation where we know
 * the prev/next entries already!
 */
#ifndef CONFIG_DEBUG_LIST
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;
}
#else
extern void __list_add(struct list_head *new,
			      struct list_head *prev,
			      struct list_head *next);
#endif


 list_add_tail:在連結串列尾插入節點

/**
 * list_add_tail - add a new entry
 * @new: new entry to be added
 * @head: list head to add it before
 *
 * Insert a new entry before the specified head.
 * This is useful for implementing queues.
 */
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
    __list_add(new, head->prev, head);
}


用法示例:

struct score
{
int num;
int English;
int math;
struct list_head list;//連結串列連結域
};

struct list_head score_head;//所建立連結串列的連結串列頭
//定義三個節點 然後插入到連結串列中
struct score stu1, stu2, stu3;

list_add_tail(&(stu1.list), &score_head);//使用尾插法

Linux 的每個雙迴圈連結串列都有一個連結串列頭,連結串列頭也是一個節點,只不過它不嵌入到宿主資料結構中,即不能利用連結串列頭定位到對應的宿主結構,但可以由之獲得虛擬的宿主結構指標

 list_del:刪除節點

/* Take an element out of its current list, with or without
 * reinitialising the links.of the entry*/
static inline void list_del(struct list_head *entry)
{
	struct list_head *list_next = entry->next;
	struct list_head *list_prev = entry->prev;

	list_next->prev = list_prev;
	list_prev->next = list_next;

}



list_entry:取出節點

/**
 * list_entry - get the struct for this entry
 * @ptr:the &struct list_head pointer.
 * @type:the type of the struct this is embedded in.
 * @member:the name of the list_struct within the struct.
 */
#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)
/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:    the pointer to the member.
 * @type:   the type of the container struct this is embedded in.
 * @member: the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({          \
	const typeof(((type *)0)->member)*__mptr = (ptr);    \
		     (type *)((char *)__mptr - offsetof(type, member)); })



list_for_each:遍歷連結串列

#define list_for_each(pos, head) \
    for (pos = (head)->next; prefetch(pos->next), pos != (head); \
    pos = pos->next)</span></span>
可以看出,使用了輔助指標pos,pos是從第一節點開始的,並沒有訪問頭節點,直到pos到達頭節點指標head的時候結束。 而且 這種遍歷僅僅是找到一個個結點的當前位置,那如何通過pos獲得起始結點的地址,從而可以引用結點的域? list.h 中定義了 list_entry 巨集:            #define   list_entry( ptr, type, member )  \               ( (type *) ( (char *) (ptr)  - (unsigned long) ( &( (type *)0 )  ->  member ) ) )           分析:(unsigned long) ( &( (type *)0 )  ->  member ) 把 0 地址轉化為 type 結構的指標,然後獲取該           結構中 member 域的指標,也就是獲得了 member 在type 結構中的偏移量。其中  (char *) (ptr) 求          出的是 ptr 的絕對地址,二者相減,於是得到 type 型別結構體的起始地址,即起始結點的地址。使用方法非常的巧妙! 比如下列用法:

struct score stu1, stu2, stu3;
struct list_head *pos;//定義一個結點指標
struct score *tmp;//定義一個score結構體變數

//遍歷整個連結串列,每次遍歷將資料打印出來
	list_for_each(pos, &score_head)//這裡的pos會自動被賦新值
	{
		tmp = list_entry(pos, struct score, list);
		printk(KERN_WARNING"num: %d, English: %d, math: %d\n", tmp->num, tmp->English, tmp->math);
	}


list_for_each_safe: 連結串列的釋放
/**
 * list_for_each_safe - iterate over a list safe against removal of list entry
 * @pos:the &struct list_head to use as a loop cursor.
 * @n:another &struct list_head to use as temporary storage
 * @head:</span>the head for your list.
 */
#define list_for_each_safe(pos, n, head) \
    for (pos = (head)->next, n = pos->next; pos != (head); \
        pos = n, n = pos->next)


3. 核心連結串列實現分析


4. 移植核心連結串列(這裡先貼出一個使用核心連結串列的核心模組小例程)

mylist.c檔案

#include<linux/module.h>
#include<linux/init.h>
#include<linux/list.h>//包含核心連結串列標頭檔案

struct score
{
	int num;
	int English;
	int math;
	struct list_head list;//連結串列連結域
};

struct list_head score_head;//所建立連結串列的連結串列頭

//定義三個節點 然後插入到連結串列中
struct score stu1, stu2, stu3;
struct list_head *pos;//定義一個結點指標
struct score *tmp;//定義一個score結構體變數

int mylist_init()
{
	INIT_LIST_HEAD(&score_head);//初始化連結串列頭 完成一個雙向迴圈連結串列的建立
	
	stu1.num = 1;
	stu1.English = 59;
	stu1.math = 99;
	
	//然後將三個節點插入到連結串列中
	list_add_tail(&(stu1.list), &score_head);//使用尾插法
	
	stu2.num = 2;
	stu2.English = 69;
	stu2.math = 98;
	list_add_tail(&(stu2.list), &score_head);
	
	stu3.num = 3;
	stu3.English = 89;
	stu3.math = 97;
	list_add_tail(&(stu3.list), &score_head);
	
	//遍歷整個連結串列,每次遍歷將資料打印出來
	list_for_each(pos, &score_head)//這裡的pos會自動被賦新值
	{
		tmp = list_entry(pos, struct score, list);
		printk(KERN_WARNING"num: %d, English: %d, math: %d\n", tmp->num, tmp->English, tmp->math);
	}
	
	return 0;
}

void mylist_exit()
{
	//退出時刪除結點
	list_del(&(stu1.list));
	list_del(&(stu2.list));
	printk(KERN_WARNING"mylist exit!\n");
}

module_init(mylist_init);
module_exit(mylist_exit);

Makefile檔案
obj-m := mylist.o

KDIR := /home/kernel/linux-ok6410

all:
	make -C $(KDIR) M=$(PWD) modules CROSS_COMPILE=arm-linux- ARCH=arm
	
clean:
	rm -f *.o *.ko *.order *.symvers



在終端上載入執行核心模組:


這裡rmmod 時會有個錯誤!不過沒大事!百度有很多解決方案!