本文只是对linux内核中的链表进行分析。内核版本是linux-2.6.32.63。文件在:linux内核/linux-2.6.32.63/include/linux/list.h。本文对list.h文件进行简要分析,有必要的地方还会以图进行说明。
linux内核中的哈希链表和其他链表不一样。他是头指针用单个的链表,只有next指针,但其他链表节点是有next和prev两个指针的双链表。
由上图可以知道hash链表中存在两种结构体,一种是hash表头,一种是hash节点。
hash表头:struct hlist_head{ struct hlist_node *first;};表头里面只存放一个hlist_node的指针,指向链表。
hash节点:struct hlist_node{struct hlist_node *next; struct hlist_node **pprev;};有两个指针,所以链表是双链表。但和一般的双链表又有点不一样,next自然是指向链表的下一个节点,但pprev则不是指向当前节点的前一个节点,而是指向当前节点的前一个节点的next指针。所以ppre是二级指针。为什么要设计成这样呢?因为为了统一操作,如果设计的和我们平时使用的双链表的话(prev指向的是前一个节点),那头节点和链表节点之间的操作就要重新定义一套(因为头结点结构体和链表节点结构体是不一样的)。所以干脆直接指向前一个节点的next指针,next的类型是hlist_node*,first的类型也是hlist_node*。这样就统一了链表操作函数了。
还有个问题:为什么头结点要设计的和链表节点不一样呢?官方解释是为了节约空间,因为一般来说哈希表中有非常多的表项,即可能有上千个表项,也即是有上千个hlist_head。如果头结点不用pprev则可以节约非常大的空间。我个人认为还有种解释是头结点的pprev(如果有这个指针)用处不大。因为所有的操作都是要通过哈希函数来算出值在哈希表中的位置,然后再有链表中查找。哈希链表本来就是个处理碰撞现象的,说明链表中的关键字通过哈希函数后能得到一样的值。所以你不知道在链表中的哪个位置,那头结点有pprev的话你也没必要从后面开始查找(也许从前面查找开些,也许是从后面)。也就是说对于头节点来说这个指针可有可无。
代码分析:
/*
* Double linked lists with a single pointer list head.
* Mostly useful for hash tables where the two pointer list head is
* too wasteful.
* You lose the ability to access the tail in O(1).
*/
// 哈希节点,这是个表头,注意只有一个first指针。这是为了有多个哈希链表时,可以减少空间上的浪费
struct hlist_head {
struct hlist_node *first;
};
// 这个哈希链表和其他链表不一样,单指针表头双循环链表。所以头节点和其他节点不一样。
// 下面是链表节点有两个指针,next是指向下一个节点,pprev则是指向前一个节点的next,表头则是first。
// 所以pprev是二级指针
struct hli