詳細的分析2.6.x內(nèi)核中鏈表的實現(xiàn),并通過實例對每個鏈表操作接口進行了分析。
1,鏈表數(shù)據(jù)結(jié)構(gòu)分為,單鏈表,雙鏈表,循環(huán)鏈表。這些在數(shù)據(jù)結(jié)構(gòu)中都有了詳細的描述。此處省略數(shù)百字。
2,Linux內(nèi)核鏈表數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)
盡管這里使用2.6內(nèi)核講解,但是于2.4內(nèi)核中的鏈表結(jié)構(gòu)相同。不同之處在于2.6擴充了兩種鏈表數(shù)據(jù)結(jié)構(gòu):鏈表的讀拷貝更新(rch)和hash鏈表(hlist)。
鏈表數(shù)據(jù)結(jié)構(gòu)的定義很簡單(include/linux/list.h)
struct list_head{\struct list_head *next, *prev;};
由上可見內(nèi)核鏈表具有雙鏈表功能,事實上他都組織成雙循環(huán)鏈表。
在linux內(nèi)核鏈表中,需要用鏈表組織起來的數(shù)據(jù)通常都包括一個list_head成員。
3,鏈表操作接口
a,聲明和初始化
#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)
b,插入/刪除/合并
對鏈表的插入操作有兩種:在表頭插入和在表尾插入。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);
假設(shè)有一個新nf_sockopt_ops結(jié)構(gòu)變量new_sockopt需要添加到nf_sockopts鏈表頭,我們應(yīng)當這樣操作:
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指針分別被設(shè)為LIST_POSITION2和LIST_POSITION1兩個特殊值,這樣設(shè)置是為了保證不在鏈表中的節(jié)點項不可訪問--對LIST_POSITION1和LIST_POSITION2的訪問都將引起頁故障。與之相對應(yīng),list_del_init()函數(shù)將節(jié)點從鏈表中解下來之后,調(diào)用LIST_INIT_HEAD()將節(jié)點置為空鏈狀態(tài)。
c,搬移
Linux提供了將原本屬于一個鏈表的節(jié)點移動到另一個鏈表的操作,并根據(jù)插入到新鏈表的位置分為兩類:
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,合并
除了針對節(jié)點的插入、刪除操作,Linux鏈表還提供了整個鏈表的插入功能:
static inline void list_splice(struct list_head *list, struct list_head *head);
|
假設(shè)當前有兩個鏈表,表頭分別是list1和list2(都是struct list_head變量),當調(diào)用list_splice(&list1,&list2)時,只要list1非空,list1鏈表的內(nèi)容將被掛接在list2鏈表上,位于list2和list2.next(原list2表的第一個節(jié)點)之間。新list2鏈表將以原list1表的第一個節(jié)點為首節(jié)點,而尾節(jié)點不變。如圖(虛箭頭為next指針):
圖4 鏈表合并list_splice(&list1,&list2)
當list1被掛接到list2之后,作為原表頭指針的list1的next、prev仍然指向原來的節(jié)點,為了避免引起混亂,Linux提供了一個list_splice_init()函數(shù):
static inline void list_splice_init(struct list_head *list, struct list_head *head);
|
該函數(shù)在將list合并到head鏈表的基礎(chǔ)上,調(diào)用INIT_LIST_HEAD(list)將list設(shè)置為空鏈。
3. 遍歷
遍歷是鏈表最經(jīng)常的操作之一,為了方便核心應(yīng)用遍歷鏈表,Linux鏈表將遍歷操作抽象成幾個宏。在介紹遍歷宏之前,我們先看看如何從鏈表中訪問到我們真正需要的數(shù)據(jù)項。
a) 由鏈表節(jié)點到數(shù)據(jù)項變量
我們知道,Linux鏈表中僅保存了數(shù)據(jù)項結(jié)構(gòu)中l(wèi)ist_head成員變量的地址,那么我們?nèi)绾瓮ㄟ^這個list_head成員訪問到作為它的所有者的節(jié)點數(shù)據(jù)呢?Linux為此提供了一個list_entry(ptr,type,member)宏,其中ptr是指向該數(shù)據(jù)中l(wèi)ist_head成員的指針,也就是存儲在鏈表中的地址值,type是數(shù)據(jù)項的類型,member則是數(shù)據(jù)項類型定義中l(wèi)ist_head成員的變量名,例如,我們要訪問nf_sockopts鏈表中首個nf_sockopt_ops變量,則如此調(diào)用:
list_entry(nf_sockopts->next, struct nf_sockopt_ops, list);
|
這里"list"正是nf_sockopt_ops結(jié)構(gòu)中定義的用于鏈表操作的節(jié)點成員變量名。
list_entry的使用相當簡單,相比之下,它的實現(xiàn)則有一些難懂:
#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)。
這里使用的是一個利用編譯器技術(shù)的小技巧,即先求得結(jié)構(gòu)成員在與結(jié)構(gòu)中的偏移量,然后根據(jù)成員變量的地址反過來得出屬主結(jié)構(gòu)變量的地址。
container_of()和offsetof()并不僅用于鏈表操作,這里最有趣的地方是((type *)0)->member,它將0地址強制"轉(zhuǎn)換"為type結(jié)構(gòu)的指針,再訪問到type結(jié)構(gòu)中的member成員。在container_of宏中,它用來給typeof()提供參數(shù)(typeof()是gcc的擴展,和sizeof()類似),以獲得member成員的數(shù)據(jù)類型;在offsetof()中,這個member成員的地址實際上就是type數(shù)據(jù)結(jié)構(gòu)中member成員相對于結(jié)構(gòu)變量的偏移量。
如果這么說還不好理解的話,不妨看看下面這張圖:
圖5 offsetof()宏的原理
對于給定一個結(jié)構(gòu),offsetof(type,member)是一個常量,list_entry()正是利用這個不變的偏移量來求得鏈表數(shù)據(jù)項的變量地址。
b) 遍歷宏
在[net/core/netfilter.c]的nf_register_sockopt()函數(shù)中有這么一段話:
…… struct list_head *i; …… list_for_each(i, &nf_sockopts) { struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i; …… } ……
|
函數(shù)首先定義一個(struct list_head *)指針變量i,然后調(diào)用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循環(huán),利用傳入的pos作為循環(huán)變量,從表頭head開始,逐項向后(next方向)移動pos,直至又回到head(prefetch()可以不考慮,用于預取以提高遍歷速度)。
那么在nf_register_sockopt()中實際上就是遍歷nf_sockopts鏈表。為什么能直接將獲得的list_head成員變量地址當成struct nf_sockopt_ops數(shù)據(jù)項變量的地址呢?我們注意到在struct nf_sockopt_ops結(jié)構(gòu)中,list是其中的第一項成員,因此,它的地址也就是結(jié)構(gòu)變量的地址。更規(guī)范的獲得數(shù)據(jù)變量地址的用法應(yīng)該是:
struct nf_sockopt_ops *ops = list_entry(i, struct nf_sockopt_ops, list);
|
大多數(shù)情況下,遍歷鏈表的時候都需要獲得鏈表節(jié)點數(shù)據(jù)項,也就是說list_for_each()和list_entry()總是同時使用。對此Linux給出了一個list_for_each_entry()宏:
#define list_for_each_entry(pos, head, member) ……
|
與list_for_each()不同,這里的pos是數(shù)據(jù)項結(jié)構(gòu)指針類型,而不是(struct list_head *)。nf_register_sockopt()函數(shù)可以利用這個宏而設(shè)計得更簡單:
…… struct nf_sockopt_ops *ops; list_for_each_entry(ops,&nf_sockopts,list){ …… } ……
|
某些應(yīng)用需要反向遍歷鏈表,Linux提供了list_for_each_prev()和list_for_each_entry_reverse()來完成這一操作,使用方法和上面介紹的list_for_each()、list_for_each_entry()完全相同。
如果遍歷不是從鏈表頭開始,而是從已知的某個節(jié)點pos開始,則可以使用list_for_each_entry_continue(pos,head,member)。有時還會出現(xiàn)這種需求,即經(jīng)過一系列計算后,如果pos有值,則從pos開始遍歷,如果沒有,則從鏈表頭開始,為此,Linux專門提供了一個list_prepare_entry(pos,head,member)宏,將它的返回值作為list_for_each_entry_continue()的pos參數(shù),就可以滿足這一要求。
4. 安全性考慮
在并發(fā)執(zhí)行的環(huán)境下,鏈表操作通常都應(yīng)該考慮同步安全性問題,為了方便,Linux將這一操作留給應(yīng)用自己處理。Linux鏈表自己考慮的安全性主要有兩個方面:
a) list_empty()判斷
基本的list_empty()僅以頭指針的next是否指向自己來判斷鏈表是否為空,Linux鏈表另行提供了一個list_empty_careful()宏,它同時判斷頭指針的next和prev,僅當兩者都指向自己時才返回真。這主要是為了應(yīng)付另一個cpu正在處理同一個鏈表而造成next、prev不一致的情況。但代碼注釋也承認,這一安全保障能力有限:除非其他cpu的鏈表操作只有l(wèi)ist_del_init(),否則仍然不能保證安全,也就是說,還是需要加鎖保護。
b) 遍歷時節(jié)點刪除
前面介紹了用于鏈表遍歷的幾個宏,它們都是通過移動pos指針來達到遍歷的目的。但如果遍歷的操作中包含刪除pos指針所指向的節(jié)點,pos指針的移動就會被中斷,因為list_del(pos)將把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。
當然,調(diào)用者完全可以自己緩存next指針使遍歷操作能夠連貫起來,但為了編程的一致性,Linux鏈表仍然提供了兩個對應(yīng)于基本遍歷操作的"_safe"接口:list_for_each_safe(pos, n, head)、list_for_each_entry_safe(pos, n, head, member),它們要求調(diào)用者另外提供一個與pos同類型的指針n,在for循環(huán)中暫存pos下一個節(jié)點的地址,避免因pos節(jié)點被釋放而造成的斷鏈。
四、 擴展
1. hlist
圖6 list和hlist
精益求精的Linux鏈表設(shè)計者(因為list.h沒有署名,所以很可能就是Linus Torvalds)認為雙頭(next、prev)的雙鏈表對于HASH表來說"過于浪費",因而另行設(shè)計了一套用于HASH表應(yīng)用的hlist數(shù)據(jù)結(jié)構(gòu)--單指針表頭雙循環(huán)鏈表,從上圖可以看出,hlist的表頭僅有一個指向首節(jié)點的指針,而沒有指向尾節(jié)點的指針,這樣在可能是海量的HASH表中存儲的表頭就能減少一半的空間消耗。
因為表頭和節(jié)點的數(shù)據(jù)結(jié)構(gòu)不同,插入操作如果發(fā)生在表頭和首節(jié)點之間,以往的方法就行不通了:表頭的first指針必須修改指向新插入的節(jié)點,卻不能使用類似list_add()這樣統(tǒng)一的描述。為此,hlist節(jié)點的prev不再是指向前一個節(jié)點的指針,而是指向前一個節(jié)點(可能是表頭)中的next(對于表頭則是first)指針(struct list_head **pprev),從而在表頭插入的操作可以通過一致的"*(node->pprev)"訪問和修改前驅(qū)節(jié)點的next(或first)指針。
2. read-copy update
在Linux鏈表功能接口中還有一系列以"_rcu"結(jié)尾的宏,與以上介紹的很多函數(shù)一一對應(yīng)。RCU(Read-Copy Update)是2.5/2.6內(nèi)核中引入的新技術(shù),它通過延遲寫操作來提高同步性能。
我們知道,系統(tǒng)中數(shù)據(jù)讀取操作遠多于寫操作,而rwlock機制在smp環(huán)境下隨著處理機增多性能會迅速下降(見參考資料4)。針對這一應(yīng)用背景,IBM Linux技術(shù)中心的Paul E. McKenney提出了"讀拷貝更新"的技術(shù),并將其應(yīng)用于Linux內(nèi)核中。RCU技術(shù)的核心是寫操作分為寫-更新兩步,允許讀操作在任何時候無阻訪問,當系統(tǒng)有寫操作時,更新動作一直延遲到對該數(shù)據(jù)的所有讀操作完成為止。Linux鏈表中的RCU功能只是Linux RCU的很小一部分,對于RCU的實現(xiàn)分析已超出了本文所及,有興趣的讀者可以自行參閱本文的參考資料;而對RCU鏈表的使用和基本鏈表的使用方法基本相同。
五、 示例
附件中的程序除了能正向、反向輸出文件以外,并無實際作用,僅用于演示Linux鏈表的使用。
為了簡便,例子采用的是用戶態(tài)程序模板,如果需要運行,可采用如下命令編譯:
gcc -D__KERNEL__ -I/usr/src/linux-2.6.7/include pfile.c -o pfile
|
因為內(nèi)核鏈表限制在內(nèi)核態(tài)使用,但實際上對于數(shù)據(jù)結(jié)構(gòu)本身而言并非只能在核態(tài)運行,因此,在筆者的編譯中使用"-D__KERNEL__"開關(guān)"欺騙"編譯器。
參考資料
- 維基百科 http://zh.,一個在GNU Documentation License下發(fā)布的網(wǎng)絡(luò)辭典,自由軟件理念的延伸,本文的"鏈表"概念即使用它的版本。
- 《Linux內(nèi)核情景分析》,毛德操先生的這本關(guān)于Linux內(nèi)核的巨著幾乎可以回答絕大部分關(guān)于內(nèi)核的問題,其中也包括內(nèi)核鏈表的幾個關(guān)鍵數(shù)據(jù)結(jié)構(gòu)。
- Linux內(nèi)核2.6.7源代碼,所有不明白的問題,只要潛心看代碼,總能清楚。
- Kernel Korner: Using RCU in the Linux 2.5 Kernel,RCU主要開發(fā)者Paul McKenney 2003年10月發(fā)表于Linux Journal上的一篇介紹RCU的文章。在 http://www./users/paulmck/rclock/上可以獲得更多關(guān)于RCU的幫助。
附件
/*********************************************************************
*
* Filename: pfile.c
* Version: 1.0
* Description: Demo for Linux LIST utility
* Compilation: gcc –D__KERNEL__ -I/usr/src/linux/include pfile.c
* Status: Stable
* Author: Yang Shazhou<pubb@163.net>
* Created at: Thu Jul 15 13:50:33 2004
* Modified at: Thu Jul 15 14:39:03 2004
* Modified by: Yang Shazhou<pubb@163.net>
*
* Copyright (c) 2004 Yang Shazhou, All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307 USA
*
********************************************************************/
#include <linux/list.h>
#include <stdio.h>
#include <strings.h>
int main(int argc,char *argv[])
{
LIST_HEAD(list); //定義存放文件內(nèi)容的鏈表
FILE *fp;
struct file_store {
char c;
struct list_head node;
} *pstore;
if(argc<2){
printf("usage: pfile <filename> <[r]>\n");
return -1;
}
if(!(fp=fopen(argv[1],"rb"))){
printf("fopen(%s) error\n",argv[1]);
return -1;
}
/* 讀文件到鏈表 */
while(1){
if(!(pstore=(struct file_store *)malloc(sizeof(struct file_store))))
break;
pstore->c=fgetc(fp);
if(feof(fp)){
free(pstore);
break;
}
list_add_tail(&pstore->node,&list); //將本字符插入鏈表中
}
fclose(fp);
/* 遍歷鏈表,輸出鏈表中的節(jié)點個數(shù),即文件字符數(shù) */
int count=0;
struct list_head *p;
list_for_each(p,&list){
count++;
}
printf("%s has altogether %d character(s)\n",argv[1],count);
/* 根據(jù)命令行參數(shù)正向/反向遍歷鏈表,輸出鏈表中存放的字符,同時釋放各節(jié)點 */
if(argc>2 && !strcasecmp(argv[2],"r")){
struct list_head *p;
list_for_each_entry_reverse(pstore,&list,node){ //反向遍歷,沒有保護
p=pstore->node.next;
list_del(&pstore->node);
putchar(pstore->c);
free(pstore);
/* 如果沒有這一句,將報segmentation fault */
pstore=list_entry(p,struct file_store,node); //取數(shù)據(jù)項
}
}else{
struct file_store *p;
list_for_each_entry_safe(pstore,p,&list,node){ //正向遍歷,有保護
list_del(&pstore->node);
putchar(pstore->c);
free(pstore);
}
}
return 0;
}