Linux內核設計與實現筆記(6)----內核數據結構

內核數據結構

本章介紹幾種Linux內核常用的內建數據結構,其中最常用的有:

鏈表

隊列

映射

二叉樹

1. 鏈表

鏈表是一種存放和操作可變數量節點的數據結構。

鏈表和靜態數組的區別:

1. 鏈表包含的元素都是動態創建并插入鏈表的,在編譯時不知道需要創建多少個元素。

2. 因為鏈表中每個節點的創建時間各不相同,所以各節點在內存中無需占用連續內存區

單向鏈表和雙向鏈表

單向鏈表:

在鏈表中,每個節點都包含有一個指向下一個節點的指針,只能向后連續的鏈表,即單向鏈表。如下:

// 單向鏈表中的一個節點

struct list_element {

void *data;    // 有效數據

struct list_element *next;    // 指向下一個節點的指針

};

雙向鏈表:

在鏈表中,每個節點都包含一個指向下一個和指向前一個節點的指針,可以同時向前和向后相互連接,即雙向鏈表。如下:

// 雙向鏈表中的一個節點

struct list_element {

void *data;    // 有效數據

struct list_element *next;    // 指向下一個節點的指針

struct list_element *prev;    // 指向前一個節點的指針

};

環形鏈表

通常情況下,鏈表的最后一個節點沒有下一個節點,所以鏈表末尾節點的向后指針設置為NULL。如果將鏈表末尾節點的向后節點指向鏈表的首節點,鏈表首尾相連,即構成環形鏈表。

在環形雙向鏈表中,尾節點的向后指針指向鏈表的首節點,鏈表首節點的向前指針指向鏈表的尾節點,構成環形雙向鏈表。

Linux內核的標準鏈表采用環形雙向鏈表形式實現。具有最大的靈活性。

沿鏈表移動

鏈表的訪問方法是沿鏈表線性移動。即先訪問某個元素,再沿該節點的向后指針訪問下一個節點,依次向后移動。

在有的鏈表中,首元素會用一個特殊指針表示,即頭指針,構成有頭鏈表。可以利用頭指針找到鏈表的起始節點。

在非環形鏈表中,向后指針指向NULL的節點是尾節點。

在環形鏈表中,向后指針指向頭節點的節點是尾節點。

遍歷一個雙向鏈表需要線性地訪問從第一個節點到最后一個節點之間的所有節點。也可以從鏈表中的任意指定節點開始向前或向后訪問別的節點。

Linux內核中的實現

普通的鏈表實現方式,通過在數據結構體中添加向前和向后的節點指針,將節點串聯在鏈表中。如:

struct node {

int data;    // 節點數據

struct node *next;    // 指向下一個節點的指針

struct node *prev;    // 指向上一個節點的指針

};

Linux內核中的鏈表實現方式不一樣,不是將數據結構填入鏈表節點,而是將鏈表節點填入數據結構中。

1. 鏈表數據結構

include <linux/list.h>

struct list_head {

struct list_head *next;

struct list_head *prev;

};

內核鏈表使用方法:

include <linux/list.h>

struct node {

int data;    // 節點數據

struct list_head list;    // 雙向循環鏈表頭

};

在Linux中提供了一組對鏈表的操作,這些操作只接受list_head結構作為參數。

2. 定義一個鏈表

list_head本身沒有意義,需要被嵌入到數據結構中使用。如上所示。

鏈表需要在使用前初始化,動態創建并初始化鏈表:

struct node *list_node;

list_node = kmalloc(sizeof(*list_node), GFP_KERNEL);

list_node->data = 1;

INIT_LIST_HEAD(list_node->list);

如果結構在編譯期靜態創建:

struct list_node {

.data = 1;

.list = LIST_HEAD_INIT(lise_node.list);

};

3. 鏈表頭

鏈表的鏈表頭,用來作為指向整個鏈表的索引。

static LIST_HEAD(list_node);

定義并初始化一個名為list_node的鏈表。

操作鏈表

Linux內核提供的鏈表操作,在<linux/list.h>文件中實現。所有函數的時間復雜度都是O(1)。

1. 向鏈表中增加一個節點

list_add(struct list_head *new, struct list_head *head); // 在head節點后插入new節點

list_add_tail(struct list_head *new, struct list_head *head); // 在head節點前插入new節點

因為鏈表是環形循環的,并且沒有首尾節點概念,所以可以把任何一個節點當成head.

2. 從鏈表中刪除一個節點

list_del(struct list_head *entry); // 將entry節點從鏈表中刪除

注意,該操作不會釋放entry或者包含entry的數據結構占用的內存,還需要再撤銷這些內存。

從鏈表中刪除一個節點并對其重新初始化:

list_del_init();

list_del_init(struct list_head *entry); // 刪除entry節點,并初始化entry節點

3. 移動和合并鏈表節點

把節點從一個鏈表移到另一個鏈表:

list_move(struct list_head *list, struct list_head *head); // 從一個鏈表中移除list節點,并將其加入到另一個鏈表的head節點后

list_move_tail(struct list_head *list, struct list_head *head); // 從一個鏈表中移除list節點,并將其加入到另一個鏈表的head節點前

檢查鏈表是否為空:

list_empty(struct list_head *head); // 檢查鏈表是否為空

如果鏈表為空,返回非0值;否則返回0.

把兩個未連接的鏈表合并為一個鏈表:

list_splice(struct list_head *list, struct list_head *head); // 合并兩個鏈表,將list指向的鏈表插入到指定鏈表的head節點后

list_splice_init(struct list_head *list, struct list_head *head); // 合并兩個鏈表,將list指向的鏈表插入到指定鏈表的head節點后,并初始化list指向的鏈表

遍歷鏈表

鏈表是一個能夠包含重要數據的容器,需要用鏈表移動來訪問指定的節點數據。

遍歷鏈表的時間復雜度為O(n),n為鏈表包含節點數目。

1. 基本方法

遍歷鏈表最簡單的方法是使用list_for_each()宏。使用兩個list_head類型的參數,第一個參數用來指定當前項,臨時變量;第二個參數是需要遍歷的鏈表以頭節點形式存在的list_head。每次遍歷時,第一個參數在鏈表中不斷移動指向下一個節點,知道鏈表中所有節點都被訪問為止。用法如下:

struct list_head *p;

list_for_each(p, list) {

// p指向鏈表中的節點

}

通過list_for_each()宏,可以得到指向鏈表結構的指針,但是這個指針對于用戶來說并無實際用處,用戶需要的是一個指向包含list_head的結構體的指針,而不是list_head類型的指針。

可以通過list_entry()宏來通過list_head指針獲取到包含給定list_head的數據結構。如:

struct list_head *p;

struct fox *f;

list_for_each(p, &node_list) {

 f = list_entry(p, struct fox, list);

}

2. 可用的方法

Linux內核采用list_for_each_entry()宏來遍歷鏈表,該宏內部使用了list_entry()宏。

list_for_each_entry(pos, head, member);

pos是一個指向包含list_head節點對象的指針,可以當做是list_entry()宏的返回值。

head是一個指向頭節點的指針,即遍歷開始的位置。

member是pos中list_head結構的變量名。

用法如下:

struct list_node *f;

list_for_each_entry(f, &node_list, list) {

 // ...

}

3. 反向遍歷鏈表

list_for_each_entry_reverse()和list_for_each_entry()類似,不同點是,反向遍歷。用法相同:

list_for_each_entry_reverse(pos, head, member);

可能用到反向遍歷鏈表的情況:

1. 性能原因,如:已知節點在搜索起始點前邊

2. 順序很重要時,如:堆棧中的先進先出

4. 遍歷的同時刪除

list_for_each_entry_safe(pos, next, head, member);

比list_for_each_entry(pos, head, member)安全是因為,標準的鏈表遍歷在遍歷鏈表的同時是不能刪除的。

// 反向遍歷鏈表的同時刪除節點

list_for_each_entry_safe_reverse(pos, n, head, member);

注意:可能會有并發地刪除鏈表中的不同節點,list_for_each_entry_safe()刪除節點時需要鎖定鏈表。

2. 隊列

生產者和消費者模型中,生產者創造數據,而消費者讀取消息和處理包,或者以其他方式消費這些數據。

最簡單的實現方法是隊列,生產者將數據推進隊列,消費者從隊列中讀取數據。消費者獲取數據的順序和生產者推入隊列的順序一致。

隊列,即FIFO(First in first out,先進先出)。Linux內核通用隊列實現稱為kfifo,在kernel/kfifo.c文件中實現,聲明在<linux/kfifo.h>文件中。

隊列.jpg

1. kfifo

linux的kfifo,兩個主要操作:enqueue(入隊列)和dequeue(出隊列)。

有兩個偏移量:入口偏移(下一次入隊列時的位置)和出口偏移(下一次出隊列時的位置)。出口偏移總是小于等于入口偏移。

enqueue操作拷貝數據到隊列中的入口偏移位置,并將入口偏移加上推入元素數目。

dequeue操作從隊列中出口偏移處拷貝數據,并出口偏移減去摘取的元素數目。

隊列空:出口偏移等于入口偏移時。

隊列滿:入口偏移等于隊列長度時。

2. 創建隊列

定義和初始化

// 動態

int kfifo_alloc(struct kfifo *fifo, unsigned int size, gfp_t gfp_mask); // 創建并初始化一個大小為size的kfifo

內核使用gfp_mask標識分配隊列。成功返回0;錯誤返回一個負數錯誤碼。例如:

// 創建一個隊列,名為fifo,大小為PAGE_SIZE

struct kfifo fifo;

int ret;

ret = kfifo_alloc(&kfifo, PAGE_SIZE, GFP_KERNEL);

if (ret) {

 return ret;

}

或者

void kfifo_init(struct kfifo *fifo, void *buffer, unsigned int size);

// 創建并初始化一個kfifo對象,將使用由buffer指向的size字節大小的內存

kfifo_alloc()和kfifo_init()的size必須是2的冪。

// 靜態聲明

DECLARE_KFIFO(name, size); // size必須是2的冪

INIT_KFIFO(name);

3. 推入隊列數據

unsigned int kfifo_in(struct kfifo *fifo, const void *from, unsigned int len);

// 把from指針所指的len字節數據拷貝到fifo所指的隊列中。成功返回推入數據字節大小。

4. 摘取隊列數據

unsigned int kfifo_out(struct kfifo *fifo, void *to, unsigned int len);

// 從fifo所指的隊列中拷貝出長度為len字節的數據到to所指的緩沖中。成功返回拷貝的數據長度。

kfifo_out操作后的隊列中不再有讀取的數據。

unsigned int kfifo_out_peek(struct kfifo *fifo, void *to, unsigned int len, unsigned offset);

// offset指向隊列中的索引位置

// 和kfifo_out類似,不同之處是出口偏移不增加,即摘取的數據在下次摘取時還在隊列中。

5. 獲取隊列長度

// 獲取kfifo隊列的空間總體大小(以字節為單位)

static inline unsigned int kfifo_size(struct kfifo *fifo);

// 獲取kfifo隊列中已推入的數據大小

static inline unsigned int kfifo_len(struct kfifo *fifo);

// 獲取kfifo隊列中剩余可用空間大小

static inline unsigned int kfifo_avail(struct kfifo *fifo);

// 判斷kfifo隊列是否為空

static inline int kfifo_is_empty(struct kfifo *fifo);

// 判斷kfifo隊列是否滿

static inline int kfifo_if_full(struct kfifo *fifo);

// 這兩個函數判斷為空或滿時,返回非0值;否則返回0

6. 重置和撤銷隊列

重置kfifo,即拋棄隊列中的所有內容

static inline void kfifo_reset(struct kfifo *fifo);

撤銷使用kfifo_alloc()分配的隊列

void kfifo_free(struct kfifo *fifo);

撤銷使用kfifo_init()創建的隊列時,需要釋放相關的緩沖

7. 隊列使用舉例

已經創建一個名為fifo的8KB大小的kfifo

// 向隊列中推入數據

unsigned int i;

for (i = 0; i < 32; i++) {

 kfifo_in(fifo, &i, sizeof(i));

}

// 查看隊列中數據

unsigned int val;

int ret;

ret = kfifo_out_peek(fifo, &val, sizeof(val), 0);

if (ret != sizeof(val)) {

 return -EINVAL;

}

printk(KERN_INFO "%u\n", val);

// 摘取并打印kfifo中的所有數據

while (kfifo_avail(fifo)) {

 unsigned int val;

 int ret;

 ret = kfifo_out(fifo, &val, sizeof(val));

 if (ret != sizeof(val)) {

      return -EINVAL;

 }

 printk(KERN_INFO "%u\n", val);

}

3. 映射

一個映射,也稱為關聯數組。是一個由唯一鍵組成的集合,每個鍵必然關聯一個特定的值,這種鍵到值的關聯關系稱為映射。

映射至少支持三個操作:

Add (key, value)

Remove (key)

value = Lookup (key)

還沒有學明白,明白之后再補充上

4. 二叉樹

樹結構是一個能提供分層的樹型數據結構的特定數據結構。

二叉樹是每個節點最多只有兩個子節點的樹。

二叉樹.jpg

1. 二叉搜索樹

二叉搜索樹(簡稱BST)是一個節點有序的二叉樹,有以下法則:

1. 根的左分支節點值都小于根節點值

2. 右分支節點值都大于根節點值

3. 所有的子樹也都是二叉搜索樹

二叉搜索樹1.jpg

2. 自平衡二叉搜索樹

平衡二叉搜索樹是一個所有葉子節點深度差不超過1的二叉搜索樹。

自平衡二叉搜索樹是指其操作都試圖維持(半)平衡的二叉搜索樹。

平衡二叉搜索樹.jpg

1. 紅黑樹

紅黑樹是一種自平衡二叉搜索樹。Linux主要的平衡二叉樹數據結構就是紅黑樹。

之所以能維持半平衡結構,是因為有以下屬性:

1. 所有的節點要么著紅色,要么著黑色

2. 葉子節點都是黑色

3. 葉子節點不包含數據

4. 所有非葉子節點都是兩個子節點

5. 如果一個節點是紅色,則它的子節點都是黑色

6. 在一個節點到其葉子節點的路徑中,如果總是包含同樣數目的黑色節點,則該路徑相比其他路徑是最短的。

上述條件保證了最深的葉子節點的深度不會大于兩倍的最淺葉子節點的深度。所以紅黑樹總是半平衡的。

2. rbtree

Linux實現的紅黑樹稱為rbtree。定義在lib/rbtree.c文件,聲明在<linux/rbtree.h>文件。

rbtree的根節點由數據結構rb_root表示。

// 創建一個紅黑樹

struct rb_root root = RB_ROOT;

樹里的其他節點由結構rb_node表示。

紅黑樹的時間復雜度是對數關系。

rbtree的實現沒有提供搜索和插入例程,需要用戶定義。

5. 數據結構以及選擇

Linux中最重要的四種數據結構:鏈表、隊列、映射、紅黑樹。

優先選擇鏈表的情況:

1)對數據集合的主要操作是遍歷數據。

2)當性能并非首要考慮因素時

3)需要存儲相對較少的數據項時

4)需要和內核中其他使用鏈表的代碼交互時

5)需要存儲一個大小不明的數據集合時

選擇使用隊列的情況:

1)符合生產者/消費者模式(即FIFO)時

2)需要一個定長緩沖時

選擇使用映射的情況:

1)需要映射一個UID到一個對象時

2)Linux的映射接口針對UID到指針的映射

3)需要處理發給用戶空間的描述符時

選擇使用紅黑樹的情況:

1)需要存儲大量數據,并且檢索迅速時

如果沒有執行太多次時間緊迫的查找操作,最好使用鏈表。

Linux還有一些其他的數據結構,比如:基樹(trie類型)和位圖。

6. 算法復雜度

漸進行為,指當算法的輸入變得非常大或接近于無限大時算法的行為。

算法的伸縮度,當輸入增大時算法執行的變化。

1. 算法

算法,是一系列的指令,可能有一個或多個輸入,最后產生一個結果或輸出。

從數學角度講,一個算法好比一個函數。

2. 大O符號

大O符號用來描述增長率。

4. 時間復雜度

時間復雜度表.jpg

7. 小結

本節主要講述了鏈表、隊列、映射、二叉樹數據結構。應該重用已經存在的內核基礎設施。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379

推薦閱讀更多精彩內容

  • 0.這是什么?有什么用?Linux大神將雙向循環鏈表需要開發者對鏈表基本操作函數的封裝,便于直接調用(復雜到用指針...
    jkCodic閱讀 1,382評論 0 0
  • 本文內容:1、 什么是鏈表?2、 鏈表共分幾類?3、 鏈表的 C 實現! 總表:《數據結構?》 工程代碼 Gith...
    半紙淵閱讀 40,022評論 0 54
  • 1 序 2016年6月25日夜,帝都,天下著大雨,拖著行李箱和同學在校門口照了最后一張合照,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,125評論 0 12
  • 本文內容取自于小甲魚的數據結構與算法。http://www.lxweimin.com/p/230e6fde9c75 ...
    阿阿阿阿毛閱讀 2,920評論 0 7
  • 訴說近況,但是不想打電話告訴父母,第一是怕擔心,實際上是很擔心;第二是實在不想回去,一心想去上海。 可現實卻是矛盾...
    一顆小雞蛋閱讀 303評論 0 1