Linux 內核中自己實現了雙向鏈表,可以在 include/linux/list.h 找到定義。我們將會首先從雙向鏈表數據結構開始介紹內核裡的數據結構。為什麼?因為它在內核裡使用的很廣泛,你只需要在 free-electrons.com 檢索一下就知道了。
首先讓我們看一下在 include/linux/types.h 裡的主結構體:
struct list_head {struct list_head *next,*prev;};你可能注意到這和你以前見過的雙向鏈表的實現方法是不同的。舉個例子來說,在 glib 庫裡是這樣實現的:
structGList{gpointer data;GList*next;GList*prev;};通常來說一個鏈表結構會包含一個指向某個項目的指針。但是 Linux 內核中的鏈表實現並沒有這樣做。所以問題來了:鏈表在哪裡保存數據呢?。實際上,內核裡實現的鏈表是侵入式鏈表(Intrusive list)。侵入式鏈表並不在節點內保存數據-它的節點僅僅包含指向前後節點的指針,以及指向鏈表節點數據部分的指針——數據就是這樣附加在鏈表上的。這就使得這個數據結構是通用的,使用起來就不需要考慮節點數據的類型了。
比如:
struct nmi_desc {spinlock_t lock;struct list_head head;};讓我們看幾個例子來理解一下在內核裡是如何使用 list_head 的。如上所述,在內核裡有很多很多不同的地方都用到了鏈表。我們來看一個在雜項字符驅動裡面的使用的例子。在 drivers/char/misc.c 的雜項字符驅動 API 被用來編寫處理小型硬件或虛擬設備的小驅動。這些驅動共享相同的主設備號:
#define MISC_MAJOR 10但是都有各自不同的次設備號。比如:
ls-l /dev |grep10crw-------1 root root 10,235Mar2112:01 autofsdrwxr-xr-x 10 root root 200Mar2112:01 cpucrw-------1 root root 10,62Mar2112:01 cpu_dma_latencycrw-------1 root root 10,203Mar2112:01 cusedrwxr-xr-x 2 root root 100Mar2112:01 dricrw-rw-rw-1 root root 10,229Mar2112:01 fusecrw-------1 root root 10,228Mar2112:01 hpetcrw-------1 root root 10,183Mar2112:01 hwrngcrw-rw----+1 root kvm 10,232Mar2112:01 kvmcrw-rw----1 root disk 10,237Mar2112:01 loop-controlcrw-------1 root root 10,227Mar2112:01 mcelogcrw-------1 root root 10,59Mar2112:01 memory_bandwidthcrw-------1 root root 10,61Mar2112:01 network_latencycrw-------1 root root 10,60Mar2112:01 network_throughputcrw-r-----1 root kmem 10,144Mar2112:01 nvrambrw-rw----1 root disk 1,10Mar2112:01 ram10crw--w----1 root tty4,10Mar2112:01 tty10crw-rw----1 root dialout 4,74Mar2112:01 ttyS10crw-------1 root root 10,63Mar2112:01 vga_arbitercrw-------1 root root 10,137Mar2112:01 vhci現在讓我們看看它是如何使用鏈表的。首先看一下結構體 miscdevice:
struct miscdevice{int minor;constchar*name;conststruct file_operations *fops;struct list_head list;struct device *parent;struct device *this_device;constchar*nodename;mode_t mode;};可以看到結構體miscdevice的第四個變量list 是所有注冊過的設備的鏈表。在源代碼文件的開始可以看到這個鏈表的定義:
static LIST_HEAD(misc_list);它實際上是對用list_head 類型定義的變量的擴展。
#define LIST_HEAD(name) \struct list_head name = LIST_HEAD_INIT(name)然後使用宏 LIST_HEAD_INIT 進行初始化,這會使用變量name 的地址來填充prev和next 結構體的兩個變量。
#define LIST_HEAD_INIT(name){&(name),&(name)}現在來看看注冊雜項設備的函數misc_register。它在一開始就用函數 INIT_LIST_HEAD 初始化了miscdevice->list。
INIT_LIST_HEAD(&misc->list);作用和宏LIST_HEAD_INIT一樣。
staticinlinevoid INIT_LIST_HEAD(struct list_head *list){list->next=list;list->prev =list;}接下來,在函數device_create 創建了設備後,我們就用下面的語句將設備添加到設備鏈表:
list_add(&misc->list,&misc_list);內核文件list.h 提供了向鏈表添加新項的 API 接口。我們來看看它的實現:
staticinlinevoid list_add(struct list_head *new,struct list_head *head){__list_add(new,head,head->next);}實際上就是使用3個指定的參數來調用了內部函數__list_add:
head的後面head 後面的項。__list_add的實現非常簡單:
staticinlinevoid __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 的向前指針和向後指針。
這兒還有一個問題:如何得到列表的內容呢?這裡有一個特殊的宏:
#define list_entry(ptr, type, member) \container_of(ptr, type, member)使用了三個參數:
list_head 的指針;list_head 的變量的名字;比如說:
conststruct miscdevice *p = list_entry(v,struct miscdevice,list)然後我們就可以使用p->minor 或者 p->name來訪問miscdevice。讓我們來看看list_entry 的實現:
#define list_entry(ptr, type, member) \container_of(ptr, type, member)如我們所見,它僅僅使用相同的參數調用了宏container_of。初看這個宏挺奇怪的:
#define container_of(ptr, type, member)({ \consttypeof(((type *)0)->member )*__mptr =(ptr); \(type *)((char*)__mptr - offsetof(type,member));})首先你可以注意到花括號內包含兩個表達式。編譯器會執行花括號內的全部語句,然後返回最後的表達式的值。
舉個例子來說:
#include<stdio.h>int main(){int i =0;printf("i = %d\n",({++i;++i;}));return0;}最終會打印出2。
下一點就是typeof,它也很簡單。就如你從名字所理解的,它僅僅返回了給定變量的類型。當我第一次看到宏container_of的實現時,讓我覺得最奇怪的就是表達式((type *)0)中的0。實際上這個指針巧妙的計算了從結構體特定變量的偏移,這裡的0剛好就是位寬裡的零偏移。讓我們看一個簡單的例子:
#include<stdio.h>struct s {int field1;char field2;char field3;};int main(){printf("%p\n",&((struct s*)0)->field3);return0;}結果顯示0x5。
下一個宏offsetof會計算從結構體起始地址到某個給定結構字段的偏移。它的實現和上面類似:
#define offsetof(TYPE, MEMBER)((size_t)&((TYPE *)0)->MEMBER)現在我們來總結一下宏container_of。只需給定結構體中list_head類型 字段的地址、名字和結構體容器的類型,它就可以返回結構體的起始地址。在宏定義的第一行,聲明了一個指向結構體成員變量ptr的指針__mptr,並且把ptr 的地址賦給它。現在ptr 和__mptr 指向了同一個地址。從技術上講我們並不需要這一行,但是它可以方便地進行類型檢查。第一行保證了特定的結構體(參數type)包含成員變量member。第二行代碼會用宏offsetof計算成員變量相對於結構體起始地址的偏移,然後從結構體的地址減去這個偏移,最後就得到了結構體。
當然了list_add 和 list_entry不是<linux/list.h>提供的唯一功能。雙向鏈表的實現還提供了如下API:
等等很多其它API。
via: https://github.com/0xAX/linux-insides/blob/master/DataStructures/dlist.md
譯者:Ezio 校對:Mr小眼兒
本文由 LCTT 原創編譯,Linux中國 榮譽推出