linux鏈表管理-先轉,待編輯

linux鏈表,是一種非常實用,方便的鏈表管理。

 鏈表數據結構的定義很簡單(節選自[include/linux/list.h],以下所有代碼,除非加以說明,其餘均取自該文件):

    struct list_head {

     struct list_head *next, *prev;

    };

list_head結構包含兩個指向list_head結構的指針prev和next,由此可見,內核的鏈表具備雙鏈表功能,實際上,通常它都組織成雙循環鏈表。

和普通雙鏈表結構模型不同,這裏的list_head沒有數據域。

在Linux內核鏈表中,不是在鏈表結構中包含數據,而是在數據結構中包含鏈表節點。


1. 聲明和初始化

實際上Linux只定義了鏈表節點,並沒有專門定義鏈表頭,那麼一個鏈表結構是如何建立起來的呢?讓我們來看看LIST_HEAD()這個宏:

#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)

當我們用LIST_HEAD(nf_sockopts)聲明一個名爲nf_sockopts的鏈表頭時,它的next、prev指針都初始化爲指向自己,這樣,我們就有了一個空鏈表,因爲Linux用頭指針的next是否指向自己來判斷鏈表是否爲空:

static inline int list_empty(const struct list_head *head)
{
		return head->next == head;
}

除了用LIST_HEAD()宏在聲明的時候初始化一個鏈表以外,Linux還提供了一個INIT_LIST_HEAD宏用於運行時初始化鏈表:

#define INIT_LIST_HEAD(ptr) do { \
	(ptr)->next = (ptr); (ptr)->prev = (ptr); \
} while (0)

我們用INIT_LIST_HEAD(&nf_sockopts)來使用它。

2. 插入/刪除/合併

a) 插入

對鏈表的插入操作有兩種:在表頭插入和在表尾插入。Linux爲此提供了兩個接口:

static inline void list_add(struct list_head *new, struct list_head *head);
static inline void list_add_tail(struct list_head *new, struct list_head *head);

因爲Linux鏈表是循環表,且表頭的next、prev分別指向鏈表中的第一個和最末一個節點,所以,list_add和list_add_tail的區別並不大,實際上,Linux分別用

__list_add(new, head, head->next);

__list_add(new, head->prev, head);

來實現兩個接口,可見,在表頭插入是插入在head之後,而在表尾插入是插入在head->prev之後。

假設有一個新nf_sockopt_ops結構變量new_sockopt需要添加到nf_sockopts鏈表頭,我們應當這樣操作:

list_add(&new_sockopt.list, &nf_sockopts);

從這裏我們看出,nf_sockopts鏈表中記錄的並不是new_sockopt的地址,而是其中的list元素的地址。如何通過鏈表訪問到new_sockopt呢?下面會有詳細介紹。

b) 刪除

static inline void list_del(struct list_head *entry);

當我們需要刪除nf_sockopts鏈表中添加的new_sockopt項時,我們這麼操作:

list_del(&new_sockopt.list);

被剔除下來的new_sockopt.list,prev、next指針分別被設爲LIST_POSITION2和LIST_POSITION1兩個特殊值,這樣設置是爲了保證不在鏈表中的節點項不可訪問--對LIST_POSITION1和LIST_POSITION2的訪問都將引起頁故障。與之相對應,list_del_init()函數將節點從鏈表中解下來之後,調用LIST_INIT_HEAD()將節點置爲空鏈狀態。

c) 搬移

Linux提供了將原本屬於一個鏈表的節點移動到另一個鏈表的操作,並根據插入到新鏈表的位置分爲兩類:

static inline void list_move(struct list_head *list, struct list_head *head);
static inline void list_move_tail(struct list_head *list, struct list_head *head);

例如list_move(&new_sockopt.list,&nf_sockopts)會把new_sockopt從它所在的鏈表上刪除,並將其再鏈入nf_sockopts的表頭。

d) 合併

除了針對節點的插入、刪除操作,Linux鏈表還提供了整個鏈表的插入功能:

static inline void list_splice(struct list_head *list, struct list_head *head);

假設當前有兩個鏈表,表頭分別是list1和list2(都是struct list_head變量),當調用list_splice(&list1,&list2)時,只要list1非空,list1鏈表的內容將被掛接在list2鏈表上,位於list2和list2.next(原list2表的第一個節點)之間。新list2鏈表將以原list1表的第一個節點爲首節點,而尾節點不變。如圖(虛箭頭爲next指針):


圖4 鏈表合併list_splice(&list1,&list2)
圖4 鏈表合併list_splice(&list1,&list2)

當list1被掛接到list2之後,作爲原表頭指針的list1的next、prev仍然指向原來的節點,爲了避免引起混亂,Linux提供了一個list_splice_init()函數:

static inline void list_splice_init(struct list_head *list, struct list_head *head);
	

該函數在將list合併到head鏈表的基礎上,調用INIT_LIST_HEAD(list)將list設置爲空鏈。

3. 遍歷

遍歷是鏈表最經常的操作之一,爲了方便核心應用遍歷鏈表,Linux鏈表將遍歷操作抽象成幾個宏。在介紹遍歷宏之前,我們先看看如何從鏈表中訪問到我們真正需要的數據項。

a) 由鏈表節點到數據項變量

我們知道,Linux鏈表中僅保存了數據項結構中list_head成員變量的地址,那麼我們如何通過這個list_head成員訪問到作爲它的所有者的節點數據呢?Linux爲此提供了一個list_entry(ptr,type,member)宏,其中ptr是指向該數據中list_head成員的指針,也就是存儲在鏈表中的地址值,type是數據項的類型,member則是數據項類型定義中list_head成員的變量名,例如,我們要訪問nf_sockopts鏈表中首個nf_sockopt_ops變量,則如此調用:

list_entry(nf_sockopts->next, struct nf_sockopt_ops, list);

這裏"list"正是nf_sockopt_ops結構中定義的用於鏈表操作的節點成員變量名。

list_entry的使用相當簡單,相比之下,它的實現則有一些難懂:

#define list_entry(ptr, type, member) container_of(ptr, type, member)
container_of宏定義在[include/linux/kernel.h]中:
#define container_of(ptr, type, member) ({			\
        const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
        (type *)( (char *)__mptr - offsetof(type,member) );})
offsetof宏定義在[include/linux/stddef.h]中:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

size_t最終定義爲unsigned int(i386)。

這裏使用的是一個利用編譯器技術的小技巧,即先求得結構成員在與結構中的偏移量,然後根據成員變量的地址反過來得出屬主結構變量的地址。

container_of()和offsetof()並不僅用於鏈表操作,這裏最有趣的地方是((type *)0)->member,它將0地址強制"轉換"爲type結構的指針,再訪問到type結構中的member成員。在container_of宏中,它用來給typeof()提供參數(typeof()是gcc的擴展,和sizeof()類似),以獲得member成員的數據類型;在offsetof()中,這個member成員的地址實際上就是type數據結構中member成員相對於結構變量的偏移量。

如果這麼說還不好理解的話,不妨看看下面這張圖:


圖5 offsetof()宏的原理
圖5 offsetof()宏的原理

對於給定一個結構,offsetof(type,member)是一個常量,list_entry()正是利用這個不變的偏移量來求得鏈表數據項的變量地址。

b) 遍歷宏

在[net/core/netfilter.c]的nf_register_sockopt()函數中有這麼一段話:

		……
struct list_head *i;
……
	list_for_each(i, &nf_sockopts) {
		struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i;
		……
	}
	……
	

函數首先定義一個(struct list_head *)指針變量i,然後調用list_for_each(i,&nf_sockopts)進行遍歷。在[include/linux/list.h]中,list_for_each()宏是這麼定義的:

        	#define list_for_each(pos, head) \
	for (pos = (head)->next, prefetch(pos->next); pos != (head); \
        	pos = pos->next, prefetch(pos->next))
        	

它實際上是一個for循環,利用傳入的pos作爲循環變量,從表頭head開始,逐項向後(next方向)移動pos,直至又回到head(prefetch()可以不考慮,用於預取以提高遍歷速度)。

那麼在nf_register_sockopt()中實際上就是遍歷nf_sockopts鏈表。爲什麼能直接將獲得的list_head成員變量地址當成struct nf_sockopt_ops數據項變量的地址呢?我們注意到在struct nf_sockopt_ops結構中,list是其中的第一項成員,因此,它的地址也就是結構變量的地址。更規範的獲得數據變量地址的用法應該是:

struct nf_sockopt_ops *ops = list_entry(i, struct nf_sockopt_ops, list);

大多數情況下,遍歷鏈表的時候都需要獲得鏈表節點數據項,也就是說list_for_each()和list_entry()總是同時使用。對此Linux給出了一個list_for_each_entry()宏:

#define list_for_each_entry(pos, head, member)		……

與list_for_each()不同,這裏的pos是數據項結構指針類型,而不是(struct list_head *)。nf_register_sockopt()函數可以利用這個宏而設計得更簡單:

……
struct nf_sockopt_ops *ops;
list_for_each_entry(ops,&nf_sockopts,list){
	……
}
……

某些應用需要反向遍歷鏈表,Linux提供了list_for_each_prev()和list_for_each_entry_reverse()來完成這一操作,使用方法和上面介紹的list_for_each()、list_for_each_entry()完全相同。

如果遍歷不是從鏈表頭開始,而是從已知的某個節點pos開始,則可以使用list_for_each_entry_continue(pos,head,member)。有時還會出現這種需求,即經過一系列計算後,如果pos有值,則從pos開始遍歷,如果沒有,則從鏈表頭開始,爲此,Linux專門提供了一個list_prepare_entry(pos,head,member)宏,將它的返回值作爲list_for_each_entry_continue()的pos參數,就可以滿足這一要求。



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