1. 程式人生 > >Linux核心資料結構

Linux核心資料結構

核心資料結構貫穿於整個核心程式碼中,這裡介紹4個基本的核心資料結構。

利用這4個基本的資料結構,可以在編寫核心程式碼時節約大量時間。

主要內容:

  • 連結串列
  • 佇列
  • 對映
  • 紅黑樹

1. 連結串列

連結串列是linux核心中最簡單,同時也是應用最廣泛的資料結構。

核心中定義的是雙向連結串列。

1.1 標頭檔案簡介

核心中關於連結串列定義的程式碼位於: include/linux/list.h

list.h檔案中對每個函式都有註釋,這裡就不詳細說了。

其實剛開始只要先了解一個常用的連結串列操作(追加,刪除,遍歷)的實現方法,

其他方法基本都是基於這些常用操作的。

1.2 連結串列程式碼的注意點

在閱讀list.h檔案之前,有一點必須注意:linux核心中的連結串列使用方法和一般資料結構中定義的連結串列是有所不同的。

一般的雙向連結串列一般是如下的結構,

  • 有個單獨的頭結點(head)
  • 每個節點(node)除了包含必要的資料之外,還有2個指標(pre,next)
  • pre指標指向前一個節點(node),next指標指向後一個節點(node)
  • 頭結點(head)的pre指標指向連結串列的最後一個節點
  • 最後一個節點的next指標指向頭結點(head)

具體見下圖:

list1 

傳統的連結串列有個最大的缺點就是不好共通化,因為每個node中的data1,data2等等都是不確定的(無論是個數還是型別)。

linux中的連結串列巧妙的解決了這個問題,linux的連結串列不是將使用者資料儲存在連結串列節點中,而是將連結串列節點儲存在使用者資料中。

linux的連結串列節點只有2個指標(pre和next),這樣的話,連結串列的節點將獨立於使用者資料之外,便於實現連結串列的共同操作。

具體見下圖:

list2 

linux連結串列中的最大問題是怎樣通過連結串列的節點來取得使用者資料?

和傳統的連結串列不同,linux的連結串列節點(node)中沒有包含使用者的使用者data1,data2等。

整個list.h檔案中,我覺得最複雜的程式碼就是獲取使用者資料的巨集定義

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

這個巨集沒什麼特別的,主要是container_of這個巨集

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

這裡面的type一般是個結構體,也就是包含使用者資料和連結串列節點的結構體。

ptr是指向type中連結串列節點的指標

member則是type中定義連結串列節點是用的名字

比如:

struct student
{
    int id;
    char* name;
    struct list_head list;
};
  • type是struct student
  • ptr是指向stuct list的指標,也就是指向member型別的指標
  • member就是 list

下面分析一下container_of巨集:

複製程式碼
// 步驟1:將數字0強制轉型為type*,然後取得其中的member元素
((type *)0)->member  // 相當於((struct student *)0)->list

// 步驟2:定義一個臨時變數__mptr,並將其也指向ptr所指向的連結串列節點
const typeof(((type *)0)->member)*__mptr = (ptr);

// 步驟3:計算member欄位距離type中第一個欄位的距離,也就是type地址和member地址之間的差
// offset(type, member)也是一個巨集,定義如下:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

// 步驟4:將__mptr的地址 - type地址和member地址之間的差
// 其實也就是獲取type的地址
複製程式碼

步驟1,2,4比較容易理解,下面的圖以sturct student為例進行說明步驟3:

首先需要知道 ((TYPE *)0) 表示將地址0轉換為 TYPE 型別的地址

由於TYPE的地址是0,所以((TYPE *)0)->MEMBER 也就是 MEMBER的地址和TYPE地址的差,如下圖所示:

step3 

1.3 使用示例

構造了一個核心模組來實際使用一下核心中的連結串列,程式碼在CentOS6.3 x64上執行通過。

C程式碼:

複製程式碼
#include<linux/init.h>
#include<linux/slab.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/list.h>

MODULE_LICENSE("Dual BSD/GPL");
struct student
{
    int id;
    char* name;
    struct list_head list;
};

void print_student(struct student*);

static int testlist_init(void)
{
    struct student *stu1, *stu2, *stu3, *stu4;
    struct student *stu;
    
    // init a list head
    LIST_HEAD(stu_head);

    // init four list nodes
    stu1 = kmalloc(sizeof(*stu1), GFP_KERNEL);
    stu1->id = 1;
    stu1->name = "wyb";
    INIT_LIST_HEAD(&stu1->list);

    stu2 = kmalloc(sizeof(*stu2), GFP_KERNEL);
    stu2->id = 2;
    stu2->name = "wyb2";
    INIT_LIST_HEAD(&stu2->list);

    stu3 = kmalloc(sizeof(*stu3), GFP_KERNEL);
    stu3->id = 3;
    stu3->name = "wyb3";
    INIT_LIST_HEAD(&stu3->list);

    stu4 = kmalloc(sizeof(*stu4), GFP_KERNEL);
    stu4->id = 4;
    stu4->name = "wyb4";
    INIT_LIST_HEAD(&stu4->list);

    // add the four nodes to head
    list_add (&stu1->list, &stu_head);
    list_add (&stu2->list, &stu_head);
    list_add (&stu3->list, &stu_head);
    list_add (&stu4->list, &stu_head);

    // print each student from 4 to 1
    list_for_each_entry(stu, &stu_head, list)
    {
        print_student(stu);
    }
    // print each student from 1 to 4
    list_for_each_entry_reverse(stu, &stu_head, list)
    {
        print_student(stu);
    }

    // delete a entry stu2
    list_del(&stu2->list);
    list_for_each_entry(stu, &stu_head, list)
    {
        print_student(stu);
    }

    // replace stu3 with stu2
    list_replace(&stu3->list, &stu2->list);
    list_for_each_entry(stu, &stu_head, list)
    {
        print_student(stu);
    }

    return 0;
}

static void testlist_exit(void)
{
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "testlist is exited!\n");
    printk(KERN_ALERT "*************************\n");
}

void print_student(struct student *stu)
{
    printk (KERN_ALERT "======================\n");
    printk (KERN_ALERT "id  =%d\n", stu->id);
    printk (KERN_ALERT "name=%s\n", stu->name);
    printk (KERN_ALERT "======================\n");
}

module_init(testlist_init);
module_exit(testlist_exit);
複製程式碼

Makefile:

複製程式碼
obj-m += testlist.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned
複製程式碼

安裝,解除安裝核心模組以及檢視核心模組的執行結果:

insmod testlist.ko
rmmod testlist
dmesg | tail -100

2. 佇列

核心中的佇列是以位元組形式儲存資料的,所以獲取資料的時候,需要知道資料的大小。

如果從佇列中取得資料時指定的大小不對的話,取得資料會不完整或過大。

1.1 標頭檔案簡介

核心中關於佇列定義的標頭檔案位於:<linux/kfifo.h> include/linux/kfifo.h

標頭檔案中定義的函式的實現位於:kernel/kfifo.c

1.2 佇列程式碼的注意點

核心佇列程式設計需要注意的是:

  • 佇列的size在初始化時,始終設定為2的n次方
  • 使用佇列之前將佇列結構體中的鎖(spinlock)釋放

1.3 使用示例

構造了一個核心模組來實際使用一下核心中的佇列,程式碼在CentOS6.3 x64上執行通過。

C程式碼:

複製程式碼
#include "kn_common.h"

MODULE_LICENSE("Dual BSD/GPL");
struct student
{
    int id;
    char* name;
};

static void print_student(struct student*);

static int testkfifo_init(void)
{
    struct kfifo *fifo;
    struct student *stu1, *stu2, *stu3, *stu4;
    struct student *stu_tmp;
    char* c_tmp;
    int i;
    // !!importent  init a unlocked lock
    spinlock_t sl = SPIN_LOCK_UNLOCKED;

    // init kfifo
    fifo = kfifo_alloc(4*sizeof(struct student), GFP_KERNEL, &sl);
    
    stu1 = kmalloc(sizeof(struct student), GFP_KERNEL);
    stu1->id = 1;
    stu1->name = "wyb1";
    kfifo_put(fifo, (char *)stu1, sizeof(struct student));

    stu2 = kmalloc(sizeof(struct student), GFP_KERNEL);
    stu2->id = 1;
    stu2->name = "wyb2";
    kfifo_put(fifo, (char *)stu2, sizeof(struct student));

    stu3 = kmalloc(sizeof(struct student), GFP_KERNEL);
    stu3->id = 1;
    stu3->name = "wyb3";
    kfifo_put(fifo, (char *)stu3, sizeof(struct student));

    stu4 = kmalloc(sizeof(struct student), GFP_KERNEL);
    stu4->id = 1;
    stu4->name = "wyb4";
    kfifo_put(fifo, (char *)stu4, sizeof(struct student));

    c_tmp = kmalloc(sizeof(struct student), GFP_KERNEL);
    printk(KERN_ALERT "current fifo length is : %d\n", kfifo_len(fifo));
    for (i=0; i < 4; i++) {

        kfifo_get(fifo, c_tmp, sizeof(struct student));
        stu_tmp = (struct student *)c_tmp;
        print_student(stu_tmp);
        printk(KERN_ALERT "current fifo length is : %d\n", kfifo_len(fifo));
    }
    
    printk(KERN_ALERT "current fifo length is : %d\n", kfifo_len(fifo));
    kfifo_free(fifo);
    kfree(c_tmp);
    return 0;
}

static void print_student(struct student *stu)
{
    printk(KERN_ALERT "=========================\n");
    print_current_time(1);
    printk(KERN_ALERT "id = %d\n", stu->id);
    printk(KERN_ALERT "name = %s\n", stu->name);
    printk(KERN_ALERT "=========================\n");
}

static void testkfifo_exit(void)
{
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "testkfifo is exited!\n");
    printk(KERN_ALERT "*************************\n");
}

module_init(testkfifo_init);
module_exit(testkfifo_exit);
複製程式碼

其中引用的kn_common.h檔案:

複製程式碼
#include<linux/init.h>
#include<linux/slab.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/kfifo.h>
#include<linux/time.h>

void print_current_time(int);
複製程式碼

kn_common.h對應的kn_common.c:

複製程式碼
#include "kn_common.h"

void print_current_time(int is_new_line)
{
    struct timeval *tv;
    struct tm *t;
    tv = kmalloc(sizeof(struct timeval), GFP_KERNEL);
    t = kmalloc(sizeof(struct tm), GFP_KERNEL);

    do_gettimeofday(tv);
    time_to_tm(tv->tv_sec, 0, t);

    printk(KERN_ALERT "%ld-%d-%d %d:%d:%d",
           t->tm_year + 1900,
           t->tm_mon + 1,
           t->tm_mday,
           (t->tm_hour + 8) % 24,
           t->tm_min,
           t->tm_sec);

    if (is_new_line == 1)
        printk(KERN_ALERT "\n");
    
    kfree(tv);
    kfree(t);
}
複製程式碼

Makefile:

複製程式碼
obj-m += fifo.o
fifo-objs := testkfifo.o kn_common.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned
複製程式碼

安裝,解除安裝核心模組以及檢視核心模組的執行結果:

insmod fifo.ko
rmmod fifo
dmesg | tail -40

3. 對映

對映的有點想其他語言(C#或者python)中的字典型別,每個唯一的id對應一個自定義的資料結構。

1.1 標頭檔案簡介

核心中關於對映定義的標頭檔案位於:<linux/idr.h> include/linux/idr.h

標頭檔案中定義的函式的實現位於:lib/idr.c

1.2 對映程式碼的注意點

對映的使用需要注意的是,給自定義的資料結構申請一個id的時候,不能直接申請id,先要分配id(函式idr_pre_get),分配成功後,在獲取一個id(函式idr_get_new)。

1.3 使用示例

構造了一個核心模組來實際使用一下核心中的對映,程式碼在CentOS6.3 x64上執行通過。

C程式碼:

複製程式碼
#include<linux/idr.h>
#include "kn_common.h"

MODULE_LICENSE("Dual BSD/GPL");
struct student
{
    int id;
    char* name;
};

static int print_student(int, void*, void*);

static int testidr_init(void)
{
    DEFINE_IDR(idp);
    struct student *stu[4];
    //    struct student *stu_tmp;
    int id, ret, i;

    // init 4 struct student
    for (i=0; i<4; i++) {

        stu[i] = kmalloc(sizeof(struct student), GFP_KERNEL);
        stu[i]->id = i;
        stu[i]->name = "wyb";
    }

    // add 4 student to idr
    print_current_time(0);
    for (i=0; i < 4; i++) {

        do {
            if (!idr_pre_get(&idp, GFP_KERNEL))
                return -ENOSPC;
            ret = idr_get_new(&idp, stu[i], &id);
            printk(KERN_ALERT "id=%d\n", id);
        } while(ret == -EAGAIN);
    }

    // display all student in idr
    idr_for_each(&idp, print_student, NULL);

    idr_destroy(&idp);
    kfree(stu[0]);
    kfree(stu[1]);
    kfree(stu[2]);
    kfree(stu[3]);
    return 0;
}

static int print_student(int id, void *p, void *data)
{
    struct student* stu = p;
       
    printk(KERN_ALERT "=========================\n");
    print_current_time(0);
    printk(KERN_ALERT "id = %d\n", stu->id);
    printk(KERN_ALERT "name = %s\n", stu->name);
    printk(KERN_ALERT "=========================\n");

    return 0;
}

static void testidr_exit(void)
{
    printk(KERN_ALERT "*************************\n");
    print_current_time(0);
    printk(KERN_ALERT "testidr is exited!\n");
    printk(KERN_ALERT "*************************\n");
}

module_init(testidr_init);
module_exit(testidr_exit);
複製程式碼

注:其中用到的kn_common.h和kn_common.c檔案與佇列的示例中一樣。

Makefile:

複製程式碼
obj-m += idr.o
idr-objs := testidr.o kn_common.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean:
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned
複製程式碼

安裝,解除安裝核心模組以及檢視核心模組的執行結果:

insmod idr.ko
rmmod idr
dmesg | tail -30

4. 紅黑樹

紅黑樹由於節點顏色的特性,保證其是一種自平衡的二叉搜尋樹。

紅黑樹的一系列規則雖然實現起來比較複雜,但是遵循起來卻比較簡單,而且紅黑樹的插入,刪除效能也還不錯。

所以紅黑樹在核心中的應用非常廣泛,掌握好紅黑樹,即有利於閱讀核心原始碼,也可以在自己的程式碼中借鑑這種資料結構。

紅黑樹必須滿足的規則:

  • 所有節點都有顏色,要麼紅色,要麼黑色
  • 根節點是黑色,所有葉子節點也是黑色
  • 葉子節點中不包含資料
  • 非葉子節點都有2個子節點
  • 如果一個節點是紅色,那麼它的父節點和子節點都是黑色的
  • 從任何一個節點開始,到其下葉子節點的路徑中都包含相同樹木的黑節點

紅黑樹中最長的路徑就是紅黑交替的路徑,最短的路徑是全黑節點的路徑,再加上根節點和葉子節點都是黑色,

從而可以保證紅黑樹中最長路徑的長度不會超過最短路徑的2倍。

1.1 標頭檔案簡介

核心中關於紅黑樹定義的標頭檔案位於:<linux/rbtree.h> include/linux/rbtree.h

標頭檔案中定義的函式的實現位於:lib/rbtree.c

1.2 紅黑樹程式碼的注意點

核心中紅黑樹的使用和連結串列(list)有些類似,是將紅黑樹的節點放入自定義的資料結構中來使用的。

首先需要注意的一點是紅黑樹節點的定義:

複製程式碼
struct rb_node
{
    unsigned long  rb_parent_color;
#define    RB_RED        0
#define    RB_BLACK    1
    struct rb_node *rb_right;
    struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
複製程式碼

剛開始看到這個定義的時候,我覺得很奇怪,等到看懂了之後,才知道原來作者巧妙的利用記憶體對齊來將2個內容存入到一個欄位中(不服不行啊^_^!)。

欄位 rb_parent_color 中儲存了2個資訊:

  1. 父節點的地址
  2. 本節點的顏色

這2個資訊是如何存入一個欄位的呢?主要在於 __attribute__((aligned(sizeof(long))));

這行程式碼的意思就是 struct rb_node 在記憶體中的地址需要按照4 bytes或者8 bytes對齊。

注:sizeof(long) 在32bit系統中是4 bytes,在64bit系統中是8 bytes

struct rb_node的地址按4 bytes對齊,意味著分配的地址都是4的倍數。

4 的二進位制為 100 ,所以申請分配的 struct rb_node 的地址的最後2位始終是零,

struct rb_node 的欄位 rb_parent_color 就是利用最後一位來儲存節點的顏色資訊的。

明白了這點之後,rb_tree.h 中很多巨集的定義也就很好懂了。

複製程式碼
/* rb_parent_color 儲存了父節點的地址和本節點的顏色 */

/* 將 rb_parent_color 的最後2位置成0,即將顏色資訊去掉,剩下的就是parent節點的地址 */
#define rb_parent(r)   ((struct rb_node *)((r)->rb_parent_color & ~3))

/* 取得 rb_parent_color 二進位制表示的最後一位,即用於儲存顏色資訊的那一位 */
#define rb_color(r)   ((r)->rb_parent_color & 1)

/* 將 rb_parent_color 二進位制表示的最後一位置為0,即置為紅色 */
#define rb_set_red(r)  do { (r)->rb_parent_color &= ~1; } while (0)

/* 將 rb_parent_color 二進位制表示的最後一位置為1,即置為黑色 */
#define rb_set_black(r)  do { (r)->rb_parent_color |= 1; } while (0)
複製程式碼

還有需要重點看的就是rb_tree.c中的5個函式,下面對這5個函式進行一些註釋:

函式1:左旋操作,當右子樹的長度過大導致樹不平衡時,進行左旋操作

複製程式碼
/*
 *  左旋操作其實就3個動作:見圖left
 *  1. node的右子樹關聯到right的左子樹
 *  2. right的左子樹關聯到node
 *  3. right取代node的位置
 *  其他帶程式碼都是一些相應的parent指標的變化
 */
static void __rb_rotate_left(struct rb_node *node, struct rb_root *root)
{
    /* 初始化相對於node節點的父節點(圖中的P)和右節點(圖中的R) */
    struct rb_node *right = node->rb_right;
    struct rb_node *parent = rb_parent(node);

    /* 步驟1  */
    if ((node->rb_right = right->rb_left))
        rb_set_parent(rig