前言 本文为原创,可能会存在一些知识点或理解上的问题,欢迎切磋和交流 ^_^
原英文链接:https://lwn.net/Articles/175432/
内核包含了很多库例程,用于实现一些有用的数据结构。其中就有两种类型的树结构:radix-tree和rb-tree。本文主要是介绍一下radix-tree树API接口。
维基百科上有一篇关于radix-tree树的文章,但是那篇文章并没有很好的介绍linux内核中radix-tree树。Linux内核 radix-tree树实现的是一种机制,那么这种机制可以将指针值和长整型键关联起来。对存储来说,它相当高效,索引速度也相当快。Linux内核中的基数树有一些由内核特定需求驱动的特性,包括将标记与特定条目关联的能力。下图表示的linux内核基数树的叶子节点。
该节点包含了很多slot元素,每一个slot元素都可以包含指针,这个指针可以指向任意一块数据的地址。空slot元素包含内容为null。这种树结构在2.6.16-rc内核中使用非常广泛,每个根树节点有一个指针数组,数组元素个数为64。slots由长整数键的一部分建立索引。如果最高键值小于64,则可以用单个节点表示整个树。但是,通常使用的键集合很大,更大的树长的样子如下图所示(这张图是拷贝的):
这棵树有三层深。当内核查找一个特定的键时,最重要的六个bit位用于在根节点中查找适当的slot槽。接下来的6个bit位将索引中间节点中的槽,最不重要的6位将指示包含指向实际值的指针的槽。没有子节点的节点在树中不存在,因此基数树可以为稀疏树提供有效的存储。
基数树算法结构在主线内核树中有很多应用。PowerPC架构使用基数树映射真实和虚拟IRQ数字。NFS模块将基数树关联进inode结构,以跟踪未完成的请求。对于基数树最广泛的使用是在内存管理代码中。address_space结构体包含一个基数树,它管理和保存用于映射的内核页面。除此之外,基数树还用于内存管理中快速查找脏页或回写页。
作为典型的内核数据结构,有两种声明和初始化基树的模式:
#include <linux/radix-tree.h>
◆ RADIX_TREE(name, gfp_mask); /* Declare and initialize */
◆ struct radix_tree_root my_tree;
INIT_RADIX_TREE(my_tree, gfp_mask);
第一种形式写法表明并初始化一个给定名称的基数树,第二种形式是在程序运行时执行初始化。这两种形式的初始化,必须输入gfp_mask来告诉内核如何分配内存。如果是在原子上下文中操作基数树,gfp_mask标识必须是GFP_ATOMIC。
插入和删除值条目的函数如下所示:
int radix_tree_insert(struct radix_tree_root *tree, unsigned long key, void *item);
void *radix_tree_delete(struct radix_tree_root *tree, unsigned long key);
调用radix_tree_insert()接口会在给定树中插入一个给定条目(关联键)。该操作需要对插入的条目进行内存分配,如果内存分配失败,则插入失败,函数结果将返回ENOMEM错误码。如果条目已经存在于树中,则该接口将禁止覆盖插入,并且该接口返回EEXIST。如果该函数执行成功,则返回0。radix_tree_delete()用于从树中删除与键关联的条目,如果条目存在,则返回指向条目项的指针。
在某些特殊情况下,插入失败可能是一个重大问题。为了避免这种情况,提供了两种接口:
int radix_tree_preload(gfp_t gfp_mask);
void radix_tree_preload_end(void);
这个函数将尝试分配足够的内存(使用给定的gfp_mask)来保证下一个基数树插入不会失败。分配的结构存储在每个cpu变量中,这意味着调用函数必须执行插入,然后才能调度或移动到其他处理器。为此,radix_tree_preload()将在成功时返回,并禁用抢占;调用者最终必须通过调用radix_tree_preload_end()来确保再次启用抢占。如果失败,则返回-ENOMEM,并且未禁用抢占。
基数树查找可以通过以下几种方式实现:
void *radix_tree_lookup(struct radix_tree_root *tree, unsigned long key);
void **radix_tree_lookup_slot(struct radix_tree_root *tree, unsigned long key);
unsigned int radix_tree_gang_lookup(struct radix_tree_root *root, void **results, unsigned long first_index, unsigned int max_items);
最简单的形式是radix_tree_lookup(),它在树中查找键并返回关联的项(如果失败则返回NULL)。相反,radix_tree_lookup_slot()将返回一个指针,指向持有该项指针的槽。然后,调用者可以更改指针,将新项与键关联起来。但是,如果条目不存在,radix_tree_lookup_slot()将不会为它创建一个槽,因此这个函数不能代替radix_tree_insert()。
最后,调用radix_tree_gang_lookup()将返回结果中的max_items项,并从first_index开始伴随使键值升序。返回的条目项可能比请求的个数少,但是个数少(除了0之外)并不意味着树中没有更多的值。
值得注意的是,基数树操作不是原子操作,在其内部逻辑中不存在锁的使用。函数调用者必须确保多个线程不会破坏树结构,或者造成不可避免的竞争关系。Nick Piggin目前有一个补丁,是说用读-复制-更新来删除树节点。这个patch允许执行查找操作,而不需要加锁保护。只要满足如下两个条件:(1) 返回的指针必须在原子上下文中操作;(2) 调用者避免自己构造竞争关系。目前还不清楚这个patch何时合入。
基数树支持被称作“tags”的特性,其特定的bit位可以在树的条目上进行设置。Tags被用于比如标记脏或回写的页面。相关api接口如下:
void *radix_tree_tag_set(struct radix_tree_root *tree, unsigned long key, int tag);
void *radix_tree_tag_clear(struct radix_tree_root *tree, unsigned long key, int tag);
int radix_tree_tag_get(struct radix_tree_root *tree, unsigned long key, int tag);
radix_tree_tag_set用于设置标记,那么这些标记存放在被键索引的条目上。如果在一个不存在键上设置标记,这明显是错误的。函数返回值是指向标记条目的指针。这些tags看起来很像一些任意的整数值,但是当前代码只允许设置两个标记。使用除0或1意外的标记,会造成内存踩踏。大概就是这个意思。
使用radix_tree_tag_clear()清除tags,同样,返回值是指向(未标记的)条目的指针。radix_tree_tag_get()函数将检查按键索引的条目项是否具有给定的标记集,如果键不存在,返回值为0; 如果键存在,但没有设置标记,返回值为-1;否则返回值为+1。但是,这个函数目前在源代码中被注释掉了,因为没有树内代码使用它。
另外还有两个查询tag标签的函数接口:
int radix_tree_tagged(struct radix_tree_root *tree, int tag);
unsigned int radix_tree_gang_lookup_tag(struct radix_tree_root *tree, void **results,unsigned long first_index, unsigned int max_items, int tag);
如果树中的任何项带有给定的标记,则radix_tree_tagged()返回一个非零值。可以使用radix_tree_gang_lookup_tag()获得具有给定标记的项列表。
最后,关于基数树API,有一个有趣的点就是没有销毁基数树的函数接口。显然,这是想让基数树永远存在。在实际操作中,从基数树中删除所有条目项将释放除根节点之外与之相关的所有内存,这个操作可以被正常的执行。