单处理器系统上的 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
为什么
synchronize_rcu()
不能在运行可抢占 RCU 的 UP 系统上立即返回?
- 快速测验 #1 的答案
为什么在这种情况下调用
synchronize_rcu()
是不合法的?因为调用函数正在扫描受 RCU 保护的链表,因此位于 RCU 读取侧关键部分内。因此,被调用函数已在 RCU 读取侧关键部分内被调用,并且不允许阻塞。
- 快速测验 #2 的答案
RCU 回调必须遵守什么锁定限制?
在 RCU 回调中获取的任何锁都必须使用自旋锁原语的 _bh 变体在其他地方获取。例如,如果 RCU 回调获取了“mylock”,则此锁的进程上下文获取必须使用诸如 spin_lock_bh() 之类的东西来获取锁。请注意,也可以使用自旋锁的 _irq 变体,例如 spin_lock_irqsave()。
如果进程上下文代码只是使用 spin_lock(),那么,由于可以从软中断上下文中调用 RCU 回调,因此回调可能会从中断进程上下文关键部分的软中断中调用。这将导致自死锁。
这种限制似乎是无谓的,因为很少有 RCU 回调直接获取锁。但是,许多 RCU 回调间接获取锁,例如,通过
kfree()
原语。- 快速测验 #3 的答案
为什么
synchronize_rcu()
不能在运行可抢占 RCU 的 UP 系统上立即返回?因为某个其他任务可能在 RCU 读取侧关键部分的中间被抢占。如果
synchronize_rcu()
只是立即返回,它会过早地发出宽限期结束的信号,当该其他线程再次开始运行时,这将给它带来令人讨厌的震惊。