充電站 | 內核鏈表的妙用

相信接觸過C語言的同學對鏈表這個數據結構都不陌生,鏈表作爲C語言的一種常用數據結構,它的實現原理大家也已瞭如指掌,但不知大家是否瞭解內核中鏈表是如何實現的。沒有對Linux內核有過深入接觸的同學可能不會了解內核鏈表,也不會知道它的實現是有多麼的巧妙。下面我就給大家一呈其中妙處。

相比於普通的鏈表實現方式,Linux內核的實現可以說是獨樹一幟的。傳統的鏈表要通過數據內部添加一個指向數據的next節點指針,才能串聯在鏈表中。比如,假定一個cat數據結構來描述貓科動物中的一員。

struct cat
{
unsiged long tail_length;/*尾巴長度*/
unsiged long weight;/*重量*/
bool is_fantastic;/*這隻貓奇妙嗎?*/
}

存儲這個結構到鏈表裏的方法通常是在數據結構中嵌入一個鏈表指針,比如:

struct cat
{
unsiged long tail_length;/*尾巴長度*/
unsiged long weight;/*重量*/
bool is_fantastic;/*這隻貓奇妙嗎?*/
struct cat *next;/*指向下一隻貓*/
struct cat *prev; /*指向上一隻貓*/
}

但顯然如果Linux內核若採用這種實現方式,因爲內核中各個設備的數據類型都不相同,將導致內核無法統一的管理各種設備,並且這樣的實現方式也會極大的增加代碼的重複性,這顯然與聚內核的理念相違背。 故而在內核2.1版本官方引入了內核鏈表實現,用一個既簡單又高效的鏈表來統一其它鏈表,其數據結構如下:

/**
 * 雙向循環鏈表的節點結構
 */
struct list_head 
{
struct list_head *next, *prev;
};

可以看到鏈表中每個節點內都沒有數據域,也就是說無論數據結構有多複雜,它在鏈表中只有前後級指針。而當一個數據結構(即是描述設備的設備結構體)想用通用鏈表管理,只需在結構體內加入包含節點的字段即可。

struct cat
{
unsiged long tail_length;/*尾巴長度*/
unsiged long weight;/*重量*/
bool is_fantastic;/*這隻貓奇妙嗎?*/
struct list_head list;/*所以cat結構體形參鏈表*/
}

並且雙向鏈表可以從任意一個節點的前後遍歷整個鏈表,遍歷非常方便。使用循環鏈表使得可以不斷地循環遍歷管理節點,像進程的調度:操作系統會把就緒的進程放在一個管理進程的就緒隊列的通用鏈表中管理起來,循環不斷地,爲他們分配時間片,獲得cpu進行週而復始的進程調度。 現在鏈表已經能用了但顯然不夠方便。因此內核又提供了一組鏈表操作例程,如list_add()方法加入一個新節點到鏈表,而這些方法有個統一的特點:它們只接受list_head結構作爲參數。

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

通過實現這種只有節點的鏈表,便可以統一的管理內核中設備結構體。但是還需要解決的問題便是如何通過鏈表節點得到設備結構體的首地址,而這纔是內核鏈表實現最巧妙的地方。

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

內核鏈表獲得設備結構體的首地址是通過container_of這個宏,我們來具體分析一下這個宏的實現,首先是它的三個參數ptr是成員變量的指針,type是結構體的類型,member是成員變量的名字。container _of宏的作用是通過結構體內某個成員變量的地址和該變量名,以及結構體類型。找到該結構體變量的地址。這裏使用的是一個利用編譯器技術的小技巧,即先求得結構成員在結構中的偏移量,然後根據成員變量的地址反過來得出主結構變量的地址。下面具體分析各個部分: Typeof: 是用於返回一個變量的類型,這是GCC編譯器的一個拓展功能,即typeof是編譯器相關的,不是c語言中的某個標準。 (((type*)0)->member): ((type*)0)將0地址轉換爲type類型的結構體指針,也就是讓編譯器認爲這個結構體是開始於程序段起始位置0,開始於0地址的話,我們得到的成員變量地址就直接相當於成員變量的偏移地址了。 (((type*)0)->member)引用結構體中member成員

const typeof( ((type *)0)->member ) *__mptr = (ptr);

這裏的意思是用typeof()獲取結構體內member成員屬性的類型,然後定義一個該類型的臨時指針變量_mptr,將ptr所指向的member地址賦給_mptr,作用是防止對ptr和ptr指向的內容造成破壞。 其中用到的offsetof(type,member)

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

這個宏的意思就是求出member相對於0地址的偏移量。

(type *)( (char *)__mptr - offsetof(type,member) );})

這句話的意思是,把_mptr轉換城char*類型,因爲offsetof得到的偏移量是以字節爲單位的。兩者相減得到結構體的起始位置,再強轉爲type類型。自此我們便能得到設備結構體的首地址。 使用container_of宏,我們定義一個簡單的函數便可返回包含list_head的父類型結構體。依靠list_entry方法,內核提供了創建,操作以及其他鏈表管理方法,你甚至不需要知道list_head所嵌入的對象數據結構。

可以看到Linux內核鏈表的實現不可謂不巧妙,通過對內存地址的通透理解,來實現通用的鏈表結構,才能更方便統一的管理內核中繁雜的設備。而內核中還有許多其它的巧妙實現思路,多多閱讀並學習大牛的實現思路和巧妙方法,是能夠顯著提升我們日常開發效率的,同時也是對思維的一種提升。

  • End -

技術發展的日新月異,阿木實驗室將緊跟技術的腳步,不斷把機器人行業最新的技術和硬件推薦給大家。看到經過我們培訓的學員在技術上突飛猛進,是我們培訓最大的價值。如果你在機器人行業,就請關注我們的公衆號,我們將持續發佈機器人行業最有價值的信息和技術。 阿木實驗室致力於爲機器人研發提供開源軟硬件工具和課程服務,讓研發更高效!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章