Linux內核學習——鏈表的實現及應用

這學期開了陳莉君老師代的linux內核這門課,週一主要講了內核中的鏈表,我這裏寫篇博客記錄一下學習所得。
今天博客裏的數據結構和函數都來自2.6內核文件linux-2.6.22.6\include\linux\list.h,參考資料主要是陳老師編寫的《Linux操作系統原理與應用》。
這裏主要以書上的一個小例子來分析。
代碼如下

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/list.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("XIYOU");
#define N 10
struct numlist{
        int num;
        struct list_head list;
};

struct numlist numhead;

static int __init doublelist_init(void)
{
        //初始化頭結點
        struct numlist * listnode;
        struct list_head * pos;
        struct numlist *p;
        int i;

        printk("doublelist is starting...\n");
        INIT_LIST_HEAD(&numhead.list);

        for(i=0;i<N;i++)
        {
                listnode = (struct numlist*)kmalloc(sizeof(struct numlist),GFP_KERNEL);
                listnode->num = i+1;
                list_add_tail(&listnode->list,&numhead.list);
                printk("Node %d has added to the doublelist...\n",i+1);
        }
        i=1;
        list_for_each(pos,&numhead.list){
                p = list_entry(pos,struct numlist,list);
                printk("Node %d's data : %d\n",i,p->num);
                i++;
        }
        return 0;
}
static void __exit doublelist_exit(void)
{
        struct list_head *pos,*n;
        struct numlist *p;
        int i;

        i=1;
        list_for_each_safe(pos,n,&numhead.list){
                list_del(pos);
                p = list_entry(pos,struct numlist,list);
                kfree(p);
                printk("Node %d has removed from the doublelist...\n",i++);
        }
        printk("doublelist is exiting..\n");
}
module_init(doublelist_init);
module_exit(doublelist_exit);

這段代碼編寫了一個模塊,這個模塊的加載函數中實現了一個有10個成員的循環雙鏈表,模塊的卸載函數中則將成員一一刪除。

1、鏈表的定義

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

學過鏈表的朋友應該不難理解,這就是一個雙鏈表的結點定義,只不過這個結點沒有數據域,因爲數據域一般是我們根據具體情況在自己寫的結構體中定義的,比如例子中的struct numlist{ int num; struct list_head list; };,這是我們自己定義的結構體結點,內核鏈表結構struct list_head list是它的一個成員。

2、鏈表的聲明和初始化
在list.h中有兩個宏

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

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

第一個宏LIST_HEAD_INIT只是完成初始化工作,第二個宏LIST_HEAD則完成定義和初始化兩項工作,如果你細心的話會發現上面的例子中沒有使用到這兩個宏,而是用了INIT_LIST_HEAD(&numhead.list);來初始化,查看list.h文件後可以看到INIT_LIST_HEAD()是個函數,它的定義如下

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

該函數的功能和 LIST_HEAD_INIT宏是完全相同的,都是對循環雙鏈表進行初始化操作。

3、增加結點
增加結點的操作常使用兩個函數

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

這兩個函數從名字上可以看出,一個是頭插、一個是尾插,但它們的底層實現都是__list_add函數。
__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;
}

熟悉雙鏈表的話,這個插入操作應該不難理解。
具體選擇是頭插法還是尾插法,要看你是想實現“隊列”還是“棧”的結構了,簡單地說“頭插可以實現棧結構,尾插可以實現隊列結構”,我們默認鏈表遍歷時都是從頭結點向後開始遍歷的。
當你使用頭插法時,每次插入的結點都在頭結點之後,那麼當你遍歷鏈表時會先遍歷到最後插入的,等於說是“先入後出、後入先出”,這就是棧的結構。
當你使用尾插法時,每次插入的結點都在頭結點之前,那麼當你遍歷鏈表時會先遍歷到最先插入的,等於說是“先入先出、後入後出”,這就是隊列的結構。

4、刪除結點
刪除節點使用下面的函數

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

該函數中依賴了__list_del這個函數,定義如下

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

可以看到這就是鏈表刪除的基本操作。
list_del函數中定義了兩個變量LIST_POISON1、LIST_POISON2,我在Poison.h文件裏找到了這兩個變量的定義:

#define LIST_POISON1  ((void *) 0x00100100)
#define LIST_POISON2  ((void *) 0x00200200)

可以看到這兩個變量就是固定地址,至於爲什麼要在刪除時將被刪除結點的prev和next指針指向兩個固定地址,我還沒有答案。

5、遍歷鏈表
在list.h中定義了有關遍歷鏈表的宏
書中介紹了兩個,其定義如下

#define list_for_each(pos, head) \
	for (pos = (head)->next; prefetch(pos->next), pos != (head); \
        	pos = pos->next)
#define list_entry(ptr,type,member)\
	((type*)((char*)(ptr) - (unsigned long)(&((type*)0)->member)))
	

《Linux操作系統原理與應用》中對list_entry的定義如上,但在我的2.6.22.6的內核源碼中是如下定義的

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

offset的定義如下

#define offset(string, ptr, member) \
	__asm__("\n@@@" string "%0" : : "i" (_offset(ptr, member)))

_offset的定義如下

#define _offset(type, member) (&(((type *)NULL)->member))

按照我尋找的這條路徑,我沒有看懂(如果有同學看懂了,希望不吝賜教),但書上的這個確實很妙。
#define list_entry(ptr,type,member)
((type*)((char*)(ptr) - (unsigned long)(&((type*)0)->member)))
ptr是指向內核鏈表結點類型的指針,就是一個struct list_head類型的指針;type是你自己定義的結構體結點的類型,在最開始的例子中就是struct numlist這個結構體類型;member是一個struct list_head類型的結構體的“名字”。
這個宏定義的妙處就在“減法”上面,list_entry這個宏的意義是:返回你自己定義的這個鏈表中ptr所在的結點的地址。結合最開始的例子來看,我們現在創建了一個有10個結點的循環雙鏈表,結點的定義是
struct numlist{
int num;
struct list_head list;
};
現在ptr就是某個結點中的struct list_head list中的list的地址,現在我們要求出ptr所在結點的首地址,顯而易見的是我們只要用ptr減去ptr相對於這個結點的偏移地址就ok了,所以任務就變成了求偏移地址。(&((type
)0)->member)這句代碼巧妙的求出了偏移地址,它讓0地址成爲(type)類型,即struct numlist類型,然後對0地址進行->member操作就得到了偏移量,是不是很巧妙?
list_for_each(pos, head) 沒什麼好說的,就是一個從頭結點遍歷的操作。

我這裏附上編譯文件的Makefile,供有興趣的朋友自己進行操作。

#Makefile 2.6
obj-m := doublelist.o
CURRENT_PATH := $(shell pwd)
LINUX_KERNEL := $(shell uname -r)
LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL)

all:
        make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
        make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

查看結果時可以使用dmesg命令在日誌裏查看。

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