關於核心中使用到的資料結構這一系列會有五篇文章,
分別介紹
鏈表
隊列
雜湊
映射
紅/黑樹狀結構
一、首先介紹核心中鏈表
核心中定義的鏈表是雙向鏈表,在上篇文章--libevent原始碼分析--queue.h中關於TAILQ_QUEUE的理解中介紹了FreeBSD中如何定義鏈表隊列,和linux核心中的定義還是有區別的,但同樣經典。
核心中關於鏈表定義的代碼位於: include/linux/list.h。list.h檔案中對每個函數都有注釋,這裡就不詳細說了。其實剛開始只要先瞭解一個常用的鏈表操作(追加,刪除,遍曆)的實現方法,其他方法基本都是基於這些常用操作的。 介紹核心中鏈表的定義之前,回想資料結構中定義鏈表的方式,兩者是有區別的。
一般的雙向鏈表一般是如下的結構, 有個單獨的頭結點(head) 每個節點(node)除了包含必要的資料之外,還有2個指標(pre,next) pre指標指向前一個節點(node),next指標指向後一個節點(node) 頭結點(head)的pre指標指向鏈表的最後一個節點 最後一個節點的next指標指向頭結點(head)
(感謝原作者)
傳統的鏈表有個最大的缺點就是不好共通化,因為每個node中的data1,data2等等都是不確定的(無論是個數還是類型)。
linux中的鏈表巧妙的解決了這個問題,linux的鏈表不是將使用者資料儲存在鏈表節點中,而是將鏈表節點儲存在使用者資料中。
linux的鏈表節點只有2個指標(pre和next),這樣的話,鏈表的節點將獨立於使用者資料之外,便於實現鏈表的共同操作。 如下圖所示:
這個圖畫的非常的標準,好好揣摩。
在include/linxu/list.h中的定義也是非常簡單:
struct list_head { 20 struct list_head *next, *prev; 21 };在使用的時候,自己定義結構體,但是結構體中除了使用者的資料就是這個結構體。這樣便可構造自己定義的雙向鏈表。
在瞭解了基本內容看具體實現,只知道資料成員list的地址,怎樣去訪問自身以及其他成員呢。
linux鏈表中的最大問題是怎樣通過鏈表的節點來取得使用者資料。
和傳統的鏈表不同,linux的鏈表節點(node)中沒有包含使用者的使用者data1,data2等。 下面進入正題:
在include/linux/list.h標頭檔中可以看到這段代碼。
#define list_entry(ptr,type,member) / container_of(ptr,type,member)
其中container_of這個宏在/include/linux/kernel.h的標頭檔中。
#define container_of(ptr, type, member) ({ \648 const typeof( ((type *)0)->member ) *__mptr = (ptr); \649 (type *)( (char *)__mptr - offsetof(type,member) );})
//這裡面的type一般是個結構體,也就是包含使用者資料和鏈表節點的結構體。
//ptr是指向type中鏈表節點的指標
//member則是type中定義鏈表節點是用的名字
關於這個宏解釋有幾點需要解釋,
1、typeof(type),這是一個宏,這個宏返回一個type的類型,例如:int a; typeof(a) b;等價於int b;
2、offsetof(type,member)宏 它定義在include/linx/stddef.h中,如下:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
這個宏返回member在type類型中的位移量,type是一個結構,例如:
typeof(list_head,next);返回0,也就是返回相對於結構起始地址的位移量。可能會有疑問為何將0強制轉化為某一個類型的指標,然後這個指標指向這個類型中的某一個成員,指標所指成員的地址就是這個成員在這個類型中的位移量。
這種情況一般都使用在擷取結構體中某一成員的位移。因為首地址是從0開始,那麼結構成員的地址從數值上看就是他的位移量。可能還不怎麼明白,那麼指標是什麼,是一個地址,指標的內容是某個變數的首地址,將0強轉為指標類型,也就是說指標值為零,而這個值就是所指對象的首地址。(位移量+首地址=成員地址,這裡只不過將首地址變為0,那麼成員地址就是位移量。)
可以用一個簡單的例子說明:
struct student{ int id; char* name; struct list_head list;};<ul><li>type是struct student</li><li>ptr是指向stuct list的指標,也就是指向member類型的指標</li><li>member就是 list</li></ul>下面的圖以sturct student為例進行說明這個宏:
首先需要知道 ((TYPE *)0) 表示將地址0轉換為 TYPE 類型的地址
由於TYPE的地址是0,所以((TYPE *)0)->MEMBER 也就是 MEMBER的地址和TYPE地址的差,如下圖所示:
3、使用typeof(((type *)0)->member)來定義指標 __ptr,而不是這樣:const typeof(member) *__ptr=ptr;。
其實,這個很簡單,因為member是結構的成員,只能通過結構來訪問。
4、在這句話中(type *)( (char *)__mptr - offsetof(type,member) ); 減號前就是成員的地址,減號後是這個成員在結構中的位移量,兩者相減便是這個結構的首地址。
鏈表中資料的訪問:
在檔案include/linux/list.h中,有訪問鏈表資料的代碼
#define list_for_each_entry(pos, head, member) for(pos=list_entry((head)->next,typeof(*pos),member);...)
#define list_for_each_entry(pos, head, member)
for(pos=list_entry((head)->next,typeof(*pos),member);...)
從上面的使用來看,替換list_entry宏以及container_of宏後,變成如下:
pos=({const typeof(((typeof(*pos) *)0)->member) *__ptr=(head)->next;
pos=({const typeof(((typeof(*pos) *)0)->member) *__ptr=(head)->next; (typeof(*pos) *)((char *)__ptr - offsetof(typeof(typeof(*pos)),member));});
二、還有一種鏈表,作為雙向鏈表使用
struct hlist_head{ struct hlist_node *first;};struct hlist_node{ struct hlist_node *next, **pprev;};這個雙向鏈表不是真正的雙向鏈表,因為表頭只有一個first域,為什麼這樣設計。代碼中的注釋解釋:為了節約記憶體,特別適合作為Hash表的衝突鏈,但Hash表很大時,那麼表頭節約下來的記憶體就相當客觀了,雖然每個表頭只節約一個指標。
同時,表頭的不一致性也會帶來鏈表操作上的困難,顯然就是在表頭和首資料節點之間插入節點時需要特別處理,這也就是為什麼會設計二級指標pprev的原因。看看代碼
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; *(n->pprev)=n;}解釋:指標n指向新節點,指標next指向將要在它之前插入新節點的那個節點。
看上面的代碼,就可以看到二級指標pprev的威力了。有沒有看到,當next就是第一個資料節點時,這裡的插入也就是在表頭和首資料節點之間插入一個節點,但是並不需要特別處理。而是統一使用*(n->pprev)來訪問前驅的指標域(在普通節點中是next,而在表頭中是first)。看到這個和上篇文章中講解的TAILQ_QUEUE是不是很相似。其實在FreeBSD中也講解了這種資料結構。
精益求精的Linux鏈表設計者(因為list.h沒有署名,所以很可能就是Linus Torvalds)認為雙頭(next、prev)的雙鏈表對於HASH表來說"過於浪費",因而另行設計了一套用於HASH表應用的hlist資料結構--單指標表頭雙迴圈鏈表,從上圖可以看出,hlist的表頭僅有一個指向首節點的指標,而沒有指向尾節點的指標,這樣在可能是海量的HASH表中儲存的表頭就能減少一半的空間消耗。
因為表頭和節點的資料結構不同,插入操作如果發生在表頭和首節點之間,以往的方法就行不通了:表頭的first指標必須修改指向新插入的節點,卻不能使用類似list_add()這樣統一的描述。為此,hlist節點的prev不再是指向前一個節點的指標,而是指向前一個節點(可能是表頭)中的next(對於表頭則是first)指標(struct list_head **pprev),從而在表頭插入的操作可以通過一致的"*(node->pprev)"訪問和修改前驅節點的next(或first)指標。