分类: LINUX
2010-10-25 22:29:07
http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html
导言:
linux内核中采用了大量的链表结构来组织数据。这些链表大多采用了[include/linux/list.h]中实现的一套精彩的链表数据结构。
1.定义:
传统链表数据结构的经典定义(以单链表为例):
struct list_node {
struct list_node *next;
ElemType data;
};
ElemType为数据类型。
内核链表的定义:
struct list_head {
struct list_head *next, *prev;
};
list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。内核链表和经典的定义方式不一样,它的指针指向的是指针类型,而不是链表结点结构类型,这样是为了保证通用性,应为你并不知道要定义一个什么样结构的结点。
2.内核链表的初始化:
实际上Linux只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的呢?让我们来看看LIST_HEAD()这个宏:
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)
用LIST_HEAD(name)声明一个名为name的链表头时,它的prev,next指针都初始化为指向自己,这样我们就有了一个空链表, 因为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)来使用它。
表头指针的前向指针prev指向它自己;后向指针next指向自己时链表为空。
3.插入节点:
对链表的插入操作有两种:在表头插入和在表尾插入。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指向最末一个结点,在表头插head->next之后,在表尾插是插在head->prev之后。
假设有一个新nf_sockopt_ops结构变量new_sockopt需要添加到nf_sockopts链表头,我们应当这样操作:
list_add(&new_sockopt.list, &nf_sockopts);
从这里我们看出,nf_sockopts链表中记录的并不是new_sockopt的地址,而是其中的list元素的地址。如何通过链表访问到new_sockopt呢?下面会有详细介绍。
4.删除结点:
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()将节点置为空链状态。
5.搬移
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的表头。
6.合并
除了针对节点的插入、删除操作,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指针):
当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设置为空链。
7提取数据
list_entry(ptr,type,member)
ptr是指向该数据中list_head成员的指针,也就是存储在链表中的地址值,type是整个数据项(如sturct student{})的类型,member则是数据项类型定义中list_head成员的变量名,它的返回值是指向整个数据项的指针。根据该指针可以提取任意数据。
8.遍历
List_for_each(struct list_head *pos, struct list_head *head)
在[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()总是同时使用。
实例:
struct list_head *entry;
struct list_head head;//链表头
list_for_each(entry,&head)
{
card=list_entry(entry,struct cs_card,list);
if(card->dev_midi==minor)
break;
}
对此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参数,就可以满足这一要求。
chinaunix网友2010-10-26 18:55:21
很好的, 收藏了 推荐一个博客,提供很多免费软件编程电子书下载: http://free-ebooks.appspot.com