Linux內核中的雙向鏈表struct list_head


 一、雙向鏈表list_head

Linux內核驅動開發會經常用到Linux內核中經典的雙向鏈表list_head,以及它的拓展接口和宏定義:list_add、list_add_tail、list_del、list_entry、list_for_each等。

在內核源碼中,list_head結構體的定義在文件source_code/include/linux/types.h文件中,結構體定義如下:

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

通過這個結構體開始構建鏈表,然后插入、刪除節點,遍歷整個鏈表等,內核已經提供好了現成的調用接口。

 

1、創建鏈表

Linux內核中提供了下面的接口來進行雙向鏈表初始化:

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

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

可以通過LIST_HEAD(my_lsit)來進行一個雙向鏈表的初始化,初始化后,my_list的prev和next指針都將指向自己,如下:

struct list_head my_list = {&my_list, &my_list};

正常的鏈表都是為了遍歷結構體中其它有意義的字段而創建的,但是上面定義的my_list中只有prev和next指針,因此沒有實際意義的字段數據,所以,需要創建一個宿主結構,然后在此結構中嵌套my_list字段,宿主結構中又有其它的字段,例如:

struct my_task_lsit {
  int val;
  struct list_head my_list;
}

然后通過下面的code創建出第一個節點:

struct my_task_list first_task = {
    .val = 1,
    .my_list = LIST_HEAD_INIT(first_task.my_list)
};

這樣子,宿主結構中的my_list的prev和next指針分別指向了自己,如下所示: 

 

 

2、添加節點

在Linux內核中已經提供了添加雙向鏈表節點的函數接口了。

(1)list_add()函數

該函數的接口代碼如下所示:

/**
 * list_add - add a new entry
 * @new: new entry to be added
 * @head: list head to add it after
 *
 * Insert a new entry after the specified head.
 * This is good for implementing stacks.
 */
static inline void list_add(struct list_head *new, struct list_head *head)
{
    __list_add(new, head, head->next);
}

根據注釋可以知道,是在鏈表頭head的后方插入一個新的節點,而且該接口函數能利用實現堆棧,同時list_add函數再調用__list_add函數,函數代碼如下所示:

/*
 * Insert a new entry between two known consecutive entries.
 *
 * This is only for internal list manipulation where we know
 * the prev/next entries already!
 */
static inline void __list_add(struct list_head *new,
                  struct list_head *prev,
                  struct list_head *next)
{
    if (!__list_add_valid(new, prev, next))
        return;

    next->prev = new;
    new->next = next;
    new->prev = prev;
    WRITE_ONCE(prev->next, new);    //prev->next = new;
}

函數實現的功能其實就是在head鏈表頭和鏈表頭后的第一個節點之間插入一個新節點,然后這個新的節點就變成了鏈表頭后的第一個節點。

接下來,開始創建一個鏈表頭head_task,並使用LIST_HEAD(head_task);進行初始化,如下所示:

然后創建實際的第一個節點:

struct my_task_list first_task = {
    .val =1,
    .my_list = LIST_HEAD_INIT(first_task.my_list)
};

創建完成后,通過使用list_add接口將這個first_task節點插入到head_task之后:

list_add(&first_task.my_list, &head_task);

完成后,雙向鏈表的指針指向如下所示:

然后繼續創建第二個節點,同樣把它插入到head_task之后:

struct my_task_list second_task = {
    .val = 2,
    .my_list = LIST_HEAD_INIT(second_task.my_list)
};

然后調用list_add()添加節點:

list_add(&second_task.my_list, &head_task);

插入后指針指向如下所示:

以此類推,每次插入一個新的節點,都是緊靠着head_task節點的,而之前插入的節點以此排序靠后,所以最后的哪個節點是第一個插入head_task的哪個節點,所以結論是:先來的節點靠后,而后來的節點靠前,也就是先進后出,后進先出,類似於棧的結構。

(2)list_add_tail()函數

list_add接口是從鏈表頭head后添加節點,同樣,Linux內核也提供了從鏈表尾處向前添加節點的接口list_add_tail,具體實現代碼如下所示:

/**
 * list_add_tail - add a new entry
 * @new: new entry to be added
 * @head: list head to add it before
 *
 * Insert a new entry before the specified head.
 * This is useful for implementing queues.
 */
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
    __list_add(new, head->prev, head);
}

從注釋可以知道,該接口函數是從一個鏈表頭前面插入一個節點,並且該接口適合於隊列的實現,__list_add()函數展開如下所示:

/*
 * Insert a new entry between two known consecutive entries.
 *
 * This is only for internal list manipulation where we know
 * the prev/next entries already!
 */
static inline void __list_add(struct list_head *new,
                  struct list_head *prev,
                  struct list_head *next)
{
    if (!__list_add_valid(new, prev, next))
        return;

    next->prev = new;
    new->next = next;
    new->prev = prev;
    WRITE_ONCE(prev->next, new);    //prev->next = new;
}

由代碼可以知道,list_add_tail就相當於在鏈表頭的前方依次插入新的節點(也可以理解為在鏈表尾開始插入新的節點,head節點就是尾節點,保持不變)。

接下來,先開始創建一個鏈表頭(鏈表尾),同時調用LIST_HEAD進行初始化:

struct my_task_list head_task = {
    .val = 0,
    .my_list = LIST_HEAD_INIT(&head_task.my_list)
};

結構圖形如下所示:

接下來,創建第一個節點first_task,並且調用list_add_tail函數將節點插入到鏈表頭前面:

struct my_task_list first_task = {
    .val = 1,
    .my_list = LIST_HEAD_INIT(&first_task.my_list)
};

list_add_tail(&first_task.my_list, &head_task);

數據結構圖形如下所示:

然后,繼續創建第二個節點second_task,並且調用list_add_tail函數將節點添加到鏈表里面

struct my_task_list second_task = {
    .val = 2,
    .my_list = LIST_HEAD_INIT(&second_task.my_list)
};

list_add_tail(&second_task.my_list, &head_task);

添加完成后,數據結構圖如下所示:

依次類推,每次插入的新節點都是緊靠着head的表尾,而插入的第一個節點first_task排在了第一位,second_task排在了第二位,也就是:先插入的節點排在前面,后插入的節點排在后面,也就是先進先出,后進后出,類似於隊列的結構。

 

3、刪除節點

Linux內核里面同樣提供了刪除雙向鏈表節點的的接口list_del()函數,代碼如下所示:

static inline void list_del(struct list_head *entry)
{
    __list_del_entry(entry);
    entry->next = LIST_POISON1;
    entry->prev = LIST_POISON2;
}

list_del()函數里面繼續調用了__list_del_entry()接口,如下:

/**
 * list_del - deletes entry from list.
 * @entry: the element to delete from the list.
 * Note: list_empty() on entry does not return true after this, the entry is
 * in an undefined state.
 */
static inline void __list_del_entry(struct list_head *entry)
{
    if (!__list_del_entry_valid(entry))
        return;

    __list_del(entry->prev, entry->next);
}

__list_del()的函數如下所示:

/*
 * Delete a list entry by making the prev/next entries
 * point to each other.
 *
 * This is only for internal list manipulation where we know
 * the prev/next entries already!
 */
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
    next->prev = prev;
    WRITE_ONCE(prev->next, next);
}

利用list_del()就可以刪除掉鏈表中的任意節點,但是需要注意的是,前提條件是這個節點是已知的,也就是在鏈表中是真實存在的,其節點的prev和next指針都不為NULL。

 

4、鏈表的遍歷

Linux內核里面同樣提供了對list_head鏈表進行遍歷的接口,分別有兩種情況,第一種是同過next指針,從前向后進行遍歷,函數接口為list_for_each,如下所示:

/**
 * list_for_each    -    iterate over a list
 * @pos:    the &struct list_head to use as a loop cursor.
 * @head:    the head for your list.
 */
#define list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)

該接口為一個宏定義,有兩個參數。第一個參數為struct list_head的循壞因子,第二個參數為鏈表頭。另外一個接口是通過prev指針進行遍歷的,如下:

/**
 * list_for_each_prev    -    iterate over a list backwards
 * @pos:    the &struct list_head to use as a loop cursor.
 * @head:    the head for your list.
 */
#define list_for_each_prev(pos, head) \
    for (pos = (head)->prev; pos != (head); pos = pos->prev)

傳入參數和第一個接口類似。

 

5、節點替換

Linux內核里面提供了對鏈表的節點進行替換的接口,函數的實現如下所示:

/**
 * list_replace - replace old entry by new one
 * @old : the element to be replaced
 * @new : the new element to insert
 *
 * If @old was empty, it will be overwritten.
 */
static inline void list_replace(struct list_head *old,
                struct list_head *new)
{
    new->next = old->next;
    new->next->prev = new;
    new->prev = old->prev;
    new->prev->next = new;
}

list_replac接口的參數有兩個,分別是舊的節點,也就是需要被替換的節點,第二個參數是新的節點,也就是替換的的節點,其實就是通過改變prev和next指針的指向從而達到替換的效果。

另外,內核里面還提供了其它的接口,例如list_move(節點移位),節點翻轉,節點查找等接口,可以通過源碼進行分析。

 

6、宿主結構

(1)找出宿主結構list_entry(ptr, type, member)

上面的操作都是基於struct list_head這個鏈表進行的,涉及到的結構體也都是如下結構體:

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

但是,應該真正關心的是包含list_head結構體字段的宿主結構體,因為只有定位到了宿主結構體的起始地址,才能夠對宿主結構體中其它有意義的字段進行操作。

struct my_task_list {
    int val;
    struct list_head my_list;
};

如何根據my_list這個字段的地址,尋找到my_task_list的地址呢?

在Linux內核中可以通過一個經典的宏定義:container_of(ptr, type, member),list_entry()宏定義如下:

/**
 * list_entry - get the struct for this entry
 * @ptr:    the &struct list_head pointer.
 * @type:    the type of the struct this is embedded in.
 * @member:    the name of the list_head within the struct.
 */
#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

container_of宏定義如下所示:

/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:    the pointer to the member.
 * @type:    the type of the container struct this is embedded in.
 * @member:    the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({                \
    void *__mptr = (void *)(ptr);                    \
    BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) &&    \
             !__same_type(*(ptr), void),            \
             "pointer type mismatch in container_of()");    \
    ((type *)(__mptr - offsetof(type, member))); })

其宏的注釋為:

根據結構體中的一個成員變量地址導出包含這個成員變量member的struct結構體地址。

參數:

         ptr:成員變量member的地址

         type:包含成員變量member的宿主結構體類型

         member:宿主結構體中member成員變量的名稱

使用舉例為前面的strucr my_task_list:

struct my_task_list {
    int val;
    struct list_head my_list;
};

struct my_task_list first_task = {
    .val = 1,
    .my_list = LIST_HEAD(first_task.my_list)
};

若使用container_of宏去尋找fitst_task的地址,則這樣使用:

ptr = container_of(&first_task.my_list, struct my_task_list, my_list);

因此container_of宏的功能就是根據first_task.my_list字段得到的地址得出first_task結構的真實地址。

offsetof宏定義如下所示:

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

將宏進行展開,並將ptr、type和member里面的值代入,可以得到下面的表達式:

#define container_of(&first_task.my_list, struct my_task_list, my_list) ({          \
  void *__mptr = (void *)(&first_task.my_list);                                      \
  ((struct my_task_list *)(__mptr - ((size_t) &((struct my_task_list *)0)->my_list))); })

因此,container_of這個宏就是包含了兩個語句,第一個語句如下:

void *__mptr = (void *)(&first_task.my_list);

該語句的作用為,提取出first_task_list中my_list的地址,強制轉換為(void *)類型后,並賦值給__mptr指針。

第二個語句如下:

((struct my_task_list *)(__mptr - ((size_t) &((struct my_task_list *)0)->my_list)));

第二個語句的作用為,使用__mptr這個地址減去my_task_list中的偏移(該偏移量是通過將0地址強制轉換為struct my_task_list指針類型,然后取出my_list中的地址,也就是相對於0地址的偏移,也就是my_list字段相對於宿主結構struct my_task_list的偏移),兩者相減之后,得到的就是first_task宿主結構的起始地址,最后將地址強制轉換為(struct my_task_list *)類型並返回。

(2)宿主結構的遍歷

在上面可以知道,通過container_of這個宏,能根據結構體中成員變量的地址找到宿主結構的地址,並且能通過對成員變量提供的鏈表進行遍歷,同時,也可以通過Linux內核接口對宿主結構進行遍歷。

list_for_each_entry()能夠對宿主結構進行遍歷,其定義如下:

/**
 * list_for_each_entry    -    iterate over list of given type
 * @pos:    the type * to use as a loop cursor.
 * @head:    the head for your list.
 * @member:    the name of the list_head within the struct.
 */
#define list_for_each_entry(pos, head, member)                \
    for (pos = list_first_entry(head, typeof(*pos), member);    \
         &pos->member != (head);                    \
         pos = list_next_entry(pos, member))

該宏需要傳入三個參數:

         pos:宿主結構體類型指針

         head:鏈表的表頭

         member:結構體內鏈表成員變量的名稱

宏內還調用了另外兩個宏,分別是list_first_entry和list_next_entry,其定義如下:

/**
 * list_first_entry - get the first element from a list
 * @ptr:    the list head to take the element from.
 * @type:    the type of the struct this is embedded in.
 * @member:    the name of the list_head within the struct.
 *
 * Note, that list is expected to be not empty.
 */
#define list_first_entry(ptr, type, member) \
    list_entry((ptr)->next, type, member)

由注釋可以知道該宏是獲取嵌入鏈表內第一個宿主結構體的地址。

/**
 * list_next_entry - get the next element in list
 * @pos:    the type * to cursor
 * @member:    the name of the list_head within the struct.
 */
#define list_next_entry(pos, member) \
    list_entry((pos)->member.next, typeof(*(pos)), member)

該宏是獲取嵌入鏈表內下一個宿主結構體的地址,通過這兩個宏,便可以實現宿主結構的遍歷。

#define list_for_each_entry(pos, head, member)                \
    for (pos = list_first_entry(head, typeof(*pos), member);    \
         &pos->member != (head);                    \
         pos = list_next_entry(pos, member))

首先,pos定位到第一個宿主結構的地址,然后循壞獲取下一個宿主結構的地址,判斷宿主結構中的member成員變量(宿主結構中struct list_head定義的字段)地址是否為head,是的話,退出循壞,從而實現了宿主結構的遍歷,通過遍歷,能對宿主結構的其它成員變量進行操作,例如:

struct my_task_list *pos_ptr = NULL;
list_for_each_entry(pos_ptr,
&head_task.my_list, my_list) { printk(KERN_INFO “val = %d\n”, pos_ptr->val); }

 

參考:

《LINUX設備驅動程序(第3版)》

《Linux設備驅動開發詳解:基於最新的Linux 4.0內核》

《深入Linux內核架構》

https://blog.csdn.net/wanshilun/article/details/79747710


免責聲明!

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



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