一、链表数据结构简介
链表是一种常用的组织数据的数据结构,它通过指针将一系列数据节点连接成一条数据链,是线性表的一种重要实现方式。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。
通常链表包括两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。链表一般可以分为单链表、双链表、循环链表等多种类型。
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()函数。