原文 :http://wurong81.spaces.live.com/blog/cns!5EB4A630986C6ECC!393.entry
Linux内核用到的Radix Tree
1. 简介
Linux内核的页高速缓存中,文件每个数据块最多只能对应一个Page Cache,它通过两个数据结构来管理这些 Cache 项,一个是 radix tree,另一个是双向链表。Radix tree 是一种搜索树,Linux 内核利用这个数据结构来通过文件内偏移快速定位 Cache 项,下图 是 radix tree对地址索引的一个示意图,该 radix tree 的分叉为4(22),树高为4,用来快速定位8位文件内偏移。radix tree 中的每一个叶子节点指向文件内相应偏移所对应的Cache项。
将上图继续形象化,Linux中radix tree可以描述为下图:
该图显示的分叉数为2^2 = 4。它的树高height为3。每个节点都包含一个height域以标记以该子树为根其下的最大二进制整数是多少,以优化搜索速度。当然,需要一个类似下面的一个全局数组来进行的:
static unsigned long height_to_maxindex[RADIX_TREE_MAX_PATH+1];
2. 与基本radix tree的重要区别
(1) 对应的key值为2进制形式的整数,而非字符串,整数比较远快于字符串比较。所有的叶节点对应的是long整数值,也就是地址空间值
(2) 当后向链接(分叉)用2进制整数描述,则路径边将不会包含任何实际内容,只需要后向链接即可描述一条路径,这就跟标准的Trie结构一样了。上图中将2进制表示为十进制数值,就是“0,1,2,3”4个后向链接。
3. 实现关键:
(1) 后向链接的数目由不同系统而不同,比如lib/radix-tree.c中有下面的define:
#ifdef __KERNEL__
#define RADIX_TREE_MAP_SHIFT (CONFIG_BASE_SMALL ? 4 : 6)
#else
#define RADIX_TREE_MAP_SHIFT 3 /* For more stressful testing */
#endif
#define RADIX_TREE_MAP_SIZE (1UL << RADIX_TREE_MAP_SHIFT)
#define RADIX_TREE_MAP_MASK (RADIX_TREE_MAP_SIZE-1)
也就是说,内核应用中,如果CONFIG_BASE_SMALL被标记,则每个节点提供2^4=32个分叉,否则提供2^6=64个分叉。RADIX_TREE_MAP_MASK则表示取4或6位。显然最大深度为:
#define RADIX_TREE_INDEX_BITS (8 /* CHAR_BIT */ * sizeof(unsigned long))
#define RADIX_TREE_MAX_PATH (DIV_ROUND_UP /
(RADIX_TREE_INDEX_BITS,RADIX_TREE_MAP_SHIFT))
(2) insert/delete因为需要维护每个节点的height域,所以不是单纯按照基本radix tree的split和merge来做的,而是需要调整相关节点的height值,是通过分别调用radix_tree_extend和radix_tree_shrink来实现的。
其中radix_tree_extend只有在新插入的整数大于当前height下所能存储的最大整数值时才需要调整,因为后者是该height下满二叉树所能存储的最大值,而如果不超过最大值,则肯定会插在某一高度层次中而无需调整其它任何节点的height。
(3) 考虑并发:由于需要页高速缓存是全局的,各进程不停的访问,必须要考虑其并发性能,单纯的对一棵树使用锁导致的大量争用是不能满足速度需要的,Linux中是在遍历树的时候采用一种RCU技术,来实现同步并发。
RCU(Read-Copy Update),说实话,我完全没去看,只知道是一种保证读该radix tree的时候,可以不要管insert/delete操作,即不需使用锁。从内核代码来看,lookup操作的时候,读一个节点的时候,采用类似于 node = rcu_dereference(*slot); 的调用。Insert/delete操作指针的时候,采用 rcu_assign_pointer(node->slots[offset], slot); 的调用。具体同步的事情都交给RCU去搞的。