2016年12月16日 星期五

Linux鏈結串列struct list_head 研究

 Linux kernel對雙向鏈結串列進行了很好的封裝,它是由一個struct list_head結構與相關操作函數所組成,定義在linux/list.h中。在許多驅動程式中都會看到它的身影,使用起來很方便。只要在自定義的結構中加入struct list_head結構體就可以使用Linux一系列的link-list函數而不用自行撰寫。雖然struct list_head是用在kernel與驅動程式中,但理論上一般應用程式也可以使用,所以即使平常不接觸驅動程式也值得好好研究一番。

使用情境如下:
struct list_head {
 struct list_head *next, *prev;
};
list_head只包含2個指標, 分別用來指向前一個(prev)以及後一個(next)的節點位址
struct My_DATA {
        int Data_A;
        int Data_B;
        int Data_C;
        struct list_head list;
};
list_head運作模式

接著,介紹一下list_head相關的操作函數:

LIST_HEAD(Name):
這個巨集會宣告一個struct list_head的結構變數並作初始化,也就是
pervnext指標指向自己。
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
也可以使用INIT_LIST_HEAD這個函式來初始化頭節點,但是這樣的作法要先宣告一struct list_head變數然後再餵給INIT_LIST_HEAD做初始化

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


list_empty
檢查這個串列否為空。
int list_empty(const struct list_head *head)
{
        return head->next == head;
}
__list_add
__list_add這個函式會將一個新的節點加入一對已知的前(prev)/(next)端節點之間,也就是說原本相連的2個端點中間會插入一個新節點。

static  void __list_add(struct list_head *new_lst,
                              struct list_head *prev,
                              struct list_head *next)
{
        next->prev = new_lst;
        new_lst->next = next;
        new_lst->prev = prev;
        prev->next = new_lst;
}


list_add
__list_add重新包裝,讓一個新的節點加入串列的開頭,新節點會變成HEAD指向的next,新加入的節點等於是向前端插入。
PS: LDD這書中把這樣的操作比喻為Stack(LIFO)
void list_add(struct list_head *new_lst, 
              struct list_head *head)
{
 __list_add(new_lst, head, head->next);
}



list_add_tail
__list_add重新包裝,讓一個新的節點加入串列末端中,新節點會變成HEAD指向的prev,新加入的節點等於是向尾端插入。
PS: LDD這書中把這樣的操作比喻為Queue(FIFO),事實上它還真的能拿來實作Queue

void list_add_tail(struct list_head *new_lst, struct list_head *head)
{
        __list_add(new_lst, head->prev, head);
}

__list_del  &  list_del 
__list_del這個函式會刪除某一個節點entry,這需要先知道entry的前(prev)/(next)端節點。entry原本的前端節點的next指標會指向entry原本的後端節點,而後端節點的prev指標會指向entry的前端節點,這樣一來就沒有其它節點的指標會指向entry,達到刪除的效果。

static  void __list_del(struct list_head * prev, 
                              struct list_head * next  )
{
        next->prev = prev;
        prev->next = next;
}
list_del除了呼叫__list_del之外,還會將entryprev/next指標指向LIST_POISON1LIST_POISON2這兩個特別位址。
void list_del(struct list_head * entry) { __list_del(entry->prev,entry->next); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; }


查了一下網路上的資料,LIST_POISON1LIST_POISON2被定義在linux/poison.h中,從注解上來看,當訪問到這兩個特別位址時會發生page faultLIST_POISON1LIST_POISON2用來標記那些沒有被初始化以及沒有在鍊表中的list項,讓它們不可被訪問。

/*
 * These are non-NULL pointers that will result in page faults
 * under normal circumstances, used to verify that nobody uses
 * non-initialized list entries.
 */
#define LIST_POISON1  ((void *) 0x00100100 + POISON_POINTER_DELTA)
#define LIST_POISON2  ((void *) 0x00200200 + POISON_POINTER_DELTA)

list_for_each(pos, head)
list_for_each是一個巨集,原型如下:

#define list_for_each(pos, head) \
        for (pos = (head)->next; pos != (head); pos = pos->next)
由定義可以知道是在走訪整個鏈結串列,pos為一個指標並指向第一個項目(head->next),以此為起點一路訪問下去,終止條件是當pos指向head時,代表已經走訪完一圈了。

list_entry(ptr,type,member)
list_entry是一個巨集,是container_of這個巨集組成的,而container_of又會用到offsetof這另一個巨集。 原型如下:

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

#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)
offsetof用來得知strucct中某一member的位址相對於該strucct起始位址的距離,而container_of是只要得知某一member的位址,就可以利用它進一步推算出strucct的起始位址。(offsetof和container_of進一步的描述之前已寫在這一篇-->Linux的container_of與offsetof巨集 )。所以,list_for_eachist_entry這兩個巨集搭配使用就可以存取整個鏈結串列。

list.h的相關函數還有很多,上面只介紹到幾個常用到的。接下來來做個範例,以下為完整的Sample code。

由於linux/list.h不能直接由一般應用程式include,所以可以直接將list.h中的struct list head結構與要用到的操作函式直接複製到code中。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

/**************** add list.h函式 ****************
* 加入上面描述的linux/list.h的操作函式。
************************************************/

struct My_DATA {
    int Data_1;
    int Data_2;
    int Data_3;
    struct list_head list;

};
訪問整個串列並印出串列項的address和data內容。
void print_List(struct list_head *head){
    struct list_head *listptr;
    struct My_DATA *entry;
 
    printf("\n*********************************************************************************\n");
    printf("(HEAD addr =  %p, next = %p, prev = %p)\n",head, head->next, head->prev);
    list_for_each(listptr, head) {
        entry = list_entry(listptr, struct My_DATA, list);
        printf("entry->Data_1= %d | list addr = %p | next = %p | prev = %p\n",
                entry->Data_1, 
                &entry->list, 
                entry->list.next,  
                entry->list.prev  );
    }
    printf("*********************************************************************************\n");
}
加入一個新的串列項,可以向串列前端或尾端加入。
struct My_DATA *add_Node_tail(struct list_head *head){
    struct My_DATA *entry;
    entry=(struct My_DATA*)malloc(sizeof(struct My_DATA));
    list_add_tail( &entry->list,head);
    return entry;
}

struct My_DATA *add_Node(struct list_head *head){
    struct My_DATA *entry;
    entry=(struct My_DATA*)malloc(sizeof(struct My_DATA));
    list_add( &entry->list,head);
    return entry;
}
刪除單一個串列項以及銷毀整個串列:
值得注意的是,刪除串列項的時候會使用到list_del函式,在這裡暫時將entry->next = LIST_POISON1和entry->prev = LIST_POISON2這2行註解掉,因為我們不是在寫驅動程式而是在user space使用,可以把entry->{prev/next}設為NULL,代表該指標沒有指向任何東西,或者也可以把它重新初始化,使用INIT_LIST_HEAD(entry)。
void remove_Node(struct My_DATA *entry){
    printf("\nremove: Data_1=%d (list %p, next %p, prev %p)\n",
            entry->Data_1,
            &entry->list,
            entry->list.next,
            entry->list.prev);

    list_del(&entry->list);
    free(entry); 
}

void free_List(struct list_head *head)
{
    struct list_head *listptr;
    struct My_DATA *entry;
 
    list_for_each(listptr, head) {
        entry = list_entry(listptr, struct My_DATA, list);
        printf("\nFree: entry->Data_1 = %d | list addr = %p | next = %p | prev = %p\n",
                        entry->Data_1,
                        &entry->list,
                        entry->list.next,
                        entry->list.prev);
 
        free(entry);
        entry=NULL;
    }
}
主程式如下: 
int main(int argc,char **argv){

    LIST_HEAD(HEAD);

    struct My_DATA *itemPtr_1 = add_Node(&HEAD);
    itemPtr_1->Data_1 = 100 ;
    struct My_DATA *itemPtr_2 = add_Node(&HEAD);
    itemPtr_2->Data_1 = 200 ;
    struct My_DATA *itemPtr_3 = add_Node(&HEAD);
    itemPtr_3->Data_1 = 300 ;
    struct My_DATA *itemPtr_4 = add_Node(&HEAD);
    itemPtr_4->Data_1 = 400 ;

    print_List(&HEAD);

    remove_Node(itemPtr_3);
    print_List(&HEAD);
    free_List(&HEAD);
}
執行結果:

從結果可以看出串列項是向前端加入,在印出串列時從head->next開始印出,當然銷毀串列時也是一樣從head->next開始,這樣的運作模式就如同LDD書中所講的LIFO(後進先出)模式。
參考:
https://lwn.net/Kernel/LDD3/
http://nano-chicken.blogspot.tw/2009/09/linuxlink-list.html
Linux Device Driver Programming  :平田豐