鏈表簡介:
鏈表是一種常用的資料結構,它通過指標將一系列資料節點串連成一條資料鏈。相對於數組,鏈表具有更好的動態性,建立鏈表時無需預Crowdsourced Security Testing道資料總量,可以隨機分配空間,可以高效地在鏈表中的任意位置即時插入或者刪除資料。鏈表的開銷主要是訪問的順序性和組織鏈的空間損失。
核心鏈表的好主要體現為兩點,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_LISTstatic 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;}#elseextern 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.oKDIR := /home/kernel/linux-ok6410all:make -C $(KDIR) M=$(PWD) modules CROSS_COMPILE=arm-linux- ARCH=armclean:rm -f *.o *.ko *.order *.symvers
在終端上載入運行核心模組:
這裡rmmod 時會有個錯誤。不過沒大事。百度有很多解決方案。