分类: LINUX
2009-11-11 17:30:23
再来看 ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))):
(char *)(ptr)使得指针的加减操作步长为一字节,(unsigned long)(&((type *)0)->member)等于ptr指向的member到该member所在结构体基地址的偏移字节数。二者一减便得出该结构体的地址。转换为 (type *)型的指针,大功告成。
尽管这里使用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 结构中定义的用于链表操作的节点成员变量名。
);
和