rcu_read_lock 和 x86-64 内存排序

rcu_read_lock and x86-64 memory ordering

在抢占式 SMP 内核上,rcu_read_lock 编译以下内容:

current->rcu_read_lock_nesting++;
barrier();

barrier 是一个编译器指令,不会编译成任何内容。

因此,根据 Intel 的 X86-64 内存订购白皮书:

Loads may be reordered with older stores to different locations

为什么实施实际上没问题?

考虑以下情况:

rcu_read_lock();
read_non_atomic_stuff();
rcu_read_unlock();

是什么阻止了 read_non_atomic_stuff 从 "leaking" 向前超过 rcu_read_lock,导致它 运行 与另一个线程中的回收代码 运行 并发?

对于其他 CPU 上的观察者来说,没有什么可以阻止这一点。没错,StoreLoad 对 ++ 的存储部分进行重新排序可以使其在加载一些内容后全局可见。

因此我们可以得出结论,current->rcu_read_lock_nesting 仅在该核心上被代码 运行 观察到, 或者远程触发了该核心上的内存屏障通过在此处进行调度,或使用专用机制让所有内核在处理器间中断 (IPI) 的处理程序中执行屏障。例如类似于 membarrier() user-space 系统调用。


如果此核心启动 运行 另一个任务,则保证该任务按程序顺序查看此任务的操作。 (因为它在同一个核心上,并且一个核心总是按顺序看到自己的操作。)此外,上下文切换可能涉及完整的内存屏障,因此可以在 另一个 核心上恢复任务而不会中断单线程逻辑。 (当此任务/线程不在任何地方 运行 时,任何核心都可以安全地查看 rcu_read_lock_nesting。)

请注意,内核为您机器的每个内核启动一个 RCU 任务;例如ps 输出在我的 4c8t 四核上显示 [rcuc/0][rcuc/1]、...、[rcu/7]。想必它们是此设计的重要组成部分,让读者可以无障碍地等待。

我还没有研究 RCU 的全部细节,但是 "toy" 中的一个例子 https://www.kernel.org/doc/Documentation/RCU/whatisRCU.txt 是将 synchronize_rcu() 实现为 for_each_possible_cpu(cpu) run_on(cpu); 的 "classic RCU",以使回收器在可能已执行 RCU 操作的每个核心(即每个核心)上执行。完成后,我们知道作为切换的一部分,一定在某处发生了完整的内存屏障。

所以是的,RCU 不遵循经典方法,您需要一个完整的内存屏障(包括 StoreLoad)来使核心在执行任何读取之前等到第一个存储可见。 RCU 避免了读取路径中完整内存屏障的开销。除了避免争用之外,这是它的主要吸引力之一。