Linux內核設計與實現 總結筆記(第六章)內核數據結構

內核數據結構

Linux內核實現了這些通用數據結構,並且提倡你們在開發時重用。node

內核開發者應該儘量地使用這些數據結構,而不要自做主張的山寨方法。linux

通用的數據結構有如下幾種:鏈表、隊列、映射和二叉樹算法

 

1、鏈表

1.1 單向鏈表和雙向鏈表

鏈表是Linux中最簡單、最普通的數據結構。shell

最簡單的數據結構表示一個鏈表:數組

/* 一個鏈表中的一個元素 */
struct list_element {
    void *data;                           /* 有效數據 */
    struct list_element *next;       /* 指向下一個元素的指針 */
};
list_element

而後還有雙向鏈表數據結構

/* 一個鏈表中的一個元素 */
struct list_element {
    void *data;             /* 有效數據 */
    struct list_element *next;      /* 指向下一個元素的指針 */
    struct list_element *prev;      /* 指向前一個元素的指針 */
};
list_element

 

1.2 環形鏈表

一般狀況下,鏈表最後一個元素後面沒有元素了,因此將鏈表元素中的向後指針設置爲NULL,以此代表是鏈表中的最後一個元素。ide

在有些鏈表中,鏈表尾元素指向鏈表首元素,這種鏈表首位相連,被稱爲環形鏈表函數

 

1.3 沿鏈表移動

只能是線性移動,先訪問某個元素,而後訪問下一個元素,不斷重複。oop

若是須要隨機訪問,通常不使用鏈表。spa

有時,首元素會用一個特殊指針表示,該指針稱爲頭指針。

 

1.4 Linux內核中的實現

linux的內核方式不同凡響,它不是將數據結構塞入鏈表,而是將鏈表結點塞入數據結構。

鏈表的數據結構在<linux/list.h>中聲明,結構很簡單:

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

這樣子咱們能夠用這個來實現一個鏈表了

struct fox {
    unsigned long tail_length;    /* 尾巴長度,以釐米爲單位 */
    unsigned long weight;          /* 重量,以千克爲單位 */
    bool is_fantasitic                 /* 這隻狐狸奇妙嗎? */
    struct list_head list;             /* 全部fox結構體造成鏈表 */
};
fox例子

可是list_head的頭鏈表要找到用戶自動義的結構體指針仍是要費點功夫

#define container_of(ptr, type, member) ({              \
    const typeof( ((type *)0)->member) *__mptr = (ptr); \
    (type *)( (char *)__mptr - offsetof(type, member) );})

#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)
list_entry

依靠list_entry()方法,內核提供了建立、操做以及其餘鏈表管理的各類例程。全部這些方法都不須要知道list_head所嵌入對象的數據結構。

1.4.2 定義一個鏈表

鏈表須要在使用前初始化,最多見的方式是在運行時初始化鏈表

struct fox *red_fox;
red_fox = kmalloc(sizeof(struct fox), GFP_KERNEL);
red_fox->tail_length = 40;
red_fox->weight = 6;
red_fox->is_fantastic = false;
INIT_LIST_HEAD(&red_fox->list);
鏈表初始化

固然若是一個結構在編譯期靜態建立,須要在其中給出一個鏈表的直接引用:

struct fox red_fox = {
    .tail_length = 40,
    .weight = 6,
    .list = LIST_HEAD_INIT(red_fox.list),
};
鏈表靜態建立

1.4.3 鏈表頭

鏈表須要一個標準的索引指針指向整個鏈表,即鏈表的頭文件。

內核鏈表最傑出的特性就是:任何節點都是無差異的,索引整個鏈表的節點,也是一個常規的節點。

static LIST_HEAD(fox_list);

該函數定義並初始化了一個名爲fox_list的鏈表例程。

 

1.5 操做鏈表

相關的函數都在文件<linux/list.h>中有原型,大多都是之內聯函數的形式實現的。

1.5.1 向鏈表中增長一個節點

給鏈表增長一個節點,向指定鏈表的head節點後插入new節點

list_add(struct list_head *new, struct list_head *head);

把節點增長到鏈表尾,

list_add_tail(struct list_head *new, struct list_head *head);

 

1.5.2 從鏈表中刪除一個節點

函數從鏈表中刪除entry元素,該操做不會釋放entry或釋放包含entry的數據結構體所佔用的內存。僅僅是將entry元素從鏈表中一走,調用後一般還須要撤銷包含entry的數據結構體和其餘的entry項。

list_del(struct list_head *entry)

 

1.5.3 移動和合並鏈表節點

/* 把節點從一個鏈表移到另外一個鏈表
    從鏈表中移除list項,而後將其加入到另外一鏈表的head節點後面 */
list_move(struct list_head *list, struct list_head *head);

/* 把節點從一個鏈表移到另外一個鏈表的末尾 
    和list_move同樣,不過是將list項插入到head前面 */
list_move_tail(struct list_head *list, struct list_head *head);

/* 檢查鏈表是否爲空,若是鏈表爲空返回非0,不然返回0 */
list_empty(struct list_head *head);

/* 把兩個未鏈接的鏈表合併在一塊兒
    將list指向的鏈表插入到指定鏈表的head元素後面 */
list_splice(struct list_head *list, struct list_head *head);

/* 把兩個未鏈接的鏈表合併在一塊兒,並從新初始化原來的鏈表
    不一樣於list_splice,list指向的鏈表要被從新初始化 */
list_splice_init(struct list_head *list, struct list_head *head);

若是碰巧已經獲得了next和prev指針,能夠直接調用內部鏈表函數,從而省下一點時間。獲取指針的時間。

 

1.6 遍歷鏈表

和操做鏈表不一樣,鏈表遍歷的複雜度爲O(n),n是鏈表所包含的元素數目

①基本方法

遍歷鏈表最簡單的方法是使用list_for_each()宏

/* 須要使用兩個list_head類型的參數,第一個指向當前項,臨時變量
    第二個指向參數是須要遍歷的鏈表以頭節點的形式存在的list_head 
    每次遍歷,第一個參數在鏈表中不斷移動,知道訪問完全部元素 */
struct list_head *p;
list_for_each(p, list) {
    /* p指向鏈表中的元素 */  
}
list_for_each使用例子

不過得到指向鏈表結構的指針基本沒用,須要使用list_entry()宏,獲取數據結構的指針。

struct  list_head *p;
struct fox *f;
list_for_each(p, &fox_list) {
    /* f points to the struct in which the list is embedded */
    f = list_entry(p, struct fox, list);
}
list_entry使用例子

 

②可用的方法

上面的寫法不夠靈活,因此多數內核採用list_for_each_entry()宏遍歷鏈表

/* 這裏pos是一個指向包含list_head節點對象的指針,能夠當作list_entry的返回值
    head是一個指向頭節點的指針,即遍歷開始位置
    member是pos中list_head結構的變量名 */
list_for_each_entry(pos, head, member);

/* 一個例子 */
struct fox *f;
list_for_each_entry(f, &fox_list, list) {
    /* on each iteration, 'f' points to the next fox structure ... */
}
list_for_each_entry使用例子

在inotify內核文件系統的更新通知機制中,有實際的例子:

static struct inotify_watch *inode_find_handle(struct inode *inode,
    struct inotify_handle *ih)
{
    struct inotify_watch *watch;

    list_for_each_entry(watch, &inode->inotify_watches, i_list) {
        if(watch->ih == ih)
            return watch;
    }
    return NULL;
}
inotify的實際使用例子

 

③反向遍歷鏈表

宏list_for_each_entry_reverse()和list_for_each_entry()相似,不一樣的是它是反向遍歷

/* 函數再也不是沿着next指針遍歷,而是沿着prev遍歷
    用法和list_for_each_entry()相同 */
list_for_each_entry_reverse(pos, head, member);
list_for_each_entry_reverse說明

反向能夠組成相似堆的功能

 

④遍歷的同時刪除

標準的鏈表遍歷是沒法同時刪除節點的。

lsit_for_each_entry_safe(pos, next, head, member)

inotify中也有例子:

void inotify_inode_is_dead(struct inode *inode)
{
    struct inotify_watch *watch, *next;

    mutex_lock(&inode->inotify_mutex);
    list_for_each_entry_safe(watch, next, &inode->inotify_watches, i_list) {
        struct inotify_handle *ih = watch->ih;
        mutex_lock(&ih->mutex);
        inotify_remove_watch_locked(ih, watch); /* deletes watch */
        mutex_unlock(&ih->mutex);
    }
    mutex_unlock(&inode->inotify_mutex);
}
inotify_inode_is_dead例子

內核還提供了反向遍歷並刪除,list_for_each_entry_safe_reverse()

list_for_each_entry_safe_reverse(pos, n, head, member);

剩下的就在<linux/list.h>中。。。

 

1.7 鏈表練習的例子

 

2、隊列

實現生產者和消費者最簡單的方式是使用隊列。

Linux內核通用隊列實現稱爲kfifo。在<kernel/kfifo.h>中聲明,在kernel/kfifo.c中實現。使用前請仔細檢查文件<linux/kfifo.h>

2.1 kfifo

linux的kfifo和多數其餘隊列實現相似,提供兩個主要操做:

  • enqueue(入隊列):拷貝數據到隊列中的入口偏移位置
  • dequeue(出隊列):從隊列中出口偏移處拷貝數據

kfifo對象維護兩個偏移量:

  • 入口偏移量:下一次入隊列時的位置,入口偏移等於出口偏移時隊列爲空,入口偏移等於隊列長度是滿
  • 出口偏移量:下一次出隊列時的位置,出口偏移老是小於等於入口偏移

 

2.2 建立隊列

使用kfifo前,必須對它進行定義和初始化,有靜態和動態分配兩種,動態更廣泛:

/* size:初始化kfifo的大小 */
/* gfp_mask:表示分配隊列,12章詳細討論 */
/* 成功:返回0,錯誤:返回負數錯誤碼 */
int kfifo_alloc(struct kfifo *fifo, unsigned int size, gfp_t gfp_mask);

/* 使用例子 */
struct kfifo fifo;
int ret;

ret = kfifo_alloc(&fifo, PAGE_SIZE, GFP_KERNEL);
if(ret)
    return ret;
/* "fifo"如今表明一個大小爲PAGE_SIZE的隊列 */

/* 若是本身分配緩衝,能夠調用 */
/* 由buffer指定size字節大小的內存,並且提到的size必須是2的冪 */
void kfifo_init(struct kfifo *fifo, void *buffer, unsigned int size);
kfifo_alloc動態分配

靜態分配不太經常使用:

/* 建立一個名稱爲name,大小爲size的kfifo對象 */
DECLARE_KFIFO(name, size);
INIT_KFIFO(name);
kfifo靜態建立

 

2.3 推入隊列數據

當kfifo對象建立和初始化後,推入數據到隊列須要經過kfifo_in()方法完成:

/* from指針所指的len字節數據拷貝到fifo所指定的隊列中
    成功:返回推入數據的字節大小。
    若是隊列中空閒字節小於len,則最多拷貝可用空間大小。
    而後返回值就會小於len,甚至會返回0
unsigned int kfifo_in(struct kfifo *fifo, const void *from, unsigned int len);
kfifo_in()說明

 

 2.4 摘取隊列數據

摘取數據經過函數kfifo_out()完成。

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

從fifo所指的隊列中拷貝出長度爲len字節的數據到to所指的緩衝中。

若是隻是查看數據內容,而不刪除它,可使用kfifo_out_peek()方法。

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

該函數出口偏移不增長,下次還能被kfifo_out得到。

 

2.5 獲取隊列長度

kfifo相關的有,獲取隊列空間整體大小、獲取隊列已推入的數據大小、獲取還有多少可用空間、

判斷隊列空、判斷隊列滿

/* 獲取用於存儲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);
/* 判斷隊列是否爲空 */
static inline int kfifo_is_empty(struct kfifo *fifo);
/* 判斷隊列是否爲滿 */
static inline int kfifo_is_full(struct kfifo *fifo);
獲取kfifo隊列長度

 

2.6 重置和撤銷隊列

若是重置,那麼以前的內容會被拋棄掉。若是撤銷,須要根據不一樣初始化狀況設置。

static inline void kfifo_reset(struct kfifo *fifo);

void kfifo_free(struct kfifo *fifo);
重置和撤銷

 

2.7 隊列使用舉例

 內核例程:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/mutex.h>
#include <linux/kfifo.h>


#define FIFO_SIZE 128

#define PROC_FIFO "record-fifo"

static DEFINE_MUTEX(read_lock);

static DEFINE_MUTEX(write_lock);

#if 0
#define DYNAMIC
#endif

#ifdef DYNAMIC
struct kfifo_rec_ptr_1 test;
#else
typedef STRUCT_KFIFO_REC_1(FIFO_SIZE) mytest;

static mytest test;
#endif

static const char *expected_result[] = {
    "a",
    "bb",
    "ccc",
    "dddd",
    "eeeee",
    "ffffff",
    "ggggggg",
    "hhhhhhhh",
    "iiiiiiiii",
    "jjjjjjjjjj",
};

static int __init testfunc(void)
{
    char buf[100];
    unsigned int i;
    unsigned int ret;
    struct { unsigned char buf[6]; } hello = { "hello" };

    printk(KERN_INFO "record fifo test start\n");

    kfifo_in(&test, &hello, sizeof(hello));

    printk(KERN_INFO "fifo peek len: %u\n" ,kfifo_peek_len(&test));

    for(i=0;i<10;i++) {
        memset(buf, 'a'+i, i+1);
        kfifo_in(&test, buf, i+1);
    }

    printk(KERN_INFO "skip 1st element\n");
    kfifo_skip(&test);

    printk(KERN_INFO "fifo len: %u\n", kfifo_len(&test));

    ret = kfifo_out_peek(&test, buf, sizeof(buf));
    if(ret)
        printk(KERN_INFO "%.*s\n", ret, buf);

    i = 0;
    while(!kfifo_is_empty(&test)) {
        ret = kfifo_out(&test, buf, sizeof(buf));
        buf[ret] = '\0';
        printk(KERN_INFO "item = %.*s\n", ret, buf);
        if(strcmp(buf, expected_result[i++])) {
            printk(KERN_WARNING "value mismatch: test failed\n");
            return -EIO;
        }
    }

    if(i != ARRAY_SIZE(expected_result)) {
        printk(KERN_WARNING "value mismatch: test failed\n");
        return -EIO;
    }
    printk(KERN_INFO "test passed\n");

    return 0;
}

static ssize_t fifo_write(struct file *file, const char __user *buf,
        size_t count, loff_t *ppos)
{
    int ret;
    unsigned int copied;

    if(mutex_lock_interruptible(&write_lock))
        return -ERESTARTSYS;

    ret = kfifo_from_user(&test, buf, count, &copied);

    mutex_unlock(&write_lock);

    return ret ? ret : copied;
}

static ssize_t fifo_read(struct file *file, char __user *buf, size_t count,
        loff_t *ppos)
{
    int ret;
    unsigned int copied;

    if(mutex_lock_interruptible(&read_lock))
        return -ERESTARTSYS;

    ret = kfifo_to_user(&test, buf, count, &copied);

    mutex_unlock(&read_lock);

    return ret ? ret : copied;
}

static const struct file_operations fifo_fops = {
    .owner = THIS_MODULE,
    .read  = fifo_read,
    .write = fifo_write,
    .llseek = noop_llseek,
};

static int __init example_init(void)
{
#ifdef DYNAMIC
    int ret;
    ret = kfifo_alloc(&test, FIFO_SIZE, GFP_KERNEL);
    if(ret) {
        printk(KERN_ERR "error kfifo_alloc\n");
        return ret;
    }
#else
    INIT_KFIFO(test);
#endif
    if(testfunc() < 0) {
#ifdef DYNAMIC
        kfifo_free(&test);
#endif
        return -EIO;
    }

    if(proc_create(PROC_FIFO, 0, NULL, &fifo_fops) == NULL) {
#ifdef DYNAMIC
        kfifo_free(&test);
#endif
        return -ENOMEM;
    }
    return 0;
}

static void __exit example_exit(void)
{
    remove_proc_entry(PROC_FIFO, NULL);
#ifdef DYNAMIC
    kfifo_free(&test);
#endif
}

module_init(example_init);
module_exit(example_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Stefani Seibold <stefani@seibold.net>");
內核例程

讀函數:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <semaphore.h>
#include <pthread.h>

#define DEVICE_NAME "/proc/record-fifo"
#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(arr[0]))

static const char *expected_result[] = {
    "a",
    "bb",
    "ccc",
    "dddd",
    "eeeee",
    "ffffff",
    "ggggggg",
    "hhhhhhhh",
    "iiiiiiiii",
    "jjjjjjjjjj",
};

int main(void)
{
    int fd;
    int ret;
    int i;
    char *buf;
    fd = open(DEVICE_NAME, O_RDWR);
    if(fd < 0) {
        printf("open kfifo err!\n");
        return -1;
    }

    buf = malloc(10);
    if(buf<0)
        return -1;

    while(1) {
        ret = read(fd, buf, 10);
        if(ret < 0)
            printf("read kfifo err!\n");
        else if(ret == 0)
            printf("no kfifo read!\n");
        else {
            printf("----kfifo read----\n");
            buf[ret] = '\0';
            printf("read length is %d, and the string is %s\n", ret, buf);
        }
        sleep(1);
    }
    return 0;
}
myfifo_read.c

寫函數:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <semaphore.h>
#include <pthread.h>

#define DEVICE_NAME "/proc/record-fifo"
#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(arr[0]))

static const char *expected_result[] = {
    "a",
    "bb",
    "ccc",
    "dddd",
    "eeeee",
    "ffffff",
    "ggggggg",
    "hhhhhhhh",
    "iiiiiiiii",
    "jjjjjjjjjj",
};

int main(void)
{
    int fd;
    int ret;
    int i;
    fd = open(DEVICE_NAME, O_RDWR);
    if(fd < 0) {
        printf("open kfifo err!\n");
        return -1;
    }

    while(1) {
        for(i=0;i<ARRAY_SIZE(expected_result);i++) {
            ret = write(fd, expected_result[i], strlen(expected_result[i]));
            printf("the size of array[%d])=%d\n", i, strlen(expected_result[i]));
            if(ret < 0)
                printf("write err!\n");
            sleep(2);
            printf("-----kfifo write -----\n");
        }
    }
    return 0;
}
myfifo_write.c

Makefile:

obj-m+=myfifo.o
#testkfifo-objs:= kfifo.o kn_common.o

EXEC1 = myfifo_write
EXEC2 = myfifo_read

OBJS1 = myfifo_write.o
OBJS2 = myfifo_read.o

CURRENT_PATH:=$(shell pwd)

LINUX_KERNEL:=$(shell uname -r)

LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)

all:$(EXEC1) $(EXEC2) modules
modules:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned

$(EXEC1):$(OBJS1)
    gcc -o $@ $(OBJS1)

$(EXEC2):$(OBJS2)
    gcc -o $@ $(OBJS2)

clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned
    rm -f $(EXEC1) $(EXEC2)
Makefile

 

3、映射

映射也常稱爲關聯數組,實際上是一個由惟一鍵組成的集合,而每一個鍵必然關聯一個特定的值。

Add(key, value);

Remove(key);

value = Lookup(key);

主要是用在分配UID,經過數據結構idr來映射用戶空間的UID

3.1 初始化一個dir

創建一個idr很簡單,首先動態或者靜態分配一個idr數據結構。

void idr_init(struct idr *idp);    /* 動態分配idr結構 */

struct idr id_huh;    /* 靜態定義idr結構 */    
idr_init(&id_huh);    /* 初始化idr結構 */ 
idr初始化

 

3.2 分配一個新的UID

一旦創建idr,就能夠分配新的UID了。具體分爲兩步:

  • 告訴idr須要分配新的UID,容許其在必要時調整後備樹大小
  • 真正請求新的UID。
/* 調整由idp指向的idr的大小 */
/* 成功:返回1,失敗:返回0 */
int idr_pre_get(struct idr *idp, gfp_t gfp_mask);

/* 執行獲取新的UID,並加到idr方法 */
int idr_get_new(struct idr *idp, void *ptr, int *id);
調整後備樹的大小

  

3.3 查找UID

在idr中分配的UID,須要查找他們。 

/* 若是調用成功,返回id關聯的指針 */
/* 若是錯誤,返回空指針 */
/* 值得注意的是若是UID映射的是空指針,哪怕成功也返回NULL */
void *idr_find(struct idr *idp, int id);
idr_find查找UID

 

3.4 刪除UID

/* 將id關聯的指針一塊兒從映射中刪除 */
void idr_remove(struct idr *idp, int id);
從idr中刪除UID

 

3.5 撤銷idr

釋放idr中未使用的內存

/* 不釋放當前分配給UID使用的任何內存 */
void idr_destroy(struct idr *idp);

 

 

4、二叉樹

4.1 二叉搜索樹

二叉搜索樹(BST)是一個節點有序的二叉樹,順序一般遵循下列法則:

  • 根的左分支節點都小於根節點值
  • 右分支節點值都大於根節點值
  • 全部的子樹也都是二叉搜索樹 

樹中搜索一個給定值或按序遍歷樹都至關快捷。

 

4.2 自平衡二叉搜索樹

深度:根結點要到它節點須要通過的父結點數目

高度:樹處於最底層節點的深度

 自平衡二叉樹:全部節點的深度差不超過1

4.2.1 紅黑樹

紅黑樹是一種自平衡二叉搜索樹,主要遵循下面六個屬性

  1. 全部節點要麼着紅色,要麼着黑色
  2. 葉子節點都是黑色
  3. 葉子節點不包含數據
  4. 全部非葉子節點都有兩個子節點
  5. 若是一個節點是紅色,則它的子節點都是黑色
  6. 在一個節點到其葉子節點的路徑中,若是老是包含一樣數目的黑色節點,則該路徑相比其餘路徑是最短的

 

4.2.2 rbtree

在linux中紅黑樹稱爲rbtree,在文件lib/rbtree.c中,聲明在文件<linux/rbtree.h>

建立一個紅黑樹,須要分配一個rb_root結構,而且須要初始化爲特殊值RB_ROOT:

 

5、數據結構以及選擇

上面介紹了四種數據結構:鏈表、隊列、映射和紅黑樹

  • 若是對數據集合的主要操做是遍歷數據,使用鏈表
  • 若是你的代碼符合生產者/消費者模式,使用隊列
  • 若是你須要映射一個UID到一個對象,使用映射
  • 若是你須要存儲大量數據,而且檢索迅速,使用紅黑樹

 

6、算法複雜度

在計算機中,有必要將算法的複雜度量化地表示出來。

6.1 算法

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

6.2 大o符號

 大O符號用來描述這種增加率

6.3 大θ符號

 

6.4 時間複雜度

相關文章
相關標籤/搜索