前言
在网上发现关于此的文章转载到处都是。我也就不再做重复性的工作了,将我觉得重要的做下笔记就好了,整理下思路。
概况
链表是经典的组织有序数据的数据结构,是线性表的一种重要实现方式(另外一种是数组)。其基本思想是通过指针将一系列数据节点连接成一条数据链,达到有序组织数据的目的。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。
通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表有许多变种,如单链表,双链表,带头(尾)节点的链表,循环链表等。其中在参考文献中讨论相关各个结构的效率问题。
1 .固定空间,并且空间连续
2 .连续存储,支持随机访问
3 .数据局部性好
4 .插入删除节点开销比较大,要移动元素以维持随机访问的特性。
所以,对于经常访问元素时,我们采用数组的方式来实现;如果是经常插入和删除元素时,我们采用链表来实现。
单链表只能单向遍历,而双链表可以在链表的任意节点进行遍历全链表,但是也带来了内存的开销(小数据无所谓,当数据量大到一定程度,开销还是很可观的)。
图示中演示的是带头节点的链表,即浪费一个节点,换取插入与删除的一致性(不必判断链表插入时或删除后是否为空的情况)。
单链表示意图
双链表示意图
Linux下的实现
在Linux 内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在 [include/linux/list.h] 实现的一个相当精彩的链表数据结构。
数据结构书上演示的程序,一般是节点中包含数据,定义如下
typedef int data_t ;
struct node
{
struct node *next;
struct node *priv;
data_t data;//真实数据
}
但是在Linux 的实现中,节点与数据是分离的。 如果一个结构需要用到链表结构,直接包含链表hlist_head或list_head即可。如此在链表中链表头只需用一个struct list_head类型来表示即可,不管链表中节点的数据结构多么庞大,链表头只需要占4个字节或者8个字节。如下是Linux hlist 和list的定义。
struct hlist_head {
struct hlist_node * first;
};
struct hlist_node {
struct hlist_node * next, ** pprev;
};
/* *双链表 */
struct list_head {
struct list_head * next, ** prev;
};
对于定义,我们或许更加习惯分开成为如下格式
struct list_head * next;
struct list_head * prev;
};
注:hlist是单链表的实现 (h 代表 hash ,用在 hash 中 ) , list 是双链表的实现( task 等都有赖于该实现)
请注意hlist 的实现, prev 用到了二级指针,这种差异带来了相关插入删除等操作的不同(在后文分析)。图示给出了多个节点链接的情况(很多图示没有第一个节点的 pprev 域指向 first 的指针,请注意)。
在Linux 内核链表中,需要用链表组织起来的数据通常会包含一个struct list_head成员。
如参考文献1 中就给出了nf_sockopt_ops结构的示意图
struct nf_sockopt_ops的定义为
{
struct list_head list;
u_int8_t pf;
……
int ( * set )( struct sock * sk, int optval, void __user * user, unsigned int len);
……
int ( * compat_get)( struct sock * sk, int optval,
void __user * user, int * len);
……
struct module * owner;
};
在nf_sockopt_ops开始的部分就有一个struct list_head list,注意不是指针(占用8 字节 @32 位机器上)。
对于RCU 的讨论请参考 《RCU 同步机制》
注:该文章下的附件链接错误,真实的链接地址为
http://www-128.ibm.com/developerworks/cn/linux/kernel/l-chain/attachment.txt