Linux內核中的算法和數據結構


算法和數據結構紛繁復雜,但是對於Linux Kernel開發人員來說重點了解Linux內核中使用到的算法和數據結構很有必要。

在一個國外問答平台stackexchange.com的Theoretical Computer Science子板有一篇討論實際使用中的算法和數據結構,Vijay D做出了詳細的解答,其中有一部分是Basic Data Structures and Algorithms in the Linux Kernel對Linux內核中使用到的算法和數據結構做出的歸納整理。詳情參考這里

同時有一篇中文翻譯在https://linux.cn/article-2317-1.html可以找到。

下面就以Vijay D的回答作為藍本進行學習總結。

測試方法准備

由於需要在內核中進行代碼測試驗證,完整編譯安裝內核比較耗時耗力。准備采用module形式來驗證。

Makefile

obj-m:=linked-list.o

KERNELBUILD:=/lib/modules/$(shell uname -r)/build

default:
        make -C ${KERNELBUILD} M=$(shell pwd) modules
clean:
        rm -rf *.o *.cmd *.ko *.mod.c .tmp_versions

linked-list.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/list.h>

int linked_list_init(void)
{
    printk("%s\n", __func__);
    return 0;
}

void linked_list_exit(void)
{
    printk("%s\n", __func__);
}

module_init(linked_list_init);
module_exit(linked_list_exit);
MODULE_AUTHOR("Arnold Lu");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Linked list test");

安裝module

sudo insmod linked-list.ko

查找安裝情況

lsmod | grep linked-list

執行log

<4>[621267.946711] linked_list_init
<4>[621397.154534] linked_list_exit

刪除module

sudo rmmod linked-list

 

鏈表、雙向鏈表、無鎖鏈表

Linked list, doubly linked list, lock-free linked list.

有一篇關於內核鏈表深入分析Linux內核鏈表值得參考。

鏈表是一種常用的組織有序數據的數據結構,它通過指針將一系列數據節點連接成一條數據鏈,是線性表的一種重要實現方式。相對於數組,鏈表具有更好的動態性,建立鏈表時無需預先知道數據總量,可以隨機分配空間,可以高效地在鏈表中的任意位置實時插入或刪除數據。鏈表的開銷主要是訪問的順序性和組織鏈的空間損失。

通常鏈表數據結構至少應包含兩個域:數據域和指針域,數據域用於存儲數據,指針域用於建立與下一個節點的聯系。按照指針域的組織以及各個節點之間的聯系形式,鏈表又可以分為單鏈表、雙鏈表、循環鏈表等多種類型.

image001

通過設計前驅和后繼兩個指針域,雙鏈表可以從兩個方向遍歷,這是它區別於單鏈表的地方。如果打亂前驅、后繼的依賴關系,就可以構成"二叉樹";如果再讓首節點的前驅指向鏈表尾節點、尾節點的后繼指向首節點(如圖2中虛線部分),就構成了循環鏈表;如果設計更多的指針域,就可以構成各種復雜的樹狀數據結構。

image002

循環鏈表的特點是尾節點的后繼指向首節點。前面已經給出了雙循環鏈表的示意圖,它的特點是從任意一個節點出發,沿兩個方向的任何一個,都能找到鏈表中的任意一個數據。如果去掉前驅指針,就是單循環鏈表。

Simple doubly linked list

數據結構:

struct list_head {
    struct list_head *next, *prev;
};

 

聲明和初始化:
static inline void INIT_LIST_HEAD(struct list_head *list)

在表頭插入和在表尾插入:
static inline void list_add(struct list_head *new, struct list_head *head)

static inline void list_add_tail(struct list_head *entry, struct list_head *head)

刪除,被刪除的節點prev、next分別被設為LIST_POISON2、LIST_POISON1,當訪問此節點時會引起葉故障。保證不在鏈表中的節點項不可訪問。
static inline void list_del(struct list_head *entry)

static inline void list_del_init(struct list_head *entry)  將entry從鏈表解下來,重新初始化,就可以訪問節點。

將節點從一個鏈表搬移到另一個鏈表,根據插入表頭和表位分兩種
static inline void list_move(struct list_head *list, struct list_head *head)

static inline void list_move_tail(struct list_head *list, struct list_head *head)

用新節點替換糾結點:
static inline void list_replace(struct list_head *old, struct list_head *new)

將list插入到head:
static inline void list_splice(const struct list_head *list, struct list_head *head)

static inline void list_splice_tail(struct list_head *list, struct list_head *head)

static inline void list_splice_init(struct list_head *list, struct list_head *head) 將list設為空鏈表

static inline void list_splice_tail_init(struct list_head *list, struct list_head *head) 將list設為空鏈表

image004

static inline void list_cut_position(struct list_head *list, struct list_head *head, struct list_head *entry)

遍歷宏:
list_entry(ptr, type, member)

list_first_entry(ptr, type, member)

list_last_entry(ptr, type, member)

list_next_entry(pos, member)

list_prev_entry(pos, member)

list_for_each(pos, head)

list_for_each_prev(pos, head) 反向操作

list_for_each_safe(pos, n, head) 安全操作

list_for_each_entry(pos, head, member) 遍歷鏈表是獲取鏈表節點

list_for_each_entry_safe(pos, n, head, member) 安全操作

list_for_each_entry_reverse(pos, head, member) 反向操作

判斷鏈表是否為空:
static inline int list_empty(const struct list_head *head)

Doubly linked list with a single pointer list head

linux內核里邊除了著名的list雙向循環鏈表以外,還有一個重要的數據結構,就是哈希鏈表。哈希鏈表也在很多重要的地方有所使用,比如linux內核的dentry,進程查詢,文件系統等,可以說,弄明白hlist對於理解linux內核具有重要的意義。

struct hlist_head {
    struct hlist_node *first;
};

struct hlist_node {
    struct hlist_node *next, **pprev;
};

linux內核的hash鏈表有兩個數據結構組成,一個是hlist_head是hash表的表頭,一個是hlist_node是hash標的后續節點。

在使用的時候,一般定義一個struct hlist_head xxx[100]數組(100只是一個代表的數字,視具體情況而定),采取哈希函數來將鍵值與數組的對應的地址聯系起來,如果出現沖突的話,就在hlist_head的后邊繼續添加。
hlist_head的成員first指針指向后續的第一個節點,如果哈希鏈表是空的話,就為NULL。
為什么hlist_head不弄成雙向鏈表呢,因為為了節約空間,如果一個指針的話,一個哈希數組的空間消耗就會減半。
hlist_node的成員next指向后續的節點的地址,如果為空就是NULL,另一個成員pprev是二級指針,指向前一個節點的next成員的地址,如果前一個成員是hlist_head的話,pprev的值就是前一個的first指針的地址。

#define HLIST_HEAD(name) struct hlist_head name = {  .first = NULL }  定義並且初始化。

#define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL) 在定義之后,需要初始化,不然使用會導致錯誤。

static inline void INIT_HLIST_NODE(struct hlist_node *h) 初始化node節點

static inline int hlist_empty(const struct hlist_head *h) 判斷hash鏈表是否為空

static inline void hlist_del(struct hlist_node *n) 刪除節點,並且將節點next、pprev指針修改為LIST_POSITION1和LIST_POSITION2。

static inline void hlist_del_init(struct hlist_node *n) 此種方法更安全,刪除然后再初始化節點。

static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) 將節點插入到hash鏈表的頭結點后邊。

static inline void hlist_add_before(struct hlist_node *n, struct hlist_node *next) 將一個節點插入到next前面。

static inline void hlist_add_behind(struct hlist_node *n, struct hlist_node *prev) 將一個節點插入到prev后面。

遍歷訪問節點:

hlist_for_each(pos, head)

hlist_for_each_safe(pos, n, head)

#define hlist_entry(ptr, type, member) container_of(ptr,type,member)

hlist_entry_safe(ptr, type, member)

hlist_for_each_entry(pos, head, member)

hlist_for_each_entry_safe(pos, n, head, member)

 

Lock-less NULL terminated single linked list

無鎖鏈表定義在include/linux/llist.h。

數據結構如下:

struct llist_head {
    struct llist_node *first;
};

struct llist_node {
    struct llist_node *next;
};

 

#define LLIST_HEAD(name)    struct llist_head name = LLIST_HEAD_INIT(name)

static inline void init_llist_head(struct llist_head *list)

llist_entry(ptr, type, member)

llist_for_each(pos, node)

static inline bool llist_empty(const struct llist_head *head)

static inline struct llist_node *llist_next(struct llist_node *node)

static inline bool llist_add(struct llist_node *new, struct llist_head *head)

bool llist_add_batch(struct llist_node *new_first, struct llist_node *new_last, struct llist_head *head)

static inline struct llist_node *llist_del_all(struct llist_head *head)

struct llist_node *llist_del_first(struct llist_head *head)

llist_add、llist_add_batch、llist_del_first都是基於cmpxchg原子操作來實現,整個操作是原子的;llist_del_all是基於xchg來實現的。

 

cmpxchg(void* ptr, int old, int new),如果ptr和old的值一樣,則把new寫到ptr內存,否則返回ptr的值,整個操作是原子的。在Intel平台下,會用lock cmpxchg來實現,這里的lock個人理解是鎖住內存總線,這樣如果有另一個線程想訪問ptr的內存,就會被block住。

B+樹

B+ Trees with comments telling you what you can't find in the textbooks.

A relatively simple B+Tree implementation. I have written it as a learning exercise to understand how B+Trees work. Turned out to be useful as well.

...

A tricks was used that is not commonly found in textbooks. The lowest values are to the right, not to the left. All used slots within a node are on the left, all unused slots contain NUL values. Most operations simply loop once over all slots and terminate on the first NUL.

關於B樹及B樹衍生樹有篇介紹不錯《從B樹、B+樹、B*樹談到R 樹》。

B樹誕生的背景:

在大規模數據存儲中,實現索引查詢這樣一個實際背景下,樹節點存儲的元素數量是有限的,這樣就會導致二叉樹結構由於樹的深度過大而造成磁盤I/O讀寫過於頻繁,進而導致查詢效率低下。
那么如何減少樹的深度,一個基本的想法是采用多叉樹結構。
因為磁盤的操作費時費資源,那么如何提高效率,即如何避免頻繁的讀取呢?根據磁盤查找存取的次數往往由樹的高度決定,所以只要通過較好的結構降低樹的高度。根據平衡二叉樹的啟發,自然就想到平衡多叉樹結構。

幾個算法時間復雜度度量:

O(n) 表示某函數值(未列出)是 n 的常數倍;亦即他們增長的速度相當.稱 大O,big O (發音 "歐" 英文字母 O )
同理:O(logN):是 logN 的常數倍;O(nlogn):是 nlogn 的常數倍

 

優先排序列表

Priority sorted lists used for mutexes, drivers, etc.

plist有兩個重要結構體struct plist_head和struct plist_node,分別用來表示plist表頭和plist節點。

struct plist_head {
    struct list_head node_list;
};

struct plist_node {
    int prio;
    struct list_head prio_list;
    struct list_head node_list;
};

相關函數:

PLIST_HEAD(head) 初始化plist表頭
PLIST_NODE_INIT(node, __prio) 初始化plist節點

static inline void plist_head_init(struct plist_head *head) 初始化plist表頭

static inline void plist_node_init(struct plist_node *node, int prio) 初始化plist節點

添加節點、刪除節點:
extern void plist_add(struct plist_node *node, struct plist_head *head); 通過plist_add添加到head的node是按照prio優先級由高到低順序在node_list上排列。
extern void plist_del(struct plist_node *node, struct plist_head *head);
extern void plist_requeue(struct plist_node *node, struct plist_head *head); 是plist_del的優化版本

遍歷plist:
plist_for_each(pos, head)

判斷head是否為空:
static inline int plist_head_empty(const struct plist_head *head)

判斷當前node是否在node_list上:
static inline int plist_node_empty(const struct plist_node *node)

獲取前一、后一節點:
plist_next(pos)
plist_prev(pos)

獲取首節點、尾節點:
static inline struct plist_node *plist_first(const struct plist_head *head)
static inline struct plist_node *plist_last(const struct plist_head *head)

下面是對plist進行的一些驗證:

static dump_list(void)
{
    struct plist_node *node_pos, *first_node, *last_node;
    int i;

    printk(KERN_DEBUG "%s start\n", __func__);
    printk("node_list: ");
    list_for_each_entry(node_pos, &test_head.node_list, node_list) {
        printk("%d ", node_pos->prio);
    }
    printk("\n");

    first_node = plist_first(&test_head);
    last_node = plist_last(&test_head);
    printk("prio_list: %d ", first_node->prio);
    list_for_each_entry(node_pos, &first_node->prio_list, prio_list) {
        printk("%d ", node_pos->prio);
    }
    printk("\n");

#if 0
    for (i = 0; i < ARRAY_SIZE(test_node); i++) {
        if(!plist_node_empty(test_node+i))
            printk(KERN_DEBUG "(test_node+%d)->prio=%d\n", i, (test_node+i)->prio);
    }
#endif
    printk(KERN_DEBUG "MIN(prio)=%d MAX(prio)=%d\n", first_node->prio, last_node->prio);
    printk(KERN_DEBUG "%s end\n", __func__);
}

static int  __init plist_test(void)
{
    int nr_expect = 0, i, loop;
    unsigned int r = local_clock();

    printk(KERN_DEBUG "start plist test\n");
    plist_head_init(&test_head);
    for (i = 0; i < ARRAY_SIZE(test_node); i++)
        plist_node_init(test_node + i, 0);

    for (loop = 0; loop < 10; loop++) {
        r = r * 193939 % 47629;
        i = r % ARRAY_SIZE(test_node);
        if (plist_node_empty(test_node + i)) {
            r = r * 193939 % 47629;
            test_node[i].prio = r % 10;
            plist_add(test_node + i, &test_head);
            nr_expect++;
        } else {
            plist_del(test_node + i, &test_head);
            nr_expect--;
        }
        plist_test_check(nr_expect);
        if (!plist_node_empty(test_node + i)) {
            plist_test_requeue(test_node + i);
            plist_test_check(nr_expect);
        }
    }

    dump_list();

    for (i = 0; i < ARRAY_SIZE(test_node); i++) {
        if (plist_node_empty(test_node + i))
            continue;
        plist_del(test_node + i, &test_head);
        nr_expect--;
        plist_test_check(nr_expect);
    }

    printk(KERN_DEBUG "end plist test\n");
    return 0;
}

通過初始化不超過10個node節點,優先級為0-9。然后查看node_list和prio_list兩鏈表的節點情況:

[22050.404475] start plist test
[22050.404481] dump_list start
[22050.404482] node_list: 0 0 1 1 2 6 8 8 9 9
[22050.404486] prio_list: 0 1 2 6 8 9
[22050.404488] MIN(prio)=0 MAX(prio)=9
[22050.404489] dump_list end
[22050.404491] end plist test
[22050.947810] start plist test
[22050.947816] dump_list start
[22050.947817] node_list: 0 1 1 2 2 3 3 3 8 8
[22050.947820] prio_list: 0 1 2 3 8
[22050.947822] MIN(prio)=0 MAX(prio)=8
[22050.947823] dump_list end
[22050.947825] end plist test
[22051.491245] start plist test
[22051.491254] dump_list start
[22051.491256] node_list: 0 1 2 3 3 3 6 9 9 9
[22051.491262] prio_list: 0 1 2 3 6 9
[22051.491266] MIN(prio)=0 MAX(prio)=9
[22051.491267] dump_list end
[22051.491271] end plist test

可以看出node_list上的節點按照優先級由高到低排序,優先級可能會重復;在prio_list上是不同優先級的節點。如下所示:

* pl:prio_list (only for plist_node)
* nl:node_list
*HEAD|             NODE(S)
*        |
*        ||------------------------------------|
*        ||->|pl|<->|pl|<--------------->|pl|<-|
*        |     |10|   |21|   |21|   |21|   |40|   (prio)
*        |     |  |   |  |   |  |   |  |   |  |
*        |     |  |   |  |   |  |   |  |   |  |
*        | ->|nl|<->|nl|<->|nl|<->|nl|<->|nl|<->|nl|<-|
*        |-------------------------------------------------|

 

紅黑樹

Red-Black trees are used for scheduling, virtual memory management, to track file descriptors and directory entries,etc.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM