RCU 补丁审查清单

本文档包含一个用于生成和审查使用 RCU 的补丁的清单。违反以下列出的任何规则都会导致与遗漏锁定原语相同的问题。此列表基于对相当长一段时间内对此类补丁的审查经验,但始终欢迎改进!

  1. RCU 是否应用于以读取为主的情况?如果数据结构的更新频率超过大约 10%,那么您应该强烈考虑其他方法,除非详细的性能测量表明 RCU 仍然是适合这项工作的工具。是的,RCU 通过增加写入侧开销来减少读取侧开销,这正是 RCU 的正常使用会比更新进行更多读取的原因。

    另一个例外是性能不是问题,而 RCU 提供了更简单的实现。这种情况的一个例子是 Linux 2.6 内核中的动态 NMI 代码,至少在 NMI 很少的架构上。

    还有一个例外是 RCU 读取侧原语的低实时延迟至关重要的情况。

    最后一个例外是 RCU 读取器用于防止无锁更新的 ABA 问题(https://en.wikipedia.org/wiki/ABA_problem)。这确实导致了轻微违反直觉的情况,其中 rcu_read_lock()rcu_read_unlock() 用于保护更新,然而,这种方法可以为某些类型的无锁算法提供与垃圾收集器相同的简化。

  2. 更新代码是否具有适当的互斥?

    RCU 确实允许读取器(几乎)裸奔,但写入器仍然必须使用某种互斥,例如

    1. 锁定,

    2. 原子操作,或

    3. 将更新限制为单个任务。

    如果您选择 #b,请准备好描述您如何在弱排序机器上处理了内存屏障(几乎所有机器都是如此 - 即使 x86 也允许稍后的加载重新排序到先于较早的存储),并准备好解释为什么这种增加的复杂性是值得的。如果您选择 #c,请准备好解释这个单任务如何不会成为大型系统上的主要瓶颈(例如,如果任务正在更新与其他任务可以读取的自身相关的信息,则根据定义不可能存在瓶颈)。请注意,“大型”的定义已发生重大变化:2000 年的八个 CPU 是“大型”,但 2017 年的一百个 CPU 并不显眼。

  3. RCU 读取侧临界区是否正确使用了 rcu_read_lock() 和其他函数?这些原语是防止宽限期过早结束所必需的,这可能会导致数据从读取侧代码下被不加修饰地释放,这会大大增加内核的精算风险。

    作为粗略的经验法则,任何对 RCU 保护的指针的解引用都必须由 rcu_read_lock()rcu_read_lock_bh()rcu_read_lock_sched() 或相应的更新侧锁覆盖。显式禁用抢占(例如,preempt_disable())可以充当 rcu_read_lock_sched(),但可读性较差,并阻止 lockdep 检测锁定问题。获取自旋锁也会进入 RCU 读取侧临界区。

    请注意,您不能依赖于已知仅在不可抢占内核中构建的代码。此类代码可能会而且将会中断,尤其是在使用 CONFIG_PREEMPT_COUNT=y 构建的内核中。

    让 RCU 保护的指针从 RCU 读取侧临界区“泄漏”出去,与让它们从锁下泄漏出去一样糟糕。当然,除非您在让它们退出 RCU 读取侧临界区之前安排了其他保护方式,例如锁或引用计数。

  4. 更新代码是否能容忍并发访问?

    RCU 的全部要点是允许读取器在没有任何锁或原子操作的情况下运行。这意味着读取器将在更新正在进行时运行。根据情况,有多种方法可以处理此并发

    1. 使用列表和 hlist 更新原语的 RCU 变体在 RCU 保护的列表中添加、删除和替换元素。或者,使用已添加到 Linux 内核中的其他 RCU 保护的数据结构。

      这几乎总是最好的方法。

    2. 按照上面的 (a) 中的步骤进行,但也维护每个元素的锁(由读取器和写入器获取),这些锁保护每个元素的状态。如果需要,读取器避免访问的字段可以由仅由更新程序获取的其他锁保护。

      这也非常有效。

    3. 使更新对读取器呈现原子性。例如,对正确对齐的字段的指针更新将呈现原子性,单个原子原语也将如此。在锁下执行的操作序列对 RCU 读取器不会呈现原子性,多个原子原语的序列也不会。一种替代方法是将多个单独的字段移动到单独的结构中,从而通过施加额外的间接级别来解决多字段问题。

      这可以工作,但开始变得有点棘手。

    4. 仔细排序更新和读取,以便读取器在更新的所有阶段都看到有效数据。这通常比听起来更困难,特别是考虑到现代 CPU 倾向于重新排序内存引用。人们通常必须在代码中自由地散布内存排序操作,这使其难以理解和测试。在它工作的地方,最好使用诸如 smp_store_release() 和 smp_load_acquire() 之类的东西,但在某些情况下,需要 smp_mb() 完全内存屏障。

      如前所述,通常最好将更改的数据分组到一个单独的结构中,以便可以通过更新指针来引用包含更新值的新结构,从而使更改看起来是原子的。

  5. 弱排序的 CPU 会带来特殊的挑战。几乎所有的 CPU 都是弱排序的 - 即使是 x86 CPU 也允许将稍后的加载重新排序到先于较早的存储。RCU 代码必须采取以下所有措施来防止内存损坏问题

    1. 读取器必须保持其内存访问的正确顺序。 rcu_dereference() 原语确保 CPU 在获取指针指向的数据之前先获取指针。这在 Alpha CPU 上确实是必要的。

      rcu_dereference() 原语也是一个极好的文档辅助工具,让读取代码的人确切地知道哪些指针受 RCU 保护。请注意,编译器也可以重新排序代码,并且它们在执行此操作时变得越来越积极。rcu_dereference() 原语因此还可以防止破坏性的编译器优化。但是,通过一些巧妙的创造力,可以错误地处理来自 rcu_dereference() 的返回值。有关更多信息,请参阅 正确使用来自 rcu_dereference() 的返回值

      各种“_rcu()”列表遍历原语(例如 list_for_each_entry_rcu())使用 rcu_dereference() 原语。请注意,更新端代码使用 rcu_dereference() 和“_rcu()”列表遍历原语是完全合法的(如果冗余的话)。这在读者和更新者共用的代码中特别有用。但是,如果在 RCU 读取端临界区之外访问 rcu_dereference(),lockdep 会发出警告。请参阅 RCU 和 lockdep 检查,了解如何处理此问题。

      当然,rcu_dereference() 和 “_rcu()” 列表遍历原语都不能替代在多个更新者之间协调的良好并发设计。

    2. 如果正在使用列表宏,则必须使用 list_add_tail_rcu()list_add_rcu() 原语,以防止弱排序的机器错误排序结构初始化和指针植入。类似地,如果正在使用 hlist 宏,则需要使用 hlist_add_head_rcu() 原语。

    3. 如果正在使用列表宏,则必须使用 list_del_rcu() 原语,以防止 list_del() 的指针毒化对并发读取器产生有害影响。类似地,如果正在使用 hlist 宏,则需要使用 hlist_del_rcu() 原语。

      可以使用 list_replace_rcu()hlist_replace_rcu() 原语,在其各自类型的 RCU 保护列表中,将旧结构替换为新结构。

    4. 与 (4b) 和 (4c) 类似的规则适用于 RCU 保护的链表“hlist_nulls”类型。

    5. 更新必须确保在发布指向该结构的指针之前,发生给定结构的初始化。当发布指向可以通过 RCU 读取端临界区遍历的结构的指针时,请使用 rcu_assign_pointer() 原语。

  6. 如果使用了 call_rcu()call_srcu()call_rcu_tasks()call_rcu_tasks_trace() 中的任何一个,则回调函数可能会从软中断上下文调用,并且在任何情况下都会禁用 bottom halves。特别是,此回调函数不能阻塞。如果需要回调阻塞,请在从回调计划的工作队列处理程序中运行该代码。在 call_rcu() 的情况下,queue_rcu_work() 函数会为您执行此操作。

  7. 由于 synchronize_rcu() 会阻塞,因此不能从任何类型的 irq 上下文调用它。相同的规则适用于 synchronize_srcu()synchronize_rcu_expedited()synchronize_srcu_expedited()synchronize_rcu_tasks()synchronize_rcu_tasks_rude()synchronize_rcu_tasks_trace()

    这些原语的加速形式与非加速形式具有相同的语义,但加速会消耗更多 CPU 资源。加速原语的使用应限制在很少发生的配置更改操作中,这些操作通常不会在实时工作负载运行时进行。请注意,对 IPI 敏感的实时工作负载可以使用 rcupdate.rcu_normal 内核启动参数完全禁用加速宽限期,但这可能会对性能产生影响。

    特别是,如果您发现自己在循环中重复调用其中一个加速原语,请帮大家一个忙:重新构建您的代码,使其批量更新,从而允许单个非加速原语覆盖整个批次。这很可能比包含加速原语的循环更快,并且对系统的其他部分(尤其是对在系统其余部分上运行的实时工作负载)来说更容易。或者,可以改用异步原语,例如 call_rcu()

  8. 从 v4.20 开始,给定的内核仅实现一种 RCU 风格,对于 PREEMPTION=n 为 RCU-sched,对于 PREEMPTION=y 为 RCU-preempt。如果更新者使用 call_rcu()synchronize_rcu(),则相应的读者可以使用:(1) rcu_read_lock()rcu_read_unlock(),(2) 任何禁用和重新启用软中断的原语对,例如 rcu_read_lock_bh()rcu_read_unlock_bh(),或 (3) 任何禁用和重新启用抢占的原语对,例如 rcu_read_lock_sched()rcu_read_unlock_sched()。如果更新者使用 synchronize_srcu()call_srcu(),则相应的读者必须使用 srcu_read_lock()srcu_read_unlock(),并且使用相同的 srcu_struct。加速 RCU 宽限期等待原语的规则与其非加速对应规则相同。

    类似地,有必要正确使用 RCU Tasks 风格。

    1. 如果更新者使用 synchronize_rcu_tasks()call_rcu_tasks(),则读者必须避免执行自愿上下文切换,即避免阻塞。

    2. 如果更新者使用 call_rcu_tasks_trace()synchronize_rcu_tasks_trace(),则相应的读者必须使用 rcu_read_lock_trace()rcu_read_unlock_trace()

    3. 如果更新者使用 synchronize_rcu_tasks_rude(),则相应的读者必须使用任何禁用抢占的方法,例如 preempt_disable() 和 preempt_enable()。

    混用不同的机制会导致混淆和内核崩溃,甚至会导致可利用的安全问题。因此,当使用非显而易见的原始操作对时,必须添加注释。一个非显而易见的配对示例是网络中的 XDP 功能,它从网络驱动程序 NAPI(软中断)上下文中调用 BPF 程序。BPF 主要依赖 RCU 保护其数据结构,但由于 BPF 程序调用完全发生在 NAPI 轮询周期中的单个 local_bh_disable() 部分内,因此这种用法是安全的。这种用法安全的原因是,当更新者使用 call_rcu()synchronize_rcu() 时,读取者可以使用任何禁用 BH 的操作。

  9. 尽管 synchronize_rcu()call_rcu() 慢,但它通常会产生更简单的代码。因此,除非更新性能至关重要,或者更新者不能阻塞,或者 synchronize_rcu() 的延迟对用户空间可见,否则应优先使用 synchronize_rcu() 而不是 call_rcu()。此外,kfree_rcu() 和 kvfree_rcu() 通常会产生比不使用 synchronize_rcu()synchronize_rcu() 更简单的代码,并且没有 synchronize_rcu() 的多毫秒延迟。因此,请在适用的情况下利用 kfree_rcu() 和 kvfree_rcu() 的“即发即忘”内存释放功能。

    synchronize_rcu() 原语的一个特别重要的特性是它会自动自我限制:如果由于任何原因导致宽限期延迟,那么 synchronize_rcu() 原语将相应地延迟更新。相比之下,使用 call_rcu() 的代码应在宽限期延迟的情况下显式限制更新速率,因为不这样做会导致过度的实时延迟,甚至 OOM 情况。

    在使用 call_rcu()kfree_rcu() 或 kvfree_rcu() 时获得此自我限制属性的方法包括:

    1. 维护 RCU 保护的数据结构所使用的数据结构元素的数量计数,包括那些等待宽限期结束的元素。强制限制此数量,根据需要暂停更新,以允许先前延迟的释放完成。或者,仅限制等待延迟释放的数量,而不是元素的总数。

      暂停更新的一种方法是获取更新侧互斥锁。(不要尝试使用自旋锁 - 其他 CPU 在锁上自旋可能会阻止宽限期永远结束。)暂停更新的另一种方法是让更新使用内存分配器周围的包装函数,以便当有太多内存等待 RCU 宽限期时,此包装函数模拟 OOM。当然,还有许多其他变体。

    2. 限制更新速率。例如,如果更新每小时仅发生一次,则不需要显式速率限制,除非您的系统已经严重损坏。dcache 子系统的旧版本采用这种方法,使用全局锁保护更新,限制其速率。

    3. 可信更新 - 如果更新只能由超级用户或其他可信用户手动完成,则可能不需要自动限制它们。这里的理论是,超级用户已经有很多方法可以使机器崩溃。

    4. 定期调用 rcu_barrier(),允许每个宽限期更新的数量有限。

    同样的注意事项也适用于 call_srcu()call_rcu_tasks()call_rcu_tasks_trace()。这就是为什么分别存在 srcu_barrier()rcu_barrier_tasks()rcu_barrier_tasks_trace() 的原因。

    请注意,尽管这些原语确实采取措施避免在任何给定 CPU 具有太多回调时耗尽内存,但有决心的用户或管理员仍然可以耗尽内存。如果已配置具有大量 CPU 的系统将其所有 RCU 回调卸载到单个 CPU 上,或者系统可用内存相对较少,则尤其如此。

  10. 所有 RCU 列表遍历原语,包括 rcu_dereference()list_for_each_entry_rcu() 和 list_for_each_safe_rcu(),必须位于 RCU 读取侧临界区内,或必须受到适当的更新侧锁的保护。RCU 读取侧临界区由 rcu_read_lock()rcu_read_unlock() 或类似的原语(例如 rcu_read_lock_bh()rcu_read_unlock_bh())分隔,在这种情况下,必须使用匹配的 rcu_dereference() 原语才能使 lockdep 满意,在这种情况下,是 rcu_dereference_bh()

    允许在持有更新侧锁时使用 RCU 列表遍历原语的原因是,当读者和更新者之间共享公共代码时,这样做对于减少代码膨胀非常有帮助。RCU 和 lockdep 检查 中讨论了为此情况提供的其他原语。

    此规则的一个例外是,当数据仅添加到链接的数据结构中,并且在读者访问该结构的任何时间都不会删除数据时。在这种情况下,可以使用 READ_ONCE() 代替 rcu_dereference(),并且可以省略读取侧标记(例如,rcu_read_lock()rcu_read_unlock())。

  11. 相反,如果您在 RCU 读取侧临界区中,并且没有持有适当的更新侧锁,则您*必须*使用列表宏的“_rcu()”变体。否则会破坏 Alpha,导致激进的编译器生成错误的代码,并让尝试理解您的代码的人感到困惑。

  12. RCU 回调获取的任何锁都必须在其他地方禁用软中断的情况下获取,例如,通过 spin_lock_bh()。如果在给定获取锁时未禁用软中断,则一旦 RCU 软中断处理程序在中断该获取的临界区时运行您的 RCU 回调,就会导致死锁。

  13. RCU 回调可以并行执行,并且确实如此。在许多情况下,回调代码只是 kfree() 周围的包装器,因此这不是问题(或者,更准确地说,在某种程度上,内存分配器锁定会处理它)。但是,如果回调确实操作共享数据结构,则它们必须使用安全访问和/或修改该数据结构所需的任何锁定或其他同步机制。

    不要假设 RCU 回调将在执行相应 call_rcu()call_srcu()call_rcu_tasks()call_rcu_tasks_trace() 的同一 CPU 上执行。例如,如果给定的 CPU 在有 RCU 回调挂起时脱机,则该 RCU 回调将在某些幸存的 CPU 上执行。(如果不是这种情况,则自我生成的 RCU 回调会阻止受害者 CPU 永远脱机。)此外,由 rcu_nocbs= 指定的 CPU 很可能 *总是* 将其 RCU 回调在其他一些 CPU 上执行,事实上,对于某些实时工作负载,这就是使用 rcu_nocbs= 内核引导参数的全部意义。

    此外,不要假设以给定顺序排队的回调将以该顺序调用,即使它们都在同一个 CPU 上排队也是如此。此外,不要假设同一 CPU 的回调将串行调用。例如,在最新的内核中,可以在卸载和取消卸载回调调用之间切换 CPU,并且在给定的 CPU 正在进行此类切换时,该 CPU 的回调可能会同时由该 CPU 的软中断处理程序和该 CPU 的 rcuo kthread 并发调用。在这种情况下,该 CPU 的回调可能会同时且无序地执行。

  14. 与大多数 RCU 的变体不同,在 SRCU 的读取端临界区(由 srcu_read_lock()srcu_read_unlock() 标记)中阻塞是允许的,因此称为“SRCU”:“可睡眠的 RCU”。请注意,如果您不需要在读取端临界区中睡眠,则应该使用 RCU 而不是 SRCU,因为 RCU 几乎总是比 SRCU 更快且更易于使用。

    同样,与其他形式的 RCU 不同,需要在构建时通过 DEFINE_SRCU() 或 DEFINE_STATIC_SRCU(),或者在运行时通过 init_srcu_struct()cleanup_srcu_struct() 进行显式初始化和清理。后两个函数会传递一个“struct srcu_struct”,该结构定义了给定 SRCU 域的范围。初始化后,将 srcu_struct 传递给 srcu_read_lock()srcu_read_unlock()synchronize_srcu()synchronize_srcu_expedited()call_srcu()。给定的 synchronize_srcu() 仅等待由 srcu_read_lock()srcu_read_unlock() 调用(并且已传递相同的 srcu_struct)所控制的 SRCU 读取端临界区。此属性使得在读取端临界区中睡眠变得可以容忍——给定子系统仅延迟其自身的更新,而不是其他使用 SRCU 的子系统的更新。因此,如果 RCU 的读取端临界区允许睡眠,SRCU 比 RCU 更不容易导致系统 OOM。

    在读取端临界区中睡眠的能力并非没有代价。首先,对应的 srcu_read_lock()srcu_read_unlock() 调用必须传递相同的 srcu_struct。其次,宽限期检测开销仅在共享给定 srcu_struct 的更新中分摊,而不是像其他形式的 RCU 那样全局分摊。因此,只有在读取密集型极高的情况下,或在需要 SRCU 的读取端死锁免疫或低读取端实时延迟的情况下,才应优先使用 SRCU 而不是 rw_semaphore。当您需要轻量级读取器时,还应考虑 percpu_rw_semaphore。

    SRCU 的快速原语 (synchronize_srcu_expedited()) 永远不会向其他 CPU 发送 IPI,因此它比 synchronize_rcu_expedited() 更适合实时工作负载。

    在 RCU Tasks Trace 读取端临界区中睡眠也是允许的,该临界区由 rcu_read_lock_trace()rcu_read_unlock_trace() 分隔。但是,这是一种特殊的 RCU 变体,在没有事先咨询当前用户的情况下,您不应使用它。在大多数情况下,您应该使用 SRCU。

    请注意,rcu_assign_pointer() 与 SRCU 的关系与与其他形式的 RCU 的关系相同,但是为了避免 lockdep splats,您应该使用 srcu_dereference() 而不是 rcu_dereference()

  15. call_rcu()synchronize_rcu() 及其朋友的全部意义在于,等待所有预先存在的读取器完成操作,然后再执行一些破坏性操作。因此,至关重要的是首先删除读取器可以遵循的任何可能受破坏性操作影响的路径,然后调用 call_rcu()synchronize_rcu() 或其朋友。

    由于这些原语仅等待预先存在的读取器,因此调用者有责任保证任何后续的读取器都会安全执行。

  16. 各种 RCU 读取端原语一定包含内存屏障。因此,您应该计划 CPU 和编译器可以自由地将代码重新排序到 RCU 读取端临界区内和外。RCU 更新端原语有责任处理此问题。

    对于 SRCU 读取器,您可以在 srcu_read_unlock() 之后立即使用 smp_mb__after_srcu_read_unlock() 来获取完整的屏障。

  17. 使用 CONFIG_PROVE_LOCKING、CONFIG_DEBUG_OBJECTS_RCU_HEAD 和 __rcu sparse 检查来验证您的 RCU 代码。这些可以帮助发现如下问题:

    CONFIG_PROVE_LOCKING

    检查对受 RCU 保护的数据结构的访问是否在正确的 RCU 读取端临界区内进行,同时持有正确的锁组合或任何其他适当的条件。

    CONFIG_DEBUG_OBJECTS_RCU_HEAD

    检查您是否在上次将同一对象传递给 call_rcu() (或其朋友)后,在 RCU 宽限期经过之前,将同一对象传递给 call_rcu()(或其朋友)。

    CONFIG_RCU_STRICT_GRACE_PERIOD

    与 KASAN 结合使用,以检查泄漏出 RCU 读取端临界区的指针。此 Kconfig 选项对性能和可伸缩性都有很大影响,因此仅限于四核 CPU 系统。

    __rcu sparse 检查

    使用 __rcu 标记指向受 RCU 保护的数据结构的指针,如果未通过 rcu_dereference() 的变体之一进行访问,sparse 将发出警告。

    这些调试辅助工具可以帮助您发现其他情况下难以发现的问题。

  18. 如果您将模块中定义的函数传递给 call_rcu()call_srcu()call_rcu_tasks()call_rcu_tasks_trace(),则必须等待所有挂起的回调被调用后,才能卸载该模块。请注意,仅等待宽限期是绝对不够的!例如,synchronize_rcu() 的实现保证等待通过 call_rcu() 在其他 CPU 上注册的回调。或者甚至在当前 CPU 最近离线又重新上线的情况下。

    您需要改为使用以下屏障函数之一

    然而,这些屏障函数绝对保证等待宽限期。例如,如果系统中没有任何地方排队等待 call_rcu() 回调,那么 rcu_barrier() 可以而且会立即返回。

    因此,如果您需要同时等待宽限期和所有预先存在的回调,您需要调用这两个函数,并且这对函数的选择取决于 RCU 的类型。

    如果需要,您可以使用类似工作队列的方式来并发执行所需的一对函数。

    有关更多信息,请参见 RCU 和可卸载模块