一、常用的鏈表和內核鏈表的區別
1.1 常規鏈表結構
通常鏈表數據結構至少應包含兩個域:數據域和指針域,數據域用於存儲數據,指針域用於建立與下一個節點的聯系。按照指針域的組織以及各個節點之間的聯系形式,鏈表又可以分為單鏈表、雙鏈表、循環鏈表等多種類型,下面分別給出這幾類常見鏈表類型的示意圖:
單鏈表:
雙鏈表:
1.2 Linux 2.6內核鏈表數據結構
鏈表數據結構的定義很簡單(節選自[include/linux/list.h],以下所有代碼,除非加以說明,其余均取自該文件):
struct list_head { struct list_head *next, *prev;}; list_head結構包含兩個指向list_head結構的指針prev和next,由此可見,內核的鏈表具備雙鏈表功能,實際上,通常它都組織成雙循環鏈表。list_head從字面上理解,好像是頭結點的意思。但從這里的代碼來看卻是普通結點的結構體。在后面的代碼中將list_head當成普通的結點來處理。
和第一節介紹的雙鏈表結構模型不同,這里的list_head沒有數據域。在Linux內核鏈表中,不是在鏈表結構中包含數據,而是在數據結構中包含鏈表節點。
linux內核鏈表與普通鏈表的示意圖:
在數據結構課本中,鏈表的經典定義方式通常是這樣的(以單鏈表為例):
struct list_node {struct list_node *next;ElemTypedata;};
因為ElemType的緣故,對每一種數據項類型都需要定義各自的鏈表結構。並且對於每種的數據結構還要定義相應的操作函數,比如插入、刪除、排序等(這正是linux內核數據結構所要避免的)。
二、 linux內核鏈表的常用操作函數
linux內核鏈表的好處:
設計思想:盡可能的代碼重用,化大堆的鏈表設計為單個鏈表。
鏈表的構造:如果需要構造某類對象的特定列表,則在其結構中定義一個類型為list_head指針的成員,通過這個成員將這類對象連 接起來,形成所需列表,並通過通用鏈表函數對其進行操作。其優點是只需編寫通用鏈表函數,即可構造和操作不同對象的列表,而無需為每類對象的每種列表編寫專用函數,實現了代碼的重用。如果想對某種類型創建鏈表,就把一個list_head類型的變量嵌入到該類型中,用list_head中的成員和相對應的處理函數來對鏈表進行遍歷。如果想得到相應的結構的指針,使用list_entry可以算出來。
2.1 新建一個鏈表
實際上Linux只定義了鏈表節點,並沒有專門定義鏈表頭,那么一個鏈表結構是如何建立起來的呢?讓我們來看看LIST_HEAD()這個宏:
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)
其中的name是struct list_head結構的變量的地址,而不是包含struct list_head的數據結構的變量的地址
2.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)
b) 刪除
static inline void list_del(struct list_head *entry);
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);
d) 合並
除了針對節點的插入、刪除操作,Linux鏈表還提供了整個鏈表的插入功能:
static inline void list_splice(struct list_head *list, struct list_head *head);
2.3 遍歷
遍歷是鏈表最經常的操作之一,為了方便核心應用遍歷鏈表,Linux鏈表將遍歷操作抽象成幾個宏。在介紹遍歷宏之前,我們先看看如何從鏈表中訪問到我們真正需要的數據項。
a) 由鏈表節點到數據項變量
list_entry宏是用來根據list_head指針查找鏈表所嵌入的結構體的地址,具體實現是依賴宏container_of:
#define list_entry(ptr, type, member) container_of(ptr, type, member)
#define container_of(ptr, type, member) ({ const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );})
#define offsetof(type, member) ((size_t) &((type *)0)-> member)
container_of有三個參數, ptr是成員變量的指針, type是指結構體的類型, member是成員變量的名字。 container_of 的作用就是在已知某一個成員變量的名字、指針和結構體類型的情況下,計算結構體的指針,也就是計算結構體的起始地址。 計算的方法其實很簡單,就是用該成員變量的指針減去它於type結構體起始位置的偏移量。在這個定義中,typeof( ((type *)0)->member ) 就是獲得 member 的類型, 然后定義了一個臨時的常量指針 __mptr, 指向 member 變量。 把 __mptr 轉換成 char * 類型, 因為 offsetof 得到的偏移量是以字節為單位。 兩者相減得到結構體的起始位置, 再強制轉換成 type 類型。
offsetof在這里,TYPE表示一個結構體的類型,MEMBER是結構體中的一個成員變量的名字。offsetof 宏的作用是計算成員變量 MEMBER 相對於結構體起始位置的內存偏移量,以字節(Byte)為單位。
b) 遍歷宏
函數首先定義一個(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()可以不考慮,用於預取以提高遍歷速度)。
大多數情況下,遍歷鏈表的時候都需要獲得鏈表節點數據項,也就是說list_for_each()和list_entry()總是同時使用。對此Linux給出了一個list_for_each_entry()宏,與list_for_each()不同,這里的pos是數據項結構指針類型,而不是(struct list_head *)。
#define list_for_each_entry(pos, head, member)……
某些應用需要反向遍歷鏈表,Linux提供了list_for_each_prev()和list_for_each_entry_reverse()來完成這一操作,使用方法和上面介紹的list_for_each()、list_for_each_entry()完全相同。
三、 內核鏈表應用舉例
雙循環鏈表是linux內核常用的數據結構,這也是linux鏈表的一個非常有特色的地方。而涉及到鏈表的函數有鏈表的定義、鏈表頭的初始化、鏈表的插入、鏈表的遍歷、鏈表的刪除和鏈表的回收。下面通過一個內核模塊來說明鏈表的相關操作。
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/list.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("David Xie");
MODULE_DESCRIPTION("List Module");
MODULE_ALIAS("List module");
//以上為內核模塊的的頭文件和模式固定的部分
struct student
{
char name[100];
int num;
struct list_head list;
};
//以上是定義包含有的struct list_head 結構的數據結構
struct student *pstudent;//定義一個結構數組,用來存放數據,注意這里pstudent是數組指針,數組的大小由后面的kmalloc來分配!
struct student *tmp_student;//遍歷時臨時用來存放指向pstudent[i]的指針
struct list_head student_list;//定義鏈表頭(是一個節點)
struct list_head *pos;//指向頭結點的一個指針,會在list_for_each中說明
int mylist_init(void)
{
int i = 0;
INIT_LIST_HEAD(&student_list);//初始化鏈表頭,注意參數是一個指針,用了&符號
pstudent = kmalloc(sizeof(struct student)*5,GFP_KERNEL);//為結構體數組分配空間,共有5個數組成員
memset(pstudent,0,sizeof(struct student)*5);//初始化結構體數組
for(i=0;i<5;i++)//建立鏈表
{
sprintf(pstudent[i].name,"Student%d",i+1);//初始化並顯示學生姓名
pstudent[i].num = i+1; //初始化學生號碼
list_add( &(pstudent[i].list), &student_list);//將pstudent[i].list節點插入到student_list鏈表中,注意這里是從頭結點處插入的,最后順序為 5、4、3、2、1
}
list_for_each(pos,&student_list)//遍歷鏈表,此函數指明pos是一個指向節點頭的指針,前面已經定義了它的類型。遍歷函數相當於一個for循環,{ }內為循環操作,沒循環一次pos=&student_list+1!
{
tmp_student = list_entry(pos,struct student,list);//list_entry(提取數據結構)指針pos指向結構體struct student中的成員list,返回值為指向list所在的結構體的指針(起始地址)
printk("<0>student %d name: %s\n",tmp_student->num,tmp_student->name);
}//輸出此結構體(結構數組其中的一個成員)的數據信息
return 0;
}
void mylist_exit(void)//刪除節點
{
int i ;
for(i=0;i<5;i++)
{
list_del(&(pstudent[i].list) );
}
kfree(pstudent);//釋放分配的內存
}
module_init(mylist_init);//內核模塊模式固定的部分
module_exit(mylist_exit);//內核模塊模式固定的部分
聲明:本文是參考了網絡上大量的文章寫成的!!!
http://www.cnblogs.com/leon19870907/articles/2180529.html
http://hi.baidu.com/pleasehyj/item/918186b851ee55f063388ecd
http://qing.weibo.com/tj/6468794333000x2f.html
http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html