1. 并发技术
由于需要页高速缓存是全局的,各进程不停的访问,必须要考虑其并发性能,单纯的对一棵树使用锁导致的大量争用是不能满足速度需要的,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去搞的。
通过使用RCU,RCU Radix树可以进行完全并发的查询操作。RCU从根本上要求原子操作地移动指针从数据结构的一个版本到新的版本,保持旧版本直到系统经过静止状态。在静止状态点,旧版本数据结构已没有用户,因此可以被安全释放。
RCU radix树的修改操作之间还需要串行化,但是查询不再需要与修改操作串行化。
RCU可使RCU radix树查询完全并行化,但修改操作成了“瓶颈”。这可通过将全树的锁破碎成较小的锁进行改善,再明显的方法是对结点进行加锁而非对整个树加锁。
radix树修改操作可分为单向和双向操作。单向操作仅执行从根节点和叶子结点的单方向指针移动,它包括插入、更新和设置标签操作。双向操作较复杂,它需要在指针移到叶子后又回移,它包括删除和清除标签操作。
梯级加锁(Ladder Locking)和锁耦合(Lock-Coupling)技术常用于数据库方面,允许单向遍历结点加锁的树(双向可能产生死锁)。如果所有的修改者从树顶到树底进行修改,并且修改的结点持有锁,那么,向下遍历时对孩子加锁,在孩子被锁住时再释放该结点锁。在这种情况下并发操作是可能的,因为只要根结点解锁,另一个操作就可以自上向下进行。如果两操作的路径没有相同操作结点,后一个操作可能在前一个操作完成之前完成。最坏的情况是流水线操作,但这还是比串行化操作好很多。
双向操作包括删除和清除标签操作,分别说明如下:
8.1清除标签
在radix树中清除一个标签包括向下遍历树、查找定位条目和清除条目标签的操作。只要孩子结点没有打标签的条目,就可以向上遍历结点清除标签。结束条件是:如果遍历遇到一个结点,在清除一个标签后,它还有一个或多个条目带有标签集,就可以结束向上遍历。为了与向下遍历期间有同样的结束点,将终止条件改为:向上遍历将在有比清除标签数更多标签的结点处结束。这样,不论何时遇到这样的结点,将作为上遍历树的结束点。
8.2删除元素
删除元素在删除无用结点时还需要删除该条目的所有标签。它的终止条件需要满足这两个方面。向上回退遍历树时需要满足下面的条件:当遇到一个非空结点且没有无用的标签时应终止向上回退遍历树。
在向下遍历树时鉴别此点的条件是:当遇到有超过2个孩子的结点、并且每个标签来说结点有多于一个标签条目被清除时,结束向上遍历。该条件用来鉴别向上回退遍历的终止点。
8.3并行操作的API实现:查询获取slot操作
查询操作支持RCU无阻塞并行读操作,因此,需要遵循RCU的用法加RCU读锁,还需要将rcu_dereference()用于获得的slot,在写(或更新)操作时,需要给新的slot使用rcu_assign_pointer()。查询操作的使用方法列出如下:
struct page **slot, *page;
rcu_read_lock();
slot = radix_tree_lookup_slot(&mapping->page_tree, index);
page = rcu_dereference(*slot);
rcu_read_unlock();
8.4并行操作的API实现:查询修改slot操作
Linux内核的radix树需要打补丁才支持并发修改。查询仅有一个全局状态:RCU静止状态,并发修改需要跟踪持有什么锁。锁状态对于操作来说必须是外部的,因此,我们需要实例化一个本地上下文跟踪这些锁。查询修改slot的方法列出如下:
struct page **slot;
DEFINE_RADIX_TREE_CONTEXT(ctx,&mapping->page_tree);
radix_tree_lock(&ctx); /*锁住了根结点*/
/* ctx.tree代替&mapping->page_tree作为根,可以传递上下文
slot = radix_tree_lookup_slot(tx.tree, index);
rcu_assign_pointer(*slot, new_page);
radix_tree_unlock(&ctx);
radix树API函数radix_tree_lookup_slot含有锁从树顶向下移动机制,锁移动的代码部分列出如下:
void **radix_tree_lookup_slot(struct
radix_tree *root, unsigned long index)
{
...
RADIX_TREE_CONTEXT(context, root); /*提供上下文和实际的root指针*、
...
do {
...
/* 从树顶向下移动锁*/
radix_ladder_lock(context, node);
...
} while (height > 0);
...
}
2.
其他注意点
间接指针和直接指针
在真实环境中,地址都是字节对齐的,所以不存在最后一位为1的情况。那么就可以用地址的最后一位来标识一些有用的信息:标识为0则意味着该节点是直接节点,直接指向item数据,反之则则为间接节点,指向的是下一层节点。
#define RADIX_TREE_INDIRECT_PTR 1
//把最后一位值为1
static inline void
*radix_tree_ptr_to_indirect(void *ptr)
{ return
(void *)((unsigned long)ptr | RADIX_TREE_INDIRECT_PTR); }
//把最后一位值为0
static inline void
*radix_tree_indirect_to_ptr(void *ptr)
{ return
(void *)((unsigned long)ptr & ~RADIX_TREE_INDIRECT_PTR); }
//判断是否是间接节点
static inline int
radix_tree_is_indirect_ptr(void *ptr)
{ return
(int)((unsigned long)ptr & RADIX_TREE_INDIRECT_PTR); }