linux核心分析--核心中的資料結構之雙鏈表(一)__Ajax

來源:互聯網
上載者:User

關於核心中使用到的資料結構這一系列會有五篇文章,

分別介紹

   鏈表

   隊列

   雜湊

   映射

   紅/黑樹狀結構
一、首先介紹核心中鏈表

        核心中定義的鏈表是雙向鏈表,在上篇文章--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)指標。


 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.