实践Linux的理论
全部博文(61)
分类: 其他平台
2014-04-28 10:06:49
链表是一种常用的组织有序数据的数据结构,它通过指针将一系列数据节点连接成一条数据链,是线性表的一种重要实现方式。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。
通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型,下面分别给出这几类常见链表类型的示意图:
1. 单链表
单链表是最简单的一类链表,它的特点是仅有一个指针域指向后继节点(next),因此,对单链表的遍历只能从头至尾(通常是NULL空指针)顺序进行。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)时,