单处理器系统上的 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() 延迟到释放锁之后。但是,在某些情况下,这可能会非常难看。

  1. 如果需要在同一关键部分中将多个项传递给 call_rcu(),则代码需要创建一个列表,然后在释放锁后遍历该列表。

  2. 在某些情况下,锁将在某些内核 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() 只是立即返回,它会过早地发出宽限期结束的信号,当该其他线程再次开始运行时,这将给它带来令人讨厌的震惊。