在Linux内核代码中,常常会看到一些经典的实现机制和策略,犹如在万花丛中看到一枝枝特立的花红,惊艳而又令人玩味。
作为实现Linux内核代码的主体语言C,它是朴素的、直接的,直接到你可以对硬件寄存器的某一位进行操作,C语言又是原始的,基本的,基本的就像构建大厦的一块块砖,运用它,你可以随意地建造自己梦想中的大厦。
但是,与其他语言不同,C语言标准库中并没有对数据结构的支持函数,比如,没有对链表、队、栈、树等数据结构操作的函数集合,但在Linux内核代码中,随处都可以觅见这些数据结构的踪影。
现实世界中数据的组织形式逃脱不出数据结构课程所涵盖的那些结构,相对于其他数据结构而言,链表这种组织方式更常用和灵活,或者说,其他数据结构,都可以从链表衍生而来。在Linux内核源代码树中,include/linux/list.h文件中用C语言实现了封装好的、易用的双向链表函数集合,这种实现是高效和可移植的–否则,这些代码也进不了内核,同时,这种实现又是巧妙和可见的,赏析这些代码,让我们领悟代码设计之美妙。
一、链表及衍生
为什么链表是数据组织的根本形式?
最简单的数组组织形式是数组,它在内存顺序存放,其存取效率无疑是高效的,但除非你存放的数据是静态的,否则,增加和删除一个元素的代价是不可小估的。而链表,在建立之初,无需知晓其节点是多少,在构建过程中,增加和删除一个节点与链表的长度无关,主要开销为访问的顺序性和链表节点所占的空间。
尽管链表可以分类为单链表、双链表和循环链表,但在此,以分析双链表为基点,从而退化或者衍生出其他数据结构。
在C 语言中,一个基本的双向链表定义如下:
图1 双链表
通过前趋(prev)和后继(next)两个指针字段,就可以从两个方向遍历双链表,这使得遍历链表的代价减少。如果打乱前驱、后继的依赖关系,就可以构 成”二叉树”;如果再让首节点的前趋指向链表尾节点、尾节点的后继指向首节点(如图1中虚线部分),就构成了循环链表;如果设计更多的指针字段,就可以构 成各种复杂的树状数据结构。
如果减少一个指针域,就退化成单链表,如果只能对链表的首尾进行插入或删除操作,就演变为队结构,如果只能对链表的头进行插入或删除操作,就退化为栈结构。
如此看来,双链表,是演化各种数据结构的基石。
抽象是软件设计中一项基本技术,如上所述,在众多数据结构中,选取双向链表作为基本数据结构,这就是一种提取和抽象。
1. 简约而又不简单的链表定义
于双向链表而言,内核中定义了如下简单结构:
struct list_head {
struct list_head *next, *prev;
};
这个不含任何数据项的结构,注定了它的通用性和未来使用的灵活性,例如前面的例子就可以按如下方式定义:
struct my_list{ void *mydata; struct list_head list; };
在此,进一步说明几点:
1)list字段,隐藏了链表的指针特性,但正是它,把我们要链接的数据组织成了链表。
2)struct list_head可以位于结构的任何位置
3)可以给struct list_head起任何名字。
4)在一个结构中可以有多个list
例如,我们对要完成的任务进行描述,而任务中又包含子任务,于是有如下结构:
————————————————————————————————–———————–
struct todo_tasks{
char *task_name;
unsigned int name_len;
short int status;
int sub_tasks;
int subtasks_completed;
struct list_head completed_subtasks;/* 已完成的子任务形成链表 */
int subtasks_waiting;
struct list_head waiting_subtasks; /* 待完成的子任务形成链表 */
struct list_head todo_list; /* 要完成的任务形成链表 */
};
-----------------------------------------------------------------------简约而又不简单struct list_head,以此为基本对象,就衍生了对链表的插入、删除、合并以及遍历等各种操作:
2. 链表的声明和初始化宏
实际上, struct list_head只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的?让我们来看看下面两个宏:
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)如果我们要申明并定义自己的链表头mylist,直接调用LIST_HEAD:
LIST_HEAD(mylist)
则mylist的next、prev指针都初始化为指向自己,这样,我们就有了一个空链表,如何判断链表是否为空,自己写一下这个简单的函数list_empty ,也就是让头指针的next指向自己而已。
3. staitic inline函数-隐藏并展开
在list.h中定义的函数大都是 staitic inline f()形式?为什么这样定义?
关键字“static”加在函数前,表示这个函数是静态函数,所谓静态函数,实际上是对函数作用域的限制,指该函数的作用域仅局限于本文件。所以说,static具有信息隐藏作用。
而关键字”inline“加在函数前,说明这个函数对编译程序是可见的,也就是说,编译程序在调用这个函数时就立即展开该函数。所以,关键字inline 必须与函数定义体放在一起才能使函数成为内联。inline函数一般放在头文件中。
4. 无处不在的隐藏特性
我们分析一下在链表中增加一个节点的函数实现:
有三个函数:
static inline void __list_add();
static inline void list_add();
static inline void list_add_tail();
————————————————————————————————-
/*
* Insert a new entry between two known consecutive entries.
*
* This is only for internal list manipulation where we know
* the prev/next entries already!
*/
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
————————————————————————————————–
/**
* list_add – add a new entry
* @new: new entry to be added
* @head: list head to add it after
*
* Insert a new entry after the specified head.
* This is good for implementing stacks.
*/
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
————————————————————————————————–
/**
* list_add_tail – add a new entry
* @new: new entry to be added
* @head: list head to add it before
*
* Insert a new entry before the specified head.
* This is useful for implementing queues.
*/
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
————————————————————————————————–
仔细体会其实现代码,看起来简单有效,但实际上也是一种抽象和封装的体现。首先__list_add()函数做基本的操作,该函数仅仅是增加一个节点,至 于这个节点加到何处,暂不考虑。list_add()调用__list_add()这个内部函数,在链表头增加一个节点,实际上实现了栈在头部增加节点的 操作,而list_add_tail()在尾部增加一个节点,实际上实现了队的操作。
至于链表的删除、搬移和合并,比较简单,不再此一一讨论。
5. 链表遍历-似走过千山万水
遍历链表本是简单的,list.h中就定义了如下的宏:
————————————————————————————————–
**
* list_for_each - iterate over a list
* @pos: the &struct list_head to use as a loop counter.
* @head: the head for your list.
*/
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); \
pos = pos->next)
————————————————————————————————–
这种遍历仅仅是找到一个个节点在链表中的位置pos,难点在于,如何通过pos获得节点的地址,从而可以使用节点中的数据? 于是 list.h中定义了晦涩难懂的list_entry()宏:
————————————————————————————————–
/**
* list_entry – get the struct for this entry
* @ptr: the &struct list_head pointer.
* @type: the type of the struct this is embedded in.
* @member: the name of the list_struct within the struct.
*/
#define list_entry(ptr, type, member) \
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
————————————————————————————————–
指针ptr指向结构体type中的成员member;通过指针ptr,返回结构体type的起始地址,如图2。
type
|———-|
| |
| |
|———-|
ptr–> | member –|
|———-|
| |
| |
|———-|
图2 list_entry()宏的示意图
为了便于理解,在此给予进一步说明。
例如my_list结构:
struct my_list{ void *mydata; struct list_head list; }; struct list_head *pos;
则list_entry(pos, mylist, list)宏,就可以根据pos的值,获取mylist的地址,也就是指向mylist的指针,这样,我们就可以存取mylist->mydata字段了。
可为什么能够达到这样的效果?
list_entry(pos, mylist, list) 展开以后为:
((struct my_list *)((char *)(pos) – (unsigned long)(&((struct my_list *)0)->list)))
这看起来会使大多数人眩晕,但仔细分析一下,实际很简单。
((size_t) &(type *)0)->member)把0地址转化为type结构的指针,然后获取该结构中member成员的指针,并将其强制转换为size_t类型。于是,由于结构从0地址开始定义,因此,这样求出member的成员地址,实际上就是它在结构中的偏移量。为了更好的理解这些,我们可以写一段程序来验证:
—————————————————————————————
#include
#include
struct foobar{
unsigned int foo;
char bar;
char boo;
};
int main(int argc, char** argv){
struct foobar tmp;
printf("address of &tmp is= %p\n\n", &tmp);
printf("address of tmp->foo= %p \t offset of tmp->foo= %lu\n", &tmp.foo, (unsigned long) &((struct foobar *)0)->foo);
printf("address of tmp->bar= %p \t offset of tmp->bar= %lu\n", &tmp.bar, (unsigned long) &((struct foobar *)0)->bar);
printf("address of tmp->boo= %p \t offset of tmp->boo= %lu\n\n", &tmp.boo, (unsigned long) &((struct foobar *)0)->boo);
printf("computed address of &tmp using:\n");
printf("\taddress and offset of tmp->foo= %p\n",
(struct foobar *) (((char *) &tmp.foo) - ((unsigned long) &((struct foobar *)0)->foo)));
printf("\taddress and offset of tmp->bar= %p\n",
(struct foobar *) (((char *) &tmp.bar) - ((unsigned long) &((struct foobar *)0)->bar)));
printf("\taddress and offset of tmp->boo= %p\n",
(struct foobar *) (((char *) &tmp.boo) - ((unsigned long) &((struct foobar *)0)->boo)));
return 0;
}Output from this code is:
address of &tmp is= 0xbfffed00
address of tmp->foo= 0xbfffed00 offset of tmp->foo= 0
address of tmp->bar= 0xbfffed04 offset of tmp->bar= 4
address of tmp->boo= 0xbfffed05 offset of tmp->boo= 5
computed address of &tmp using:
address and offset of tmp->foo= 0xbfffed00
address and offset of tmp->bar= 0xbfffed00
address and offset of tmp->boo= 0xbfffed00—————————————————————————————-
到此,我们对链表的实现机制有所了解,但在此止步的话,我们依然无法领略这风景背后的韵味。
尽管list.h是内核代码中的头文件,但我们可以把它移植到用户空间使用。
如前文所述,Linux内核中的代码,经过稍加改造后,可以在用户态下使用。include/linux/list.h中的函数和宏,是一组精心设计的API,有比较完整的注释和清晰的思路。在用户态下使用list.h,查看改造后的list.h
1. 举例
下面是用户态下的例子,用以创建、增加、删除和遍历一个双向链表。
#include
#include
#include "list.h"
struct kool_list{
int to;
struct list_head list;
int from;
};
int main(int argc, char **argv){
struct kool_list *tmp;
struct list_head *pos, *q;
unsigned int i;
struct kool_list mylist;
INIT_LIST_HEAD(&mylist.list); /*初始化链表头*/
/* 给mylist增加元素 */
for(i=5; i!=0; --i){
tmp= (struct kool_list *)malloc(sizeof(struct kool_list));
/* 或者INIT_LIST_HEAD(&tmp->list); */
printf("enter to and from:");
scanf("%d %d", &tmp->to, &tmp->from);
list_add(&(tmp->list), &(mylist.list));
/* 也可以用list_add_tail() 在表尾增加元素*/
}
printf("\n");
printf("traversing the list using list_for_each()\n");
list_for_each(pos, &mylist.list){
/* 在这里 pos->next 指向next 节点, pos->prev指向前一个节点.这里的节点是
struct kool_list类型. 但是,我们需要访问节点本身, 而不是节点中的list字段,宏list_entry()正是为此目的。*/ tmp= list_entry(pos, struct kool_list, list);
printf("to= %d from= %d\n", tmp->to, tmp->from);
}
printf("\n");
/* 因为这是循环链表,也可以以相反的顺序遍历它,
*为此,只需要用'list_for_each_prev'代替'list_for_each', * 也可以调用list_for_each_entry() 对给定类型的节点进行遍历。
* 例如:
*/
printf("traversing the list using list_for_each_entry()\n");
list_for_each_entry(tmp, &mylist.list, list)
printf("to= %d from= %d\n", tmp->to, tmp->from);
printf("\n");
/*现在,我们可以释放 kool_list节点了.我们本可以调用 list_del()删除节点元素, * 但为了避免遍历链表的过程中删除元素出错,因此调用另一个更加安全的宏 list_for_each_safe(), * 具体原因见后面的分析*/
printf("deleting the list using list_for_each_safe()\n");
list_for_each_safe(pos, q, &mylist.list){
tmp= list_entry(pos, struct kool_list, list);
printf("freeing item to= %d from= %d\n", tmp->to, tmp->from);
list_del(pos);
free(tmp);
}
return 0;
}
2. 关于删除元素的不安全性
为什么说调用list_del()删除元素有安全隐患?具体看源代码:
/*
* Delete a list entry by making the prev/next entries
* point to each other.
*
* This is only for internal list manipulation where we know
* the prev/next entries already!
*/
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
/**
* list_del – deletes entry from list.
* @entry: the element to delete from the list.
* Note: list_empty on entry does not return true after this, the entry is
* in an undefined state.
*/
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
可以看出,当执行删除操作的时候, 被删除的节点的两个指针被指向一个固定的位置(entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;)。而list_for_each(pos, head)中的pos指针在遍历过程中向后移动,即pos = pos->next,如果执行了list_del()操作,pos将指向这个固定位置的next, prev,而此时的next, prev没有任何意义,别无选择,出错。
而list_for_each_safe(p, n, head) 宏解决了上面的问题:
/**
* list_for_each_safe - iterate over a list safe against removal of list entry
* @pos: the &struct list_head to use as a loop counter.
* @n: another &struct list_head to use as temporary storage
* @head: the head for your list.
*/
#define list_for_each_safe(pos, n, head) \
for (pos = (head)->next, n = pos->next; pos != (head); \
pos = n, n = pos->next)
它采用了一个同pos同样类型的指针n 来暂存将要被删除的节点指针pos,从而使得删除操作不影响pos指针!
实际上,list.h的设计可谓精益求精,煞费苦心,用简洁的代码突破计算机科学中传统的链表实际机制,不仅考虑了单处理机,还利用了Paul E. McKenney提出的RCU(读拷贝更新)的技术,从而提高了多处理机环境下的性能。关于RCU,请看
|
文件: |
20091008-kernel_list-chenjifeng.tar.tar |
大小: |
29KB |
下载: |
下载 | |
后记:
链表,是一个古老而没有新意的话题,关于其分析的文章,也随处可见。之所以重提旧话题,是因为在讲课的过程中,每当我对那些复杂的事物进行剖析 时,剥去一层层外衣,发现,最终的实现都掉落在计算机科学最根本的问题上,比如各种最基本的数据结构,可这些,往往又是学生们不屑一顾的。在此,把链表那拿出来分析,是希冀学子们有时间关注计算机科学的根本问题。
内核里面用的最多的两类函数:链表和字符串处理。一旦出错,后果不堪设想。