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,请准备好解释此单个任务如何不会成为大型系统上的主要瓶颈(例如,如果任务正在更新与其他任务可以读取的自身相关的信息,则根据定义可能没有瓶颈)。 请注意,“大型”的定义已发生重大变化:八个CPU在2000年是“大型”,但一百个CPU在2017年并不出色。

  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_dereference()原语被各种“_rcu()”列表遍历原语使用,例如list_for_each_entry_rcu()。请注意,更新端代码使用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)的规则适用于“hlist_nulls”类型的受RCU保护的链接列表。

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

  6. 如果使用了call_rcu()call_srcu()call_rcu_tasks()call_rcu_tasks_trace()中的任何一个,则可以从softirq上下文调用回调函数,并且在任何情况下都会禁用下半部。特别是,此回调函数不能阻塞。如果需要回调阻塞,请在该回调中调度的workqueue处理程序中运行该代码。在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)任何禁用和重新启用softirq的原语对,例如,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(softirq)上下文调用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 很可能始终在其他 CPU 上执行其 RCU 回调,事实上,对于某些实时工作负载,这是使用 rcu_nocbs= 内核启动参数的全部意义。

    此外,不要假设以给定顺序排队的回调将按该顺序调用,即使它们都在同一 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_struct 的 srcu_read_lock()srcu_read_unlock() 调用所控制的 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()) 永远不会将 IPI 发送到其他 CPU,因此它比 synchronize_rcu_expedited() 对实时工作负载更友好。

    允许在 RCU Tasks Trace 读取侧临界区中睡眠,该临界区由 rcu_read_lock_trace()rcu_read_unlock_trace() 分隔。但是,这是一种特殊的 RCU 风格,您应该在使用它之前与当前的用戶确认。在大多数情况下,您应该改用 SRCU。

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

  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 上,如果该 CPU 最近脱机又重新联机。

    相反,您需要使用以下屏障函数之一:

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

    因此,如果您需要等待宽限期和所有预先存在的回调,则需要调用这两个函数,该对取决于 RCU 的风格:

    如有必要,您可以使用类似于工作队列的内容来并发执行所需的一对函数。

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