Chinaunix首页 | 论坛 | 博客
  • 博客访问: 465205
  • 博文数量: 191
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 172
  • 用 户 组: 普通用户
  • 注册时间: 2013-10-31 14:35
个人简介

没有时间把一件事情做好,却有时间把一件事情反复做!

文章分类

全部博文(191)

文章存档

2016年(2)

2015年(74)

2014年(111)

2013年(4)

我的朋友

分类: 其他平台

2014-05-05 23:27:03

原文地址:Linux内核链表 作者:rio5409

一、 链表数据结构简介

链表是一种常用的组织有序数据的数据结构,它通过指针将一系列数据节点连接成一条数据链,是线性表的一种重要实现方式。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。

通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型,下面分别给出这几类常见链表类型的示意图:
1. 单链表

图1 单链表 图1 单链表

单链表是最简单的一类链表,它的特点是仅有一个指针域指向后继节点(next),因此,对单链表的遍历只能从头至尾(通常是NULL空指针)顺序进行。2. 双链表

图2 双链表 图2 双链表

通过设计前驱和后继两个指针域,双链表可以从两个方向遍历,这是它区别于单链表的地方。如果打乱前驱、后继的依赖关系,就可以构成"二叉树";如果再让首节点的前驱指向链表尾节点、尾节点的后继指向首节点(如图2中虚线部分),就构成了循环链表;如果设计更多的指针域,就可以构成各种复杂的树状数据结构。
3. 循环链表

循环链表的特点是尾节点的后继指向首节点。前面已经给出了双循环链表的示意图,它的特点是从任意一个节点出发,沿两个方向的任何一个,都能找到链表中的任意一个数据。如果去掉前驱指针,就是单循环链表。

在Linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在[include/linux/list.h]实现的一个相当精彩的链表数据结构。本文的后继部分就将通过示例详细介绍这一数据结构的组织和使用。
二、 Linux 2.6内核链表数据结构的实现

本文主要介绍基本链表结构,然后再简要介绍一下rcu和hlist。

链表数据结构的定义很简单(节选自[include/linux/list.h],以下所有代码,除非加以说明,其余均取自该文件):

struct list_head {
	struct list_head *next, *prev;
};

list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。在Linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。
三、 链表操作接口
Linux内核链表中提供的操作链表函数
(1)初始化
static inline void INIT_LIST_HEAD(struct list_head *list)
{
 list->next = list;     /* 下一个节点指向自己 */
 list->prev = list;     /* 前一个节点指向自己 */
}
(2)添加链表节点
对链表的插入操作有两种:在表头插入和在表尾插入。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);
static inline void list_add(struct list_head *new, struct list_head *head)
{
     __list_add(new, head, head->next);  /* 节点插入到head和head->next之间 */
}


(3)删除节点
方法一:
static inline void list_del(struct list_head *entry)
{
     __list_del(entry->prev, entry->next);
     entry->next = (void *)0xDEADBEEF;      /* 将指针指向2个不可访问的位置 */
     entry->prev = (void *)0xBEEFDEAD;
}
方法二:
为了更安全的删除节点,可使用list_del_init
static inline void list_del_init(struct list_head *entry)
{
     __list_del(entry->prev, entry->next);
     INIT_LIST_HEAD(entry);
}
 
(4)提取结构的数据信息
注意list_entry传递的参数!type指传递的是类型,不是变量。


#define list_entry(ptr, type, member);


(5)、遍历链表
#define list_for_each(pos, head) \
 for (pos = (head)->next; prefetch(pos->next), pos != (head); \
  pos = pos->next)
  
遍历链表也是一个宏定义,且是一个没加{}的for循环,
因此调用此函数时一定要加上{}。
使用时注意参数pos是指针类型,我们要定义一个list_head类型的
结构体变量指针传送给这个函数。


 4、链表使用实例
 
[cpp] view plaincopyprint?
#include  
#include  
#include  
#include  
#include  
  
MODULE_LICENSE("GPL");  
MODULE_AUTHOR("monkeyzx");  
MODULE_DESCRIPTION("Mylist Module");  
MODULE_VERSION("V1.0");  
  
struct student  
{  
    char name[100];  
    int id;  
    struct list_head list;  
};  
  
struct list_head student_list, *pos;  
struct student *pstudent, *tmp_student;  
  
int __init mylist_init()  
{  
    int i = 0;  
  
    INIT_LIST_HEAD(&student_list);    //初始化list_head
  
    pstudent = kmalloc(sizeof(struct student)*5, GFP_KERNEL);  //分配空间  
    memset(pstudent, 0, sizeof(struct student)*5);  
  
    for(i=0; i<5; i++)  
    {  
        sprintf(pstudent[i].name, "Student %d", i+1);  
        pstudent[i].id = i + 1;     //结构体数组pstudent
        list_add(&(pstudent[i].list), &student_list);   //讲该节点中的链表节点加入链表student_list
    }  
  
    list_for_each(pos, &student_list)    //遍历链表
    {  
        tmp_student = list_entry(pos, struct student, list);  //取出元素
        printk("<0>student %d name: %s\n", tmp_student->id, tmp_student->name);  
  
    }  
}  
  
void __exit mylist_exit()    //释放链表
{  
    int i = 0;  
  
    for(i=0; i<5; i++)  
    {  
        list_del(&(pstudent[i].list));  
    }  
  
    kfree(pstudent);  
}  
  
module_init(mylist_init);  
module_exit(mylist_exit);  


################################################################################
makefile文件如下


[cpp] view plaincopyprint?
ifneq ($(KERNELRELEASE),)  
  
obj-m := mylist.o  
  
else  
      
KDIR := /lib/modules/2.6.18-53.el5/build  
all:  
    make -C $(KDIR) M=$(PWD) modules  
clean:  
    rm -f *.ko *.o *.mod.o *.mod.c *.symvers  
  
endif  




附录:


4. 安全性考虑
在并发执行的环境下,链表操作通常都应该考虑同步安全性问题,为了方便,Linux将这一操作留给应用自己处理
。Linux链表自己考虑的安全性主要有两个方面:
a) list_empty()判断
基本的list_empty()仅以头指针的next是否指向自己来判断链表是否为空,Linux链表另行提供了一个
list_empty_careful()宏,它同时判断头指针的next和prev,仅当两者都指向自己时才返回真。
这主要是为了应付另一个cpu正在处理同一个链表而造成next、prev不一致的情况。但代码注释也承认,
这一安全保障能力有限:除非其他cpu的链表操作只有list_del_init(),否则仍然不能保证安全,
也就是说,还是需要加锁保护。
b) 遍历时节点删除
前面介绍了用于链表遍历的几个宏,它们都是通过移动pos指针来达到遍历的目的。但如果遍历的
操作中包含删除pos指针所指向的节点,pos指针的移动就会被中断,因为list_del(pos)将把pos
的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。
当然,调用者完全可以自己缓存next指针使遍历操作能够连贯起来,但为了编程的一致性,Linux
链表仍然提供了两个对应于基本遍历操作的"_safe"接口:list_for_each_safe(pos, n, head)、
list_for_each_entry_safe(pos, n, head, member),它们要求调用者另外提供一个与pos同类
型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。
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)时,

阅读(881) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~