深入分析 Linux 内核链表的数据结构

分类: 内核技术   出处:iocblog整理  更新时间:2008-08-02   添加到收藏  


  一、 链表数据结构简介
  链表是一种常用的组织有序数据的数据结构,它通过指针将一系列数据节点连接成一条数据链,是线性表的一种重要实现方式。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。
  
  通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型,下面分别给出这几类常见链表类型的示意图:
  
  1. 单链表
   
  单链表是最简单的一类链表,它的特点是仅有一个指针域指向后继节点(next),因此,对单链表的遍历只能从头至尾(通常是null空指针)顺序进行。
  
  2. 双链表 
  通过设计前驱和后继两个指针域,双链表可以从两个方向遍历,这是它区别于单链表的地方。如果打乱前驱、后继的依赖关系,就可以构成"二叉树";如果再让首节点的前驱指向链表尾节点、尾节点的后继指向首节点(如图2中虚线部分),就构成了循环链表;如果设计更多的指针域,就可以构成各种复杂的树状数据结构。
  
  3. 循环链表
  循环链表的特点是尾节点的后继指向首节点。前面已经给出了双循环链表的示意图,它的特点是从任意一个节点出发,沿两个方向的任何一个,都能找到链表中的任意一个数据。如果去掉前驱指针,就是单循环链表。
  
  在linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在[include/linux/list.h]实现的一个相当精彩的链表数据结构。本文的后继部分就将通过示例详细介绍这一数据结构的组织和使用。
  
  二、 linux 2.6内核链表数据结构的实现
  尽管这里使用2.6内核作为讲解的基础,但实际上2.4内核中的链表结构和2.6并没有什么区别。不同之处在于2.6扩充了两种链表数据结构:链表的读拷贝更新(rcu)和hash链表(hlist)。这两种扩展都是基于最基本的list结构,因此,本文主要介绍基本链表结构,然后再简要介绍一下rcu和hlist。
  
  链表数据结构的定义很简单(节选自[include/linux/list.h],以下所有代码,除非加以说明,其余均取自该文件):
  
  struct list_head {
   struct list_head *next, *prev;
  };
  
  list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。
  
  和第一节介绍的双链表结构模型不同,这里的list_head没有数据域。在linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。
  
  在数据结构课本中,链表的经典定义方式通常是这样的(以单链表为例):
  
  struct list_node {
   struct list_node *next;
   elemtype data;
  };
  
  因为elemtype的缘故,对每一种数据项类型都需要定义各自的链表结构。有经验的c++程序员应该知道,标准模板库中的 采用的是c++ template,利用模板抽象出和数据项类型无关的链表操作接口。
  
  在linux内核链表中,需要用链表组织起来的数据通常会包含一个struct list_head成员,例如在[include/linux/netfilter.h]中定义了一个nf_sockopt_ops结构来描述netfilter为某一协议族准备的getsockopt/setsockopt接口,其中就有一个(struct list_head list)成员,各个协议族的nf_sockopt_ops结构都通过这个list成员组织在一个链表中,表头是定义在[net/core/netfilter.c]中的nf_sockopts(struct list_head)。从下图中我们可以看到,这种通用的链表结构避免了为每个数据项类型定义自己的链表的麻烦。linux的简捷实用、不求完美和标准的风格,在这里体现得相当充分。
 [来源www.iocblog.net]

 


  三、 链表操作接口
  1. 声明和初始化
  实际上linux只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的呢?让我们来看看list_head()这个宏:
  
  #define list_head_init(name) { &(name), &(name) }
  #define list_head(name) struct list_head name = list_head_init(name)
  
  当我们用list_head(nf_sockopts)声明一个名为nf_sockopts的链表头时,它的next、prev指针都初始化为指向自己,这样,我们就有了一个空链表,因为linux用头指针的next是否指向自己来判断链表是否为空:
  
  static inline int list_empty(const struct list_head *head)
  {
   return head->next == head;
  }
  
  除了用list_head()宏在声明的时候初始化一个链表以外,linux还提供了一个init_list_head宏用于运行时初始化链表:
  
  #define init_list_head(ptr) do { (ptr)->next = (ptr); (ptr)->prev = (ptr); } while (0)
  
  我们用init_list_head(&nf_sockopts)来使用它。
  

[1] [2] 下一页