liburcu 提供了多种用户态 RCU 实现,这包括了:
Memory-barrier-based RCU
Bullet-proof RCU
Signal-based RCU
QSBR 与 Linux 中的经典 RCU 非常类似,rcu_read_lock 和 rcu_read_unlock 为零开销,但是线程必须定期调用 rcu_quiescent_state 来表明自己进入了静止状态。
基于内存屏障的 RCU 和抢占式 RCU 非常类似。这种 RCU 读端开销较高,但是不需要再定期调用 rcu_quiescent_state。
Bullet-proff 和基于内存屏障的 RCU 类似,但是 RCU 读端原语会自动调用 rcu_register_thread(),这进一步增加了这些原语的开销。
基于信号的 RCU 消除了 rcu_read_lock() 和 rcu_read_unlock() 的内存屏障,但是依然允许库函数使用这些原语。这些原语的开销减小到接近 QSBR 的成都,但是这要求所有线程(包括库函数创建的)在进入第一个 RCU 读端临界区之前调用 rcu_register_thread()。
QSBR
QSBR(Quiescent-State-Based RCU) 是所有 flavor 中读性能最好的,代价是其代码侵入性较强。对于 Reader 端而言,其开销为零。
这里我们先声明几个术语:
读端临界区:读端读取关键数据的地方。退出临界区后不允许再持有对旧数据的引用。读端临界区由 rcu_read_lock 和 rcu_read_unlock 所包裹。
静止状态(Quiescent State):若线程不处于读端临界区,则被视为处于静止状态。
宽限期:若在一个时间段内所有线程都至少进入了一次静止状态,则这个时间段被称为宽限期。
QSBR 中所有含有读端临界区的线程在启动时需要调用 rcu_register_thread,在销毁时需要调用 rcu_unregister_thread。除此之外,线程被分为上线状态和离线状态,在计算宽限期时离线状态的线程不计入。
每个线程都需要周期性地调用 rcu_quiescent_state()
来表明自己进入了 QS。一旦所有的线程在一个时间段内都调用了 rcu_quiescent_state()
,这个时间段变成了一个宽限期,writer 就可以安全删除数据了。
具体而言,QSBR 中维持了一个全局计数器 rcu_gp.ctr
,每当 writer 进行同步操作后都会递增该值。同时每个 reader 线程也维护了一个线程局部的计数器 rcu_reader.ctr
。当 rcu_gp.ctr == rcu_reader.ctr
时证明线程已经进入了一次 QS。
在 synchronize_rcu() 中,writer 等待所有(在线的)线程局部的计数器和全局计数器一致。