01.29 躲在 kernel 里的双向循环链表


初窥内核 | 躲在 kernel 里的双向循环链表

Linux kernel 中存在着太多的宏,虽说这样做有助于开发人员对内核代码的复用,但这无疑

我共选择参考了三个内核版本互作补充 2.6.11、2.6.34.5 以及 5.3.8。这样做的唯一原因便是,避开一些宏的依赖,以便你能立马执行从 kernel 中摘出的代码。

我需要再次强调的是,我们避开的仅仅是那些为了系统或是编译器优化而存在的宏(就像 WRITE_ONCE、prefetch(x) 这样的),而非大量关键的、必要的宏。也许日后的某一天,我们会讨论到它们。

只要是你在后面看到的内核代码,你都可以在 http://kernel.org 网站中免费下载。

同时接下来链表部分的代码,你都可在源代码目录的 include/linux/list.h 文件中找见他们。

内核链表的结构

首先你能从下面的定义中看出来的是,struct list_head 是一个双向链表,没错,它的定义就是下面这么简单。

<code>struct list_head {
\tstruct list_head *next, *prev;
};/<code>


初窥内核 | 躲在 kernel 里的双向循环链表

不过它与你之前所学习的链表有一处不同,我相信你也会觉得它在哪里怪怪的,它没有数据域!乍看起来,它就好像没有操作系统的电脑一样,除了会占用空间好像并没什么用。

再来回忆一下,你之前学习的链表也许是这样定义的,

<code>struct normal_list {
\tint data;
\tstruct normal_list *next, *prev;
};/<code>


初窥内核 | 躲在 kernel 里的双向循环链表

而在你对 kernel 提供给你的链表结构进行封装之后,便是这个样子:


初窥内核 | 躲在 kernel 里的双向循环链表

后面的代码,我们就将对 struct fox 构成的完整链表进行操作。

<code>struct fox {
\tint data;
\tstruct list_head list;
};/<code>

这就是内核黑客们的高明之处了,你需要先将其嵌入到你所需要的数据结构中,而后使用。换句话说,你不是向链表这个结构内塞入数据,而是将 kernel 提供给你的这个链表节点塞入到你的数据中。

内核链表的构造

在你有了节点结构后,kernel 也提供给你了构造链表的宏 ,

<code>LIST_HEAD(fox_list)/<code>

执行上述代码后,你得到的不过仅仅是只有一个节点的双向循环链表。


初窥内核 | 躲在 kernel 里的双向循环链表

<code>#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \\
\tstruct list_head name = LIST_HEAD_INIT(name)/<code>

这里放上宏定义的源代码,它不过是为你创建了一个 struct list_head 类型的变量,同时又将其两个指针域都指向了自己。

你既可以说得到的是只有一个节点的双向循环链表,你也可以说它将会在后续的链表操作中起表头节点之用。

为表头添加新节点

当然 kernel 不会仅仅给你一个节点的构造方法,它还提供给你了一整套链表的操作。这些也正是运行在世界上成千上万台 Linux 主机,或上亿上十亿部安卓手机上的代码,首先你将看到的是:

<code>static inline void __list_add(struct list_head *new,
\tstruct list_head *prev,
\tstruct list_head *next)
{
\tnext->prev = new;
\tnew->next = next;
\tnew->prev = prev;
\tprev->next = new;
}

static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
\t __list_add(new, head->prev, head);
}/<code>

在这里我仅仅画出,为表头节点添加第一个后继节点的过程,后面就需要你自己来体验 kernel 链表设计的巧妙了。

<code>list_add_tail(&(f->list),   &(fox_list));/<code>


初窥内核 | 躲在 kernel 里的双向循环链表

节点的遍历

这里先给出你可以复用的部分 kernel 链表结构的代码,然后再来讨论两种不同的遍历方式。完整代码见文末。

<code>struct fox {
\tint data;
\tstruct list_head list;

};



int main()
{
\tLIST_HEAD(fox_list);
\t
\tstruct list_head *pos, *n; //n for removal use.
\tstruct fox *f; // f for loop use.
\tint i;
\t
\tfor (i = 0; i < 3; i++) {
\t f = (struct fox *)malloc(sizeof(struct fox));
\t f->data = 0 + i;
\t list_add_tail(&f->list, &fox_list);
\t
\treturn 0;
}/<code>

阅读上面的代码,你会发现这样一个问题:在上面的 for 循环中,你仅仅是在用 f 这个循环变量接收 struct fox 结构的地址,而包含数据域的指针 f 又一直处于迭代中。

那么你所拥有的便仅仅是内嵌链表结构的地址与它所包含的前后指针,而你却无法访问你真正关心的数据域的值。

换句话说,虽然你能从表头结点切入整条链;但暂时你还是很难从内访问到外层容器 struct fox 中的数据域。

方式一:list_for_each

不过面临刚刚的问题,你还是可以来遍历输出他们的地址玩一玩的。

<code>#define list_for_each(pos, head) \\
\tfor (pos = (head)->next; pos != (head); pos = pos->next)

// 定义与应用

list_for_each(pos, &fox_list)
\tprintf("%d memory address: %p\\n", i++, pos);/<code>

其中 pos 为 struct list_head 类型的指针,用作循环变量;head 则为你表头节点的地址。此时我们进行输出的内容为嵌入在内的链表结构的地址。

刚刚,你在面对的是由内而外的问题。

解决方案是 list_entry 宏,而这又不得不引出了另一个重要的宏 container_of,而前者不过是对后者的封装后的别名罢了。

<code>#define list_entry(ptr, type, member) \\
\tcontainer_of(ptr, type, member)

#define container_of(ptr, type, member) ({ \\
\tconst typeof( ((type *)0)->member ) *__mptr = (ptr); \t \\
\t(type *)( (char *)__mptr - offsetof(type,member) );})

/**
* container_of - cast a member of a structure out to the containing structure
* @ptr:\tthe pointer to the member.
* @type:\tthe type of the container struct this is embedded in.
* @member:\tthe name of the member within the struct.
*//<code>

此处我们只对 container_of 的用法做分析,container 即为容器之意,再回想前文的内容,kernel 对链表结构的处理是将其嵌入到所需的结构之中。

container_of 三个参数中,ptr 为指向某容器内内嵌变量的指针;type 为嵌入在容器内的结构体类型;member 为你定义的嵌入在容器内的变量名。

<code>list_for_each(pos, &fox_list)  {
\tf = list_entry(pos, struct fox, list);
\tprintf("info: %d\\n", f->data);
}/<code>

经过这样处理,你就可以轻松地从指向 struct list_head 变量的指针中,获得它的容器即 struct fox 变量的地址了。

方式二:list_for_each_entry

你会发现,下面将要用到的宏 list_for_each_enrty 不过又是对宏 list_entry 封装后的产物,但它较之前就更为灵活更为优雅。

还要再加上两个新的宏,一个是将指针搬移到链表的第一个节点,另一个是搬移到下一个节点,又都不过是对宏 list_entry 的封装罢了。

<code>#define list_next_entry(pos, member) \\
\tlist_entry((pos)->member.next, typeof(*(pos)), member)

#define list_first_entry(ptr, type, member) \\
\tlist_entry((ptr)->next, type, member)

/**
* list_for_each_entry\t-\titerate over list of given type
* @pos:\tthe type * to use as a loop cursor.
* @head:\tthe head for your list.
* @member:\tthe name of the list_head within the struct.
*/
#define list_for_each_entry(pos, head, member)\t\t\t \\
\tfor (pos = list_first_entry(head, typeof(*pos), member); \\
\t\t&pos->member != (head);\t\t \\
\t\tpos = list_next_entry(pos, member))/<code>

其参数与方式一中提到的都较为相似,毕竟 list_for_each_enrty 算是他们的混合体吧,下面同样给出一个遍历的应用:

<code>list_for_each_entry(f, &fox_list, list)
\tprintf("info: %d\\n", f->data);/<code>

链表的销毁

这篇文章里暂不讨论链表的删改查操作,我选择直接进行链表的销毁操作。

<code>#define list_for_each_entry_reverse(pos, head, member)
static inline void list_del(struct list_head *entry)
static inline void list_replace(struct list_head *old, struct list_head *new)
static inline void list_swap(struct list_head *entry1, struct list_head *entry2)
static inline void list_move(struct list_head *list, struct list_head *head)
static inline void list_cut_before(struct list_head *list, struct list_head *head, struct list_head *entry)
/*
* ......
*//<code>

像上面这些见名知意的操作,你都可以在 include/linux/list.h 文件中找见他们,并且免费使用。

那么接着刚刚的程序,离一个完整的链表应用,你所差的也就是对 malloc 所分配来的动态内存进行释放操作了。同样,先放出来需要用到的新的宏:

<code>/**
* list_for_each_safe - iterate over a list safe against removal of list entry
* @pos:\tthe &struct list_head to use as a loop cursor.
* @n:\t\tanother &struct list_head to use as temporary storage
* @head:\tthe head for your list.
*/
#define list_for_each_safe(pos, n, head) \\
\tfor (pos = (head)->next, n = pos->next; pos != (head); \\
\tpos = n, n = pos->next)/<code>

而后是释放操作的代码,

<code>list_for_each_safe(pos, n, &fox_list) {
\tf = list_entry(pos, struct fox, list);
\tfree(f);
}/<code>

还好这与之前所学的不带 safe 后缀的遍历还是蛮像的嘛,这里仅仅是多了一个与 pos 同为 struct list_head 类型的变量 n,它仅是作为遍历链表时对下一个节点地址的临时存储,从而避免发生遍历时找不到后继节点的情况。


分享到:


相關文章: