0x10 鏈表演化
鏈表是內核中最常見的數據結構,也是其他數據結構的基礎。和數組顯著的區別是,鏈表無需佔用連續的存儲單元,在編譯時,也不需要知道其長度,可以動態插入或者刪除元素。下圖是一個典型的雙向鏈表
圖1 雙向鏈表 1
通過前驅和後繼兩個指針,就可以遍歷整個鏈。如果打亂前驅和後繼的關係,就形成了二叉樹;如果設計更多的指針域,就可以構成各種複雜的數據結構;如果減少一個指針域,就形成單鏈表,如果讓首結點的前驅指向尾結點,則形成循環鏈表;如果只能對鏈表的頭進行插入和刪除操作,則是棧;如果只能對鏈表的首尾進行插入或刪除操作,則是隊列。
0x20 鏈表的定義和操作
鏈表的定義
struct list_head{
struct list_head * next, * prev;
}
這是一個不含數據域的鏈表,可以嵌套到任何結構中,比如,可以按照如下方式定義一個真正含有數據域的鏈表
struct my_list{
void * my_data;
struct list_head list;
}
請注意:結構體的嵌套看似指向自己,其實是指向同一類型的不同結構。
鏈表的聲明和初始化宏
實際上,struct list_head 只是定義了鏈表結點,並沒有定義鏈表的表頭,內核代碼 list.h
中定義了兩個宏
define LIST_HEAD_INIT(name) { &(name), &(name) }
/* 僅初始化*/define LIST_HEAD(name) struct list_head_name = LIST_HEAD_INIT(name)
/* 聲明並初始化*/
調用之後,mylist_head 的前驅和後繼指針都初始化指向自己,這就是一個空鏈表。判斷鏈表是否爲空,就是讓鏈表的頭指向自己。
鏈表增加一個結點
內核代碼 list.h
增加結點的函數是
static inline void list_add()
static inline void list_add_tail()
static 表明這是靜態函數,所謂的靜態函數,實際上是對函數作用域的限制,當前函數僅在文件內有效;inline,表明這是內聯函數,對編譯程序可見,編譯器在調用這個函數時,立刻展開該函數。故而,關鍵字 inline 必須與函數定義體放在一起,才能夠使得函數稱爲內聯函數。inline 函數一般放在頭文件中。
這兩個函數到底怎麼實現的呢?首先看一下如下的內部函數(兩個下劃線就表示內部函數)
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;
}
static inline void list_add(struct list_head * new, struct list_head * head)
{
__list_add(new, head, head->next);
}
鏈表的循環
鏈表的循環是一個比較重要的概念,內核代碼 list.h
定義瞭如下遍歷鏈表的宏
define list_for_each(pos, head) for(pos = (head)->next; pos != (head); pos = pos->next)
這種方式,只是找到每個結點在鏈表中的偏移,有個較爲關鍵的問題是,怎麼通過pos找到結點的起始位置。
圖2 遍歷鏈表
list.h
還定義了 list_entry() 宏,即從一個結構的成員指針找到其容器的指針
#define list_entry(ptr, type, member) /
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
(unsigned long)(&((type *)0
把地址0轉換成 type 結構的指針,緊接着獲取該結構中 member 域的指針,也就獲得了 member 在 type中的偏移量。(type *)(ptr)
得到 ptr 指針的絕對地址,相減,得到 type 類型結構體的起始地址。
圖3 list_entry() 宏
以上就是內核代碼中對鏈表的一些初始操作。
《Linux 操作系統原理與應用》第2版 ↩︎