单处理器系统上的 RCU¶
一个常见的误解是,在 UP 系统上,call_rcu()
原语可能会立即调用其函数。这种误解的基础是,由于只有一个 CPU,因此无需等待任何其他事情完成,因为没有其他 CPU 在执行任何其他事情。尽管这种方法在很多情况下都能 勉强 工作,但在一般情况下,这是一个非常糟糕的主意。本文档提供了三个示例,说明了这究竟有多么糟糕。
示例 1:软中断自杀¶
假设一个基于 RCU 的算法在进程上下文中扫描一个包含元素 A、B 和 C 的链表,并且可以在软中断上下文中删除此列表中的元素。假设进程上下文扫描正在引用元素 B 时被软中断处理中断,软中断处理删除了元素 B,然后在宽限期后调用 call_rcu()
来释放元素 B。
现在,如果 call_rcu()
直接调用其参数,那么从软中断返回后,列表扫描会发现它正在引用一个新释放的元素 B。这种情况会大大降低内核的预期寿命。
如果从硬件中断处理程序调用 call_rcu()
,也会发生同样的问题。
示例 2:函数调用致命错误¶
当然,可以通过让 call_rcu()
仅在其从进程上下文调用时才直接调用其参数来避免前面示例中描述的自杀情况。但是,这种情况可能会以类似的方式失败。
假设一个基于 RCU 的算法再次在进程上下文中扫描一个包含元素 A、B 和 C 的链表,但是它在扫描时对每个元素调用一个函数。进一步假设此函数从列表中删除元素 B,然后将其传递给 call_rcu()
以进行延迟释放。这可能有点非常规,但这是完全合法的 RCU 用法,因为 call_rcu()
必须等待宽限期过去。因此,在这种情况下,允许 call_rcu()
立即调用其参数将导致它未能兑现 RCU 的基本保证,即 call_rcu()
将延迟调用其参数,直到当前正在执行的所有 RCU 读取侧关键部分都完成。
- 快速测验 #1
为什么在这种情况下调用
synchronize_rcu()
是 不 合法的?
示例 3:死锁致死¶
假设在持有锁时调用 call_rcu()
,并且回调函数必须获取相同的锁。在这种情况下,如果 call_rcu()
直接调用回调函数,结果将是自我死锁,即使 此调用发生在完全宽限期之后的后续 call_rcu()
调用中也是如此。
在某些情况下,可以重构代码,以便将 call_rcu()
延迟到释放锁之后。但是,在某些情况下,这可能会非常难看。
如果需要在同一个临界区内将多个项目传递给
call_rcu()
,那么代码需要创建一个列表,然后在释放锁后遍历该列表。在某些情况下,锁将在某些内核 API 上持有,因此延迟
call_rcu()
直到释放锁需要通过通用 API 传递数据项。最好保证在不持有任何锁的情况下调用回调,而不是必须修改此类 API 以允许将任意数据项传递回它们。
如果 call_rcu()
直接调用回调,则需要痛苦的锁定限制或 API 更改。
- 快速测验 #2
RCU 回调必须遵守什么锁定限制?
重要的是要注意,用户空间 RCU 实现 允许 call_rcu()
直接调用回调,但前提是自这些回调排队以来已经过去了完整的宽限期。这是因为某些用户空间环境受到极大的限制。尽管如此,强烈建议编写用户空间 RCU 实现的人员避免从 call_rcu()
调用回调,从而获得上述避免死锁的好处。
总结¶
允许 call_rcu()
立即调用其参数会破坏 RCU,即使在 UP 系统上也是如此。所以不要这样做!即使在 UP 系统上,RCU 基础架构 必须 尊重宽限期,并且 必须 从不持有任何锁的已知环境中调用回调。
请注意,在 UP 系统上(包括在 UP 系统上运行的 PREEMPT SMP 构建),synchronize_rcu()
立即返回是安全的。
- 快速测验 #3
为什么在运行可抢占 RCU 的 UP 系统上,
synchronize_rcu()
不能立即返回?
- 快速测验 #1 的答案
为什么在这种情况下调用
synchronize_rcu()
是 不 合法的?因为调用函数正在扫描受 RCU 保护的链表,因此它位于 RCU 读取侧临界区内。因此,调用的函数已在 RCU 读取侧临界区内被调用,并且不允许阻塞。
- 快速测验 #2 的答案
RCU 回调必须遵守什么锁定限制?
在 RCU 回调中获取的任何锁都必须使用 spinlock 原语的 _bh 变体在其他地方获取。例如,如果 RCU 回调获取“mylock”,则此锁的进程上下文获取必须使用诸如 spin_lock_bh() 之类的东西来获取该锁。请注意,也可以使用自旋锁的 _irq 变体,例如 spin_lock_irqsave()。
如果进程上下文代码只是使用 spin_lock(),那么由于可以从软中断上下文中调用 RCU 回调,因此回调可能会从中断进程上下文临界区的软中断中调用。这将导致自我死锁。
这种限制似乎是无谓的,因为很少有 RCU 回调直接获取锁。但是,许多 RCU 回调 间接 获取锁,例如,通过
kfree()
原语。- 快速测验 #3 的答案
为什么在运行可抢占 RCU 的 UP 系统上,
synchronize_rcu()
不能立即返回?因为在 RCU 读取侧临界区中间,可能有一些其他任务被抢占了。如果
synchronize_rcu()
仅仅立即返回,它会过早地发出宽限期结束的信号,当其他线程再次开始运行时,这会给它带来一个糟糕的冲击。