這學期開了陳莉君老師代的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命令在日誌裏查看。