Linux 核心提供一套雙向連結串列的實現,你可以在 include/linux/list.h 中找到。我們以雙向連結串列著手開始介紹 Linux
核心中的資料結構 ,因為這個是在 Linux 核心中使用最為廣泛的資料結構,具體你可以 檢視 這裡。
首先讓我們看一下主要的結構體:
1 2 3 |
struct list_head { struct list_head *next, *prev; }; |
你可以看到其與常見的結構體實現有顯著不同,比如 glib 中所使用到的雙向連結串列實現。
1 2 3 4 5 |
struct GList { gpointer data; GList *next; GList *prev; }; |
通常來說,連結串列結構體要包括一個指向資料的指標,不過 Linux 核心的連結串列卻不包含此實現。那麼首要的疑問:連結串列是用什麼方式儲存資料的?
。Linux 核心所實現的是一種被稱為侵入式的連結串列(Intrusive list),這種連結串列並不在連結串列結構中包含資料,而僅提供用於維護前向與後向訪問結構的指標。這種實現方式使得連結串列資料結構非常通用,因為它並不需要關注連結串列所維護的具體資料型別。
比如:
1 2 3 4 |
struct nmi_desc { spinlock_t lock; struct list_head head; }; |
接下來讓我們看一些核心使用 list_head
的具體例子。正如在前文所述的,Linux 核心中諸多模組都使用了 list_head
。這裡我們以核心雜項字元裝置驅動(miscellaneous character drivers)部分實現為例。驅動的 API 在 drivers/char/misc.c 中,其實現了簡單硬體外設以及虛擬裝置的驅動,這個驅動共享主裝置號(Major number):
1 |
#define MISC_MAJOR 10 |
每個裝置有自己的次裝置號,具體可以看這個列子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
ls -l /dev | grep 10 crw------- 1 root root 10, 235 Mar 21 12:01 autofs drwxr-xr-x 10 root root 200 Mar 21 12:01 cpu crw------- 1 root root 10, 62 Mar 21 12:01 cpu_dma_latency crw------- 1 root root 10, 203 Mar 21 12:01 cuse drwxr-xr-x 2 root root 100 Mar 21 12:01 dri crw-rw-rw- 1 root root 10, 229 Mar 21 12:01 fuse crw------- 1 root root 10, 228 Mar 21 12:01 hpet crw------- 1 root root 10, 183 Mar 21 12:01 hwrng crw-rw----+ 1 root kvm 10, 232 Mar 21 12:01 kvm crw-rw---- 1 root disk 10, 237 Mar 21 12:01 loop-control crw------- 1 root root 10, 227 Mar 21 12:01 mcelog crw------- 1 root root 10, 59 Mar 21 12:01 memory_bandwidth crw------- 1 root root 10, 61 Mar 21 12:01 network_latency crw------- 1 root root 10, 60 Mar 21 12:01 network_throughput crw-r----- 1 root kmem 10, 144 Mar 21 12:01 nvram brw-rw---- 1 root disk 1, 10 Mar 21 12:01 ram10 crw--w---- 1 root tty 4, 10 Mar 21 12:01 tty10 crw-rw---- 1 root dialout 4, 74 Mar 21 12:01 ttyS10 crw------- 1 root root 10, 63 Mar 21 12:01 vga_arbiter crw------- 1 root root 10, 137 Mar 21 12:01 vhci |
現在我們看看裝置驅動是如何使用連結串列維護裝置列表的,首先,我們看一下 miscdevice
的 struct 定義:
1 2 3 4 5 6 7 8 9 10 11 |
struct miscdevice { int minor; const char *name; const struct file_operations *fops; struct list_head list; struct device *parent; struct device *this_device; const char *nodename; mode_t mode; }; |
可以看到 miscdevice
的第四個成員 list
,這個就是用於維護已註冊裝置連結串列的結構。在原始碼文的首部,我們可以看到以下定義:
1 |
static LIST_HEAD(misc_list); |
這個定義巨集展開,可以看到是用於定義 list_head
型別變數:
1 2 |
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name) |
LIST_HEAD_INIT
這個巨集用於對定義的變數進行雙向指標的初始化:
1 |
#define LIST_HEAD_INIT(name) { &(name), &(name) } |
現在我看可以看一下函式 misc_register
是如何進行裝置註冊的。首先是用 INIT_LIST_HEAD
對 miscdevice->list
成員變數進行初始化:
1 |
INIT_LIST_HEAD(&misc->list); |
這個操作與 LIST_HEAD_INIT
巨集一致:
1 2 3 4 5 |
static inline void INIT_LIST_HEAD(struct list_head *list) { list->next = list; list->prev = list; } |
接下來,在通過函式 device_create
進行裝置建立,同時將裝置新增到 Misc 裝置列表中:
1 |
list_add(&misc->list, &misc_list); |
核心的 list.h
檔案提供向連結串列新增節點的 API,這裡是新增操作的實現:
1 2 3 4 |
static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } |
函式實現很簡單,就是入參轉換為三個引數後呼叫內部 __list_add
:
- new – 新節點;
- head – 新節點插入的雙向連結串列頭;
- head->next – 連結串列頭的下一個節點;
_list_add
函式的實現更加簡單:
1 2 3 4 5 6 7 8 9 |
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; } |
這裡設定了新新增結點的 prev
與 next
指標,通過這些操作,就將先前使用 LIST_HEAD_INIT
所定義的 misc
連結串列的雙向指標與 miscdevice->list
結構關聯起來。
這裡還有一個問題,就是如何獲取連結串列中的資料,list_head
提供了一個特殊的巨集用於獲取資料指標。
1 2 |
#define list_entry(ptr, type, member) container_of(ptr, type, member) |
這裡有三個引數
- ptr:list_head 結構指標
- type:資料對應的 struct 型別
- member:資料中 list_head 成員對應的成員變數名
舉例如下:
1 |
const struct miscdevice *p = list_entry(v, struct miscdevice, list) |
接下來我們就夠訪問 miscdevice
的各個成員,如 p->minor
、p->name
等等,我們看一下 list_entry
的實現:
1 2 |
#define list_entry(ptr, type, member) container_of(ptr, type, member) |
其實現非常簡單,就是使用入參呼叫 container_of
巨集,巨集的實現如下:
1 2 3 |
#define container_of(ptr, type, member) ({ const typeof( ((type *)0)->member ) *__mptr = (ptr); (type *)( (char *)__mptr - offsetof(type,member) );}) |
注意,巨集使用了大括號表示式,對於大括號表示式,編譯器會展開所有表示式,同時使用最後一個表示式的結果進行返回。
舉個例子:
1 2 3 4 5 6 7 |
#include <stdio.h> int main() { int i = 0; printf("i = %dn", ({++i; ++i;})); return 0; } |
輸出結果為 2
。
另一個關鍵是 typeof
關鍵字,這個非常簡單,這個正如它的名字一樣,這個關鍵字返回的結果是變數的型別。當我第一次看到這個巨集時,最讓我覺得奇怪的是表示式 ((type*)0)
中的 0
值,實際上,使用 0
值作為地址這個是成員變數取得 struct 內相對偏移地址的巧妙實現,我們再來看個例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> struct s { int field1; char field2; char field3; }; int main() { printf("%pn", &((struct s*)0)->field3); return 0; } |
輸出結果為 0x5
。
還有一個專門用於獲取結構體中某個成員變數偏移的巨集,其實現與前面提到的巨集非常類似:
1 |
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) |
這裡對 container_of
巨集做個綜述,container_of
巨集通過 struct 中的 list_head
成員返回 struct 對應資料的記憶體地址。在巨集的第一行定義了指向 list_head
成員的指標 __mptr
,並將 ptr
地址賦給 __mptr
。從技術實現的角度來看,實際並不需要這一行定義,但這個對於型別檢查而言非常有意義。這一行程式碼確保結構體( type
)中存在 member
對應的成員。第二行使用 offsetoff
巨集計算出包含 member
的結構體所對應的記憶體地址,就是這麼簡單。
當然 list_add
與 list_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
未了,需要說的是,核心程式碼中並不僅僅只有上述這些介面。