Chinaunix首页 | 论坛 | 博客
  • 博客访问: 111593
  • 博文数量: 50
  • 博客积分: 2495
  • 博客等级: 大尉
  • 技术积分: 535
  • 用 户 组: 普通用户
  • 注册时间: 2010-09-04 11:44
文章分类
文章存档

2011年(20)

2010年(30)

我的朋友

分类: 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结构的指针prevnext,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。内核链表和经典的定义方式不一样,它的指针指向的是指针类型,而不是链表结点结构类型,这样是为了保证通用性,应为你并不知道要定义一个什么样结构的结点。

 

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的链表头时,它的prevnext指针都初始化为指向自己,这样我们就有了一个空链表, 因为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_sockopt_ops是整个的数据结构(如struct student) 。
  • new_sockopt是nf_sockopt_ops结构的一个成员变量,如struct student student1;list是nf_sockopt_ops的一成员名,它是和链表头类型同类型的。
  • nf_sockopts是链表头,struct list_head类型的。

从这里我们看出,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.listprevnext指针分别被设为LIST_POSITION2LIST_POSITION1两个特殊值,这样设置是为了保证不在链表中的节点项不可访问--LIST_POSITION1LIST_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);

假设当前有两个链表,表头分别是list1list2(都是struct list_head变量),当调用list_splice(&list1,&list2)时,只要list1非空,list1链表的内容将被挂接在list2链表上,位于list2list2.next(原list2表的第一个节点)之间。新list2链表将以原list1表的第一个节点为首节点,而尾节点不变。如图(虚箭头为next指针):

list1被挂接到list2之后,作为原表头指针的list1nextprev仍然指向原来的节点,为了避免引起混乱,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,直至又回到headprefetch()可以不考虑,用于预取以提高遍历速度)。

大多数情况下,遍历链表的时候都需要获得链表节点数据项,也就是说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参数,就可以满足这一要求。

 

阅读(782) | 评论(1) | 转发(0) |
0

上一篇:printk()小结

下一篇:VMwareTools折腾记

给主人留下些什么吧!~~

chinaunix网友2010-10-26 18:55:21

很好的, 收藏了 推荐一个博客,提供很多免费软件编程电子书下载: http://free-ebooks.appspot.com