Linux 核心資料結構:雙向連結串列

Alick發表於2015-06-23

Linux 核心提供一套雙向連結串列的實現,你可以在 include/linux/list.h 中找到。我們以雙向連結串列著手開始介紹 Linux 核心中的資料結構 ,因為這個是在 Linux 核心中使用最為廣泛的資料結構,具體你可以 檢視 這裡。

首先讓我們看一下主要的結構體:

你可以看到其與常見的結構體實現有顯著不同,比如 glib 中所使用到的雙向連結串列實現。

通常來說,連結串列結構體要包括一個指向資料的指標,不過 Linux 核心的連結串列卻不包含此實現。那麼首要的疑問:連結串列是用什麼方式儲存資料的?。Linux 核心所實現的是一種被稱為侵入式的連結串列(Intrusive list),這種連結串列並不在連結串列結構中包含資料,而僅提供用於維護前向與後向訪問結構的指標。這種實現方式使得連結串列資料結構非常通用,因為它並不需要關注連結串列所維護的具體資料型別。

比如:

接下來讓我們看一些核心使用 list_head 的具體例子。正如在前文所述的,Linux 核心中諸多模組都使用了 list_head。這裡我們以核心雜項字元裝置驅動(miscellaneous character drivers)部分實現為例。驅動的 API 在 drivers/char/misc.c 中,其實現了簡單硬體外設以及虛擬裝置的驅動,這個驅動共享主裝置號(Major number):

每個裝置有自己的次裝置號,具體可以看這個列子:

現在我們看看裝置驅動是如何使用連結串列維護裝置列表的,首先,我們看一下 miscdevice 的 struct 定義:

可以看到 miscdevice 的第四個成員 list ,這個就是用於維護已註冊裝置連結串列的結構。在原始碼文的首部,我們可以看到以下定義:

這個定義巨集展開,可以看到是用於定義 list_head 型別變數:

LIST_HEAD_INIT 這個巨集用於對定義的變數進行雙向指標的初始化:

現在我看可以看一下函式 misc_register 是如何進行裝置註冊的。首先是用 INIT_LIST_HEADmiscdevice->list 成員變數進行初始化:

這個操作與 LIST_HEAD_INIT 巨集一致:

接下來,在通過函式 device_create 進行裝置建立,同時將裝置新增到 Misc 裝置列表中:

核心的 list.h 檔案提供向連結串列新增節點的 API,這裡是新增操作的實現:

函式實現很簡單,就是入參轉換為三個引數後呼叫內部 __list_add

  • new – 新節點;
  • head – 新節點插入的雙向連結串列頭;
  • head->next – 連結串列頭的下一個節點;

_list_add 函式的實現更加簡單:

這裡設定了新新增結點的 prevnext 指標,通過這些操作,就將先前使用 LIST_HEAD_INIT 所定義的 misc 連結串列的雙向指標與 miscdevice->list 結構關聯起來。

這裡還有一個問題,就是如何獲取連結串列中的資料,list_head 提供了一個特殊的巨集用於獲取資料指標。

這裡有三個引數

  • ptr:list_head 結構指標
  • type:資料對應的 struct 型別
  • member:資料中 list_head 成員對應的成員變數名

舉例如下:

接下來我們就夠訪問 miscdevice 的各個成員,如 p->minorp->name 等等,我們看一下 list_entry 的實現:

其實現非常簡單,就是使用入參呼叫 container_of 巨集,巨集的實現如下:

注意,巨集使用了大括號表示式,對於大括號表示式,編譯器會展開所有表示式,同時使用最後一個表示式的結果進行返回。

舉個例子:

輸出結果為 2

另一個關鍵是 typeof 關鍵字,這個非常簡單,這個正如它的名字一樣,這個關鍵字返回的結果是變數的型別。當我第一次看到這個巨集時,最讓我覺得奇怪的是表示式 ((type*)0) 中的 0 值,實際上,使用 0 值作為地址這個是成員變數取得 struct 內相對偏移地址的巧妙實現,我們再來看個例子:

輸出結果為 0x5

還有一個專門用於獲取結構體中某個成員變數偏移的巨集,其實現與前面提到的巨集非常類似:

這裡對 container_of 巨集做個綜述,container_of 巨集通過 struct 中的 list_head 成員返回 struct 對應資料的記憶體地址。在巨集的第一行定義了指向 list_head 成員的指標 __mptr ,並將 ptr 地址賦給 __mptr 。從技術實現的角度來看,實際並不需要這一行定義,但這個對於型別檢查而言非常有意義。這一行程式碼確保結構體( type )中存在 member 對應的成員。第二行使用 offsetoff 巨集計算出包含 member 的結構體所對應的記憶體地址,就是這麼簡單。

當然 list_addlist_entry 並非是 <linux/list.h> 中的全部函式,對於雙向連結串列 list_head ,核心還提供了以下的介面:

  • list_add
  • list_add_tail
  • list_del
  • list_replace
  • list_move
  • list_is_last
  • list_empty
  • list_cut_position
  • list_splice

未了,需要說的是,核心程式碼中並不僅僅只有上述這些介面。

相關文章