一、Radix Tree简介
基数树(Radix Tree)也称为压缩前缀树(compact prefix tree)或 compressed trie,是一种更节省空间的前缀树。在基数树中,树的叶子结点是实际的数据条目。每个结点有一个固定的、 2^n 指针指向子结点 (每个指针称为槽 slot,n 为划分的基的大小)。当父节点只有一个子节点时,子节点会合并到父节点。
基数树示例如下:
上图引用自维基百科Radix tree。
在Linux内核中,Radix Tree由 include/linux/radix-tree.h
和 lib/radix-tree.c
两个文件实现。
二、数据结构
Linux 内核中,使用一个 struct radix_tree_root 结构作为根维护整棵 radix-tree。 每个节点采用 struct radix_tree_node 进行维护。在 radix-tree 中,节点被分作三类:
- 第一类称为 internal 内部节点,内部节点不存储任何私有数据,主要用于维护下一级 节点的 slots 数组;
- 第二类称为 exceptional 节点,这类节点与 internal 类似, 专门维护 radix-tree 中的下一级 exceptional 节点;
- 第三类就是一般节点,一般节点 的 slots 数组里面存储的就是私有数据,且不包含任何 internal 节点。
2.1 radix_tree_root
// file: include/linux/radix-tree.h
/* root tags are stored in gfp_mask, shifted by __GFP_BITS_SHIFT */
struct radix_tree_root {
unsigned int height;
gfp_t gfp_mask;
struct radix_tree_node __rcu *rnode;
};
基数树的树根由结构体radix_tree_root 表示,它包含 3 个成员:
- height – 树的高度
- gfp_mask – 内存分配标识
- rnode – 指向下一层的 radix_tree_node 节点(如果有的话)
2.2 radix_tree_node
// file: lib/radix-tree.c
struct radix_tree_node {
unsigned int height; /* Height from the bottom */
unsigned int count;
union {
struct radix_tree_node *parent; /* Used when ascending tree */
struct rcu_head rcu_head; /* Used when freeing node */
};
void __rcu *slots[RADIX_TREE_MAP_SIZE];
unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];
};
除根节点外,基数树的其它节点使用结构体 radix_tree_node 来表示。该结构体包含如下字段:
- height – 从最底层到当前层的高度
- count – 子节点数量
- parent – 指向父节点的指针
- slots – 指针数组
如果当前节点为位于树的最底层,则数组中的指针指向数据;否则,指向下一级节点。
同样的想法应用到了PG_writeback 标记,该标记表示页正在被写回磁盘。
这样,为基数树的每个节点引入了两个页描述符的标记:PG_dirty 和 PG_writeback。每个节点的 tags 字段中使用了 2 个 64 位的数组(即位图)来存放这两个标记,tags[0] 数组是脏位标记,而 tags[1] 数组是写回标记。
2.3 tags 说明
在普通的基数树中,我们只需要对数据本身进行查询、添加、删除等操作。但是在内核中,还有一种需求就是获取特定状态的数据。
以页高速缓(Page Cache)存为例。由于页的状态可能是脏页(PG_dirty),当我们要批量查找页高速缓存中的脏页时,如果要遍历整个基数树顺序访问所有的叶子节点(页描述符)就太慢了。相反,为了能快速搜索脏页,基数树中的每个中间节点都包含一个针对每个子节点(或叶子节点)的脏位标记,当至少有一个子节点的脏位标记被置位时,此标志被置位。通过这种方式,当内核遍历基数树搜索脏页时,就可以跳过脏位标记为 0 的中间节点的所有子树:中间节点的脏位标记为 0 说明其子树中的所有页描述符都不是脏的。
同样的想法应用到了PG_writeback 标记,该标记表示页正在被写回磁盘。
这样,为基数树的每个节点引入了两个页描述符的标记:PG_dirty 和 PG_writeback。每个节点的 tags 字段中使用了 2 个 64 位的数组(即位图)来存放这两个标记,tags[0] 数组是脏位标记,而 tags[1] 数组是写回标记。