Chinaunix首页 | 论坛 | 博客
  • 博客访问: 35641
  • 博文数量: 18
  • 博客积分: 815
  • 博客等级: 准尉
  • 技术积分: 135
  • 用 户 组: 普通用户
  • 注册时间: 2009-03-29 15:30
文章分类

全部博文(18)

文章存档

2011年(1)

2010年(1)

2009年(16)

我的朋友
最近访客

分类: LINUX

2009-11-11 17:30:23

在2.4版本中,引入了一种特殊的链表-通用双向链表,它是内核中实现其它链表的基础,也是面向对象的思想在C语言中的应用。在等待队列的实现中多次涉及与此链表相关的内容。

1.通用双向链表

   在include/linux/list.h中定义了这种链表:

struct list_head {

        struct list_head *next, *prev;

};

这是双向链表的一个基本框架,在其它使用链表的地方就可以使用它来定义任意一个双向链表,例如:

struct foo_list {

    int data;

    struct list_head list;

};

对于list_head类型的链表,Linux定义了五个宏:

#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \

        struct list_head name = LIST_HEAD_INIT(name)

#define INIT_LIST_HEAD(ptr) do { \

        (ptr)->next = (ptr); (ptr)->prev = (ptr); \

} while (0)

#define list_entry(ptr, type, member) \

        ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

#define list_for_each(pos, head) \

        for (pos = (head)->next; pos != (head); pos = pos->next)

前三个宏都是初始化一个空的链表,但用法不同,LIST_HEAD_INIT()在声明时使用,用来初始化结构元素,第二个宏用在静态变量初始化的声明中,而第三个宏用在函数内部。

其中,最难理解的宏为list_entry(),在内核代码的很多处都用到这个宏,例如,在调度程序中,从运行队列中选择一个最值得运行的进程,部分代码如下:

static LIST_HEAD(runqueue_head);

struct list_head *tmp;

struct task_struct *p;

list_for_each(tmp, &runqueue_head) {

    p = list_entry(tmp, struct task_struct, run_list);

    if (can_schedule(p)) {

        int weight = goodness(p, this_cpu, prev->active_mm);

        if (weight > c)

            c = weight, next = p;

    }

}

从这段代码可以分析出list_entry(ptr, type, member)宏及参数的含义:   ptr是指向list_head类型链表的指针,type为一个结构,而member为结构type中的一个域,类型为list_head,这个宏返回指向type结构的指针。在内核代码中大量引用了这个宏,因此,搞清楚这个宏的含义和用法非常重要。
<<<<<======================

list_entry()宏  list_entry(实际list_head对象指针,包含list_head的结构体类型 ,该结构体中list_head成员的名字)

#define list_entry(ptr, type, member) \
        ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
ptr是指向list_head类型链表的指针,type为一个结构,而member为结构type中的一个域,类型为list_head,这个宏返回指向type结构的指针。在内核代码中大量引用了这个宏,因此,搞清楚这个宏的含义和用法非常重要。

在我的《深入分析Linux内核源代码》中有以上叙述,学生们在阅读的过程中,对其进行了进一步的解析,过程如下:
   
设有如下结构体定义:
typedef struct xxx
{
……(结构体中其他域,令其总大小为size1)
type1 member;
……(结构体中其他域)
}type;


定义变量:
type a;
type * b;
type1 * ptr;
执行:
ptr=&(a.member);
b=list_entry(ptr,type,member);
则可使b指向a,得到了a的地址。


如何做到的呢?


先看&((type *)0)->member:
把“0”强制转化为指针类型,则该指针一定指向“0”(数据段基址)。因为指针是“type *”型的,所以可取到以“0”为基地址的一个type型变量member域的地址。那么这个地址也就等于member域到结构体基地址的偏移字节数。

再来看 ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))):

(char *)(ptr)使得指针的加减操作步长为一字节,(unsigned long)(&((type *)0)->member)等于ptr指向的member到该member所在结构体基地址的偏移字节数。二者一减便得出该结构体的地址。转换为 (type *)型的指针,大功告成。

======================>>>>>
另外,对list_head类型的链表进行删除和插入(头或尾)的宏为list_del()/list_add()/list_add_tail(),在内核的其它函数中可以调用这些宏。例如,从运行队列中删除、增加及移动一个任务的代码如下:

static inline void del_from_runqueue(struct task_struct * p)

{

        nr_running--;

        list_del(&p->run_list);

        p->run_list.next = NULL;

}

static inline void add_to_runqueue(struct task_struct * p)

{

        list_add(&p->run_list, &runqueue_head);

        nr_running++;

}

static inline void move_last_runqueue(struct task_struct * p)

{

        list_del(&p->run_list);

        list_add_tail(&p->run_list, &runqueue_head);

}

=================================
二、 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 的简捷实用、不求完美和标准的风格,在这里体现得相当充分。

 三、 链表操作接口

  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) 来使用它。

  2. 插入/删除/合并

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

  因为 Linux 链表是循环表,且表头的 next、prev 分别指向链表中的第一个和最末一个节点,所以,list_add 和 list_add_tail 的区别并不大,实际上,Linux 分别用

  __list_add(new, head, head->next
 __list_add(new, head->prev, head);

  来实现两个接口,可见,在表头插入是插入在 head 之后,而在表尾插入是插入在 head->prev 之后。

  假设有一个新 nf_sockopt_ops 结构变量 new_sockopt 需要添加到 nf_sockopts 链表头,我们应当这样操作:

 list_add(&new_sockopt.list, &nf_sockopts);

  从这里我们看出,nf_sockopts 链表中记录的并不是 new_sockopt 的地址,而是其中的 list 元素的地址。如何通过链表访问到 new_sockopt 呢?下面会有详细介绍。

  b) 删除

 static inline void list_del(struct list_head *entry);

  当我们需要删除 nf_sockopts 链表中添加的 new_sockopt 项时,我们这么操作:

list_del(&new_sockopt.list);

   被剔除下来的 new_sockopt.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);
static inline void list_move_tail
(struct list_head *list, struct list_head *head);

  例如 list_move(&new_sockopt.list,&nf_sockopts) 会把 new_sockopt 从它所在的链表上删除,并将其再链入 nf_sockopts 的表头。

d) 合并

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

static inline void list_splice
(struct list_head *list, struct list_head *head);

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

static inline void list_splice_init
(struct list_head *list, struct list_head *head);

  该函数在将 list 合并到 head 链表的基础上,调用 INIT_LIST_HEAD(list) 将 list 设置为空链。

  3. 遍历

  遍历是链表最经常的操作之一,为了方便核心应用遍历链表,Linux 链表将遍历操作抽象成几个宏。在介绍遍历宏之前,我们先看看如何从链表中访问到我们真正需要的数据项。

  a) 由链表节点到数据项变量

   我们知道,Linux 链表中仅保存了数据项结构中 list_head 成员变量的地址,那么我们如何通过这个 list_head 成员访问到作为它的所有者的节点数据呢?Linux 为此提供了一个 list_entry(ptr,type,member) 宏,其中ptr是指向该数据中 list_head 成员的指针,也就是存储在链表中的地址值,type 是数据项的类型,member 则是数据项类型定义中 list_head 成员的变量名,例如,我们要访问 nf_sockopts 链表中首个 nf_sockopt_ops 变量,则如此调用:

 list_entry(nf_sockopts->next, struct nf_sockopt_ops, list);

  这里 "list" 正是 nf_sockopt_ops 结构中定义的用于链表操作的节点成员变量名。

);

  和


阅读(346) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~