Linux内核链表

 一、链表数据结构简介

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

通常链表包括两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。链表一般可以分为单链表、双链表、循环链表等多种类型。

1、单链表

 

 

单链表是最简单的链表结构,它的结点包括两个域:数据域用来存储结点的数据信息;指针域用来存储数据元素的直接后继地址,即用来建立与下一个结点的联系。一般对单链表的遍历只能从头至尾依次顺序进行。

2、双链表

 

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

3、循环链表

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

在Linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在include/linux/list.h实现的一个相当精彩的链表数据结构。 

二、Linux内核链表分析

内核链表结构体的定义以及操作都定义在linux/list.h文件中

1、链表结构体的定义

内核链表数据结构定义很简单:

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

  

list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。

和一般的双链表结构模型不同,这里的list_head没有数据域。在Linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。因此该结构一般与其他数据类型构成新的数据结构使用。这就使内核链表具有通用性,避免了为每个数据项类型定义自己的链表的麻烦。

通常可以这样使用:

struct my_list
{
	ElemType data;
	struct list_head list;
};

  

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(head)声明一个名为head的链表头时,它的next、prev指针都初始化为指向自己,这样,我们就有了一个空链表,因为Linux用头指针的next是否指向自己来判断链表是否为空:

static inline int list_empty(const struct list_head *head)
{
	return head->next == head;
}

 LIST_HEAD(name)宏的作用就使是定义与初始化。

除了用LIST_HEAD()宏在声明的时候初始化一个链表以外,Linux还提供了一个INIT_LIST_HEAD(struct list_head *list)函数用于运行时初始化链表:

static inline void INIT_LIST_HEAD(struct list_head *list)
{
	list->next = list;
	list->prev = list;
}

例如:定义并初始化一个链表,该链表将连接struct my_list类型结点 LIST_HEAD(head) 创建一个头指针

 

3、插入、删除、合并

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);

因为内核链表是循环表,且表头的next、prev分别指向链表中的第一个和最末一个节点,所以,list_add和list_add_tail的区别并不大,实际上上述两个函数均调用下面的这个函数:

static inline void __list_add(struct list_head *new,
			      struct list_head *prev,
			      struct list_head *next)
{
	next->prev = new;
	new->next = next;
	new->prev = prev;
	prev->next = new;
}

  

该函数的作用是将new结点插入到prev和next之间。链表插入函数list_add和list_add_tail均调用这个函数来实现结点的插入操作,只不过参数不同而已。这也是内核链表实现的巧妙之处。

static inline void list_add(struct list_head *new, struct list_head *head)
{
	__list_add(new, head, head->next);
}


static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
	__list_add(new, head->prev, head);
}

 可见,在表头插入是插入在head之后,而在表尾插入是插入在head->prev之后。

例如:

struct my_list newInfo;

list_add_tail(&newInfo.list, &head);

b)删除

链表结点的删除也很简单

static inline void list_del(struct list_head *entry)
{
	__list_del(entry->prev, entry->next);
	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;
}

static inline void list_del_init(struct list_head *entry)
{
	__list_del(entry->prev, entry->next);
	INIT_LIST_HEAD(entry);
}

static inline void __list_del(struct list_head * prev, struct list_head * next)
{
	next->prev = prev;
	prev->next = next;
}

 当要删除newInfo项时,可以这么做:

list_del(&newInfo.list,&head);

被剔除下来的newInfo.list,prev、next指针分别被设为LIST_POSITION2和LIST_POSITION1两个特殊值,这样设置是为了保证不在链表中的节点项不可访问--对LIST_POSITION1和LIST_POSITION2的访问都将引起页故障。与之相对应,list_del_init()函数将节点从链表中解下来之后,调用LIST_INIT_HEAD()将节点置为空链状态。
 

c)移动元素

Linux提供了将原本属于一个链表的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类

static inline void list_move(struct list_head *list, struct list_head *head)
{
	__list_del(list->prev, list->next);
	list_add(list, head);
}

static inline void list_move_tail(struct list_head *list,
				  struct list_head *head)
{
	__list_del(list->prev, list->next);
	list_add_tail(list, head);
}

例如: list_move(&newInfo.list,&head)会把newInfo从所在链表上删除,再插入表头。

d)替换

static inline void list_replace(struct list_head *old,
				struct list_head *new)
{
	new->next = old->next;
	new->next->prev = new;
	new->prev = old->prev;
	new->prev->next = new;
}

static inline void list_replace_init(struct list_head *old,
					struct list_head *new)
{
	list_replace(old, new);
	INIT_LIST_HEAD(old);
}

 即用新结点替换旧结点,很容易理解。

e)合并

除了针对节点的插入、删除操作,内核链表还提供了整个链表的插入功能

先来看看合并的基本函数:

static inline void __list_splice(const struct list_head *list,
				 struct list_head *prev,
				 struct list_head *next)
{
	struct list_head *first = list->next;
	struct list_head *last = list->prev;

	first->prev = prev;
	prev->next = first;

	last->next = next;
	next->prev = last;
}

这段代码很容易理解,就是将list链表插入到prev和next之间(不包括头结点)。

其他合并函数都是调用这个基本函数,例如:

static inline void list_splice(const struct list_head *list,
				struct list_head *head)
{
	if (!list_empty(list))
		__list_splice(list, head, head->next);
}

  

假设当前有两个链表,表头分别是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()函数。

 

 

猜你喜欢

转载自cm14k.iteye.com/blog/983302