Linux中國

Linux 內核里的數據結構——雙向鏈表

首先讓我們看一下在 include/linux/types.h 里的主結構體:

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

你可能注意到這和你以前見過的雙向鏈表的實現方法是不同的。舉個例子來說,在 glib 庫里是這樣實現的:

struct GList {
  gpointer data;
  GList *next;
  GList *prev;
};

通常來說一個鏈表結構會包含一個指向某個項目的指針。但是 Linux 內核中的鏈表實現並沒有這樣做。所以問題來了:鏈表在哪裡保存數據呢?。實際上,內核里實現的鏈表是侵入式鏈表(Intrusive list)。侵入式鏈表並不在節點內保存數據-它的節點僅僅包含指向前後節點的指針,以及指向鏈表節點數據部分的指針——數據就是這樣附加在鏈表上的。這就使得這個數據結構是通用的,使用起來就不需要考慮節點數據的類型了。

比如:

struct nmi_desc {
    spinlock_t lock;
    struct list_head head;
};

讓我們看幾個例子來理解一下在內核里是如何使用 list_head 的。如上所述,在內核里有很多很多不同的地方都用到了鏈表。我們來看一個在雜項字元驅動裡面的使用的例子。在 drivers/char/misc.c 的雜項字元驅動 API 被用來編寫處理小型硬體或虛擬設備的小驅動。這些驅動共享相同的主設備號:

#define MISC_MAJOR              10

但是都有各自不同的次設備號。比如:

ls -l /dev |  grep 10
crw-------   1 root root     10, 235 Mar 21 12:01 autofs
drwxr-xr-x  10 root root         200 Mar 21 12:01 cpu
crw-------   1 root root     10,  62 Mar 21 12:01 cpu_dma_latency
crw-------   1 root root     10, 203 Mar 21 12:01 cuse
drwxr-xr-x   2 root root         100 Mar 21 12:01 dri
crw-rw-rw-   1 root root     10, 229 Mar 21 12:01 fuse
crw-------   1 root root     10, 228 Mar 21 12:01 hpet
crw-------   1 root root     10, 183 Mar 21 12:01 hwrng
crw-rw----+  1 root kvm      10, 232 Mar 21 12:01 kvm
crw-rw----   1 root disk     10, 237 Mar 21 12:01 loop-control
crw-------   1 root root     10, 227 Mar 21 12:01 mcelog
crw-------   1 root root     10,  59 Mar 21 12:01 memory_bandwidth
crw-------   1 root root     10,  61 Mar 21 12:01 network_latency
crw-------   1 root root     10,  60 Mar 21 12:01 network_throughput
crw-r-----   1 root kmem     10, 144 Mar 21 12:01 nvram
brw-rw----   1 root disk      1,  10 Mar 21 12:01 ram10
crw--w----   1 root tty       4,  10 Mar 21 12:01 tty10
crw-rw----   1 root dialout   4,  74 Mar 21 12:01 ttyS10
crw-------   1 root root     10,  63 Mar 21 12:01 vga_arbiter
crw-------   1 root root     10, 137 Mar 21 12:01 vhci

現在讓我們看看它是如何使用鏈表的。首先看一下結構體 miscdevice

struct miscdevice
{
      int minor;
      const char *name;
      const struct file_operations *fops;
      struct list_head list;
      struct device *parent;
      struct device *this_device;
      const char *nodename;
      mode_t mode;
};

可以看到結構體miscdevice的第四個變數list 是所有註冊過的設備的鏈表。在源代碼文件的開始可以看到這個鏈表的定義:

static LIST_HEAD(misc_list);

它實際上是對用list_head 類型定義的變數的擴展。

#define LIST_HEAD(name) 
    struct list_head name = LIST_HEAD_INIT(name)

然後使用宏 LIST_HEAD_INIT 進行初始化,這會使用變數name 的地址來填充prevnext 結構體的兩個變數。

#define LIST_HEAD_INIT(name) { &(name), &(name) }

現在來看看註冊雜項設備的函數misc_register。它在一開始就用函數 INIT_LIST_HEAD 初始化了miscdevice->list

INIT_LIST_HEAD(&misc->list);

作用和宏LIST_HEAD_INIT一樣。

static inline void INIT_LIST_HEAD(struct list_head *list)
{
    list->next = list;
    list->prev = list;
}

接下來,在函數device_create 創建了設備後,我們就用下面的語句將設備添加到設備鏈表:

list_add(&misc->list, &misc_list);

內核文件list.h 提供了向鏈表添加新項的 API 介面。我們來看看它的實現:

static inline void list_add(struct list_head *new, struct list_head *head)
{
    __list_add(new, head, head->next);
}

實際上就是使用3個指定的參數來調用了內部函數__list_add

  • new - 新項。
  • head - 新項將會插在head的後面
  • head->next - 插入前,head 後面的項。

__list_add的實現非常簡單:

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;
}

這裡,我們在prevnext 之間添加了一個新項。所以我們開始時用宏LIST_HEAD_INIT定義的misc 鏈表會包含指向miscdevice->list 的向前指針和向後指針。

這兒還有一個問題:如何得到列表的內容呢?這裡有一個特殊的宏:

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

使用了三個參數:

  • ptr - 指向結構 list_head 的指針;
  • type - 結構體類型;
  • member - 在結構體內類型為list_head 的變數的名字;

比如說:

const struct miscdevice *p = list_entry(v, struct miscdevice, list)

然後我們就可以使用p->minor 或者 p->name來訪問miscdevice。讓我們來看看list_entry 的實現:

#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) );})

首先你可以注意到花括弧內包含兩個表達式。編譯器會執行花括弧內的全部語句,然後返回最後的表達式的值。

舉個例子來說:

#include <stdio.h>

int main() {
    int i = 0;
    printf("i = %dn", ({++i; ++i;}));
    return 0;
}

最終會列印出2

下一點就是typeof,它也很簡單。就如你從名字所理解的,它僅僅返回了給定變數的類型。當我第一次看到宏container_of的實現時,讓我覺得最奇怪的就是表達式((type *)0)中的0。實際上這個指針巧妙的計算了從結構體特定變數的偏移,這裡的0剛好就是位寬里的零偏移。讓我們看一個簡單的例子:

#include <stdio.h>

struct s {
        int field1;
        char field2;
     char field3;
};

int main() {
    printf("%pn", &((struct s*)0)->field3);
    return 0;
}

結果顯示0x5

下一個宏offsetof會計算從結構體起始地址到某個給定結構欄位的偏移。它的實現和上面類似:

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

現在我們來總結一下宏container_of。只需給定結構體中list_head類型 欄位的地址、名字和結構體容器的類型,它就可以返回結構體的起始地址。在宏定義的第一行,聲明了一個指向結構體成員變數ptr的指針__mptr,並且把ptr 的地址賦給它。現在ptr__mptr 指向了同一個地址。從技術上講我們並不需要這一行,但是它可以方便地進行類型檢查。第一行保證了特定的結構體(參數type)包含成員變數member。第二行代碼會用宏offsetof計算成員變數相對於結構體起始地址的偏移,然後從結構體的地址減去這個偏移,最後就得到了結構體。

當然了list_addlist_entry不是<linux/list.h>提供的唯一功能。雙向鏈表的實現還提供了如下API:

  • list_add
  • list_add_tail
  • list_del
  • list_replace
  • list_move
  • list_is_last
  • list_empty
  • list_cut_position
  • list_splice
  • list_for_each
  • list_for_each_entry

等等很多其它API。

via: https://github.com/0xAX/linux-insides/blob/master/DataStructures/dlist.md

譯者:Ezio 校對:Mr小眼兒

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國