 查询与 rcu_dereference 检查全解)
Linux 内核 RCU 与 lockdep 协作机制读侧临界区跟踪、*_held() 查询与 rcu_dereference 检查全解【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux本篇基于内核源码树中的 RCU and lockdep checking 一文系统讲解 Linux 内核 RCURead-Copy-Update与 lockdep 锁依赖检查器的协作机制lockdep 如何感知每个任务进出各类 RCU 读侧临界区、六种保守型*_held()查询原语的语义以及CONFIG_PROVE_RCU下rcu_dereference()家族宏的验证行为。读完本文你将能在死锁排查中利用 lockdep 的 RCU 状态跟踪并能正确选用rcu_dereference_check()、rcu_dereference_protected()等原语为并发代码标注合法的解引用条件。1. 为什么 lockdep 需要看见 RCU 读侧临界区RCU 的读侧临界区如rcu_read_lock()/rcu_read_unlock()并非传统意义上的锁但它们在并发语义上承担着一类共享资源持有的角色更新方必须等待读侧临界区结束才能释放数据。如果 lockdep 对这种持有状态一无所知那么当死锁或锁序冲突恰好经由 RCU 临界区传导时lockdep 的告警就会缺失关键上下文调试会变得异常困难。为此内核为**每一种 RCU 类型flavor**都提供了独立的 lockdep 跟踪使 lockdep 能够感知每个任务何时进入或离开任何类型的 RCU 读侧临界区并将 RCU 状态纳入其依赖图从而在调试死锁等并发问题时提供额外线索。文档中特别提示2.6.32 及更早版本并非如此——早期内核中各 RCU flavor 共用跟踪状态而当前源码树中已经做到了逐 flavor 独立跟踪。从源码结构看每个 RCU 类型对应一个独立的lockdep_map定义在 kernel/rcu/update.cstruct lockdep_map rcu_lock_map { .name rcu_read_lock, .key rcu_lock_key, .wait_type_outer LD_WAIT_FREE, .wait_type_inner LD_WAIT_CONFIG, /* PREEMPT_RT implies PREEMPT_RCU */ }; struct lockdep_map rcu_bh_lock_map { .name rcu_read_lock_bh, ... .wait_type_inner LD_WAIT_CONFIG, /* PREEMPT_RT makes BH preemptible. */ }; struct lockdep_map rcu_sched_lock_map { .name rcu_read_lock_sched, ... .wait_type_inner LD_WAIT_SPIN, }; // Tell lockdep when RCU callbacks are being invoked. struct lockdep_map rcu_callback_map STATIC_LOCKDEP_MAP_INIT(rcu_callback, rcu_callback_key);这些 map 在 include/linux/rcupdate.h 中以extern声明供全局使用。rcu_read_lock()进入读侧临界区时通过rcu_lock_acquire()在 lockdep 中登记rcu_lock_map见 include/linux/rcupdate.h 中rcu_lock_acquire()对lock_acquire()的封装退出时以rcu_lock_release()注销于是 lockdep 的每个任务锁依赖链中都能反映当前正持有哪种 RCU 读侧临界区。此外还有rcu_callback_map用于在 RCU 回调执行期间告知 lockdep 当前处于回调上下文。2. 六种*_held()查询原语保守返回语义在写并发代码或调试代码时经常需要断言当前必须处于某类 RCU 读侧临界区内。文档列出了 RCU 提供的六个 lockdep 状态查询原语原语检查目标说明rcu_read_lock_held()普通 RCUvanilla RCU是否持有rcu_read_lock()rcu_read_lock_bh_held()RCU-bh是否持有 RCU-bh 读侧临界区rcu_read_lock_sched_held()RCU-sched是否持有 RCU-sched 读侧临界区rcu_read_lock_any_held()任意 vanilla RCU覆盖普通 RCU、RCU-bh、RCU-schedsrcu_read_lock_held()SRCU是否持有 SRCU 读侧临界区rcu_read_lock_trace_held()RCU Tasks Trace是否处于 tracing 专用的 RCU 类型内2.1 关键设计保守返回避免误报文档强调了这些函数的保守性conservative当它们不能确定结果时例如CONFIG_DEBUG_LOCK_ALLOC未开启、lockdep 尚未初始化等情形一律返回 1认为是持有。这样做的直接收益是像WARN_ON(!rcu_read_lock_held())这类断言在 lockdep 关闭时不会打出假阳性false positives——否则每个未开 lockdep 的内核都会在这种断言处无差别报警反而淹没真正的问题。源码印证了这一分叉行为。在 include/linux/rcupdate.h 中当CONFIG_DEBUG_LOCK_ALLOC未开启时这些函数退化为廉价的静态内联版本static inline int rcu_read_lock_held(void) { return 1; } static inline int rcu_read_lock_sched_held(void) { return !preemptible(); } static inline int rcu_read_lock_any_held(void) { return !preemptible(); }注意一个细节即便在禁用 lockdep 的退化路径下rcu_read_lock_sched_held()和rcu_read_lock_any_held()仍会返回!preemptible()——即它们退化为当前是否处于禁抢占状态的近似判断因为只有禁抢占区间才可能构成 RCU-sched 读侧临界区而rcu_read_lock_held()则直接保守返回 1。当CONFIG_DEBUG_LOCK_ALLOC开启时同名函数变为真实查询实现在 kernel/rcu/update.cint notrace rcu_read_lock_held(void) { bool ret; if (rcu_read_lock_held_common(ret)) return ret; return lock_is_held(rcu_lock_map); } int notrace rcu_read_lock_bh_held(void) { bool ret; if (rcu_read_lock_held_common(ret)) return ret; return in_softirq() || irqs_disabled(); } int notrace rcu_read_lock_any_held(void) { bool ret; if (rcu_read_lock_held_common(ret)) return ret; if (lock_is_held(rcu_lock_map) || lock_is_held(rcu_bh_lock_map) || lock_is_held(rcu_sched_lock_map)) return 1; return !preemptible(); }几个值得注意的实现细节notrace标注这些函数可能被 tracing 路径调用tracing 基础设施自身就运行在 RCU 读侧临界区内若再触发跟踪事件就会递归锁死系统因此标注notrace排除跟踪。rcu_read_lock_held_common()短路它先检查 CPU 是否处于空闲/离线等不可能持有 RCU 锁的状态能短路就直接给出确定答案避免在非法上下文中误判为持有。rcu_read_lock_bh_held()的 BH 判断RCU-bh 读侧临界区等价于软中断/中断被禁用的区间因此用in_softirq() || irqs_disabled()判断覆盖local_bh_disable()、local_irq_disable()等各种禁 BH 手段。debug_lockdep_rcu_enabled()双保险真实查询前先调用该函数kernel/rcu/update.c确认RCU 调度器已激活 debug_locks开启 非 lockdep 递归防止开机早期lockdep 尚未初始化以及 lockdep 运行时被禁用两种时机打出假阳性。SRCU 侧的srcu_read_lock_held()实现在 include/linux/srcu.htracing 专用的rcu_read_lock_trace_held()在 include/linux/rcupdate_trace.h。2.2lockdep_assert_in_rcu_*()系列从查询到断言在查询原语之上include/linux/rcupdate.h 提供了一组可直接用于WARN_ON_ONCE的断言宏#define lockdep_assert_in_rcu_read_lock() \ WARN_ON_ONCE(lockdep_assert_rcu_helper(!lock_is_held(rcu_lock_map), RCU)) #define lockdep_assert_in_rcu_reader() \ WARN_ON_ONCE(lockdep_assert_rcu_helper(!lock_is_held(rcu_lock_map) \ !lock_is_held(rcu_bh_lock_map) \ !lock_is_held(rcu_sched_lock_map) \ preemptible(), RCU))文档级约束写在宏注释里有两点容易被误用lockdep_assert_in_rcu_read_lock_bh()不接受local_bh_disable()之类的区域必须持有真正的rcu_read_lock_bh()同理lockdep_assert_in_rcu_read_lock_sched()要求真正的rcu_read_lock_sched()preempt_disable()不算。而更宽松的lockdep_assert_in_rcu_reader()则接受任何不可抢占状态因为preempt_disable、local_bh_disable、local_irq_disable保护的区间都算 RCU 读侧临界区。3. CONFIG_PROVE_RCUrcu_dereference()原语的 lockdep 检查文档指出另一个独立的内核配置项CONFIG_PROVE_RCU启用了rcu_dereference()家族原语的 lockdep 状态检查。从 kernel/rcu/Kconfig.debug 可以看到其定义方式config PROVE_RCU def_bool PROVE_LOCKING config PROVE_RCU_LIST bool RCU list lockdep debugging depends on PROVE_RCU RCU_EXPERT default n即PROVE_RCU由 lockdep 总开关PROVE_LOCKING自动派生不需要手动设置而PROVE_RCU_LIST对 RCU 链表用法做 lockdep 检查因尚有待转换的用户默认关闭以避免假阳性告警。3.1 完整原语清单在CONFIG_PROVE_RCU开启时文档列出的rcu_dereference()家族及其检查行为如下原语检查行为rcu_dereference(p)检查处于 vanilla RCU 读侧临界区rcu_dereference_bh(p)检查处于 RCU-bh 读侧临界区rcu_dereference_sched(p)检查处于 RCU-sched 读侧临界区srcu_dereference(p, sp)检查处于 SRCU 读侧临界区rcu_dereference_check(p, c)显式条件c 隐式rcu_read_lock_held()rcu_dereference_bh_check(p, c)显式条件c 隐式rcu_read_lock_bh_held()rcu_dereference_sched_check(p, c)显式条件c 隐式rcu_read_lock_sched_held()srcu_dereference_check(p, c)显式条件c 隐式srcu_read_lock_held()rcu_dereference_raw(p)不做任何检查文档明确建议能不用就不用rcu_dereference_raw_check(p)完全不触发 lockdep能不用就不用rcu_dereference_protected(p, c)显式条件c且省略所有屏障与编译器约束rcu_access_pointer(p)返回指针值省略所有屏障但保留防止重复读取/合并的编译器约束*_check变体的价值场景是同一段代码既可能被 RCU 读侧调用也可能被更新方持有其他锁调用。此时用一个布尔表达式把两种合法上下文都表达出来让 lockdep 在两种情况下都不误报且只在实际不满足任何条件时才报警。3.2 宏展开层面的检查机制所有*_check变体最终汇聚到 include/linux/rcupdate.h 的核心宏__rcu_dereference_check()#define __rcu_dereference_check(p, local, c, space) \ ({ \ /* Dependency order vs. p above. */ \ typeof(*p) *local (typeof(*p) *__force)READ_ONCE(p); \ RCU_LOCKDEP_WARN(!(c), suspicious rcu_dereference_check() usage); \ rcu_check_sparse(p, space); \ ((typeof(*p) __force __kernel *)(local)); \ })它做三件事(1) 用READ_ONCE读取指针防止编译器重复读取或合并两次读取(2) 若条件c不满足触发RCU_LOCKDEP_WARN()(3) 在 sparse__CHECKER__静态检查下验证指针带有__rcu注解。普通rcu_dereference()就是c传 0 的特例见 include/linux/rcupdate.h#define rcu_dereference(p) rcu_dereference_check(p, 0)此时条件退化为纯rcu_read_lock_held()。告警宏RCU_LOCKDEP_WARN()本身include/linux/rcupdate.h有一个精巧的双重调用结构#define RCU_LOCKDEP_WARN(c, s) \ do { \ static bool __section(.data..unlikely) __warned; \ if (debug_lockdep_rcu_enabled() (c) \ debug_lockdep_rcu_enabled() !__warned) { \ __warned true; \ lockdep_rcu_suspicious(__FILE__, __LINE__, s); \ } \ } while (0)注释说明了原因在检查条件(c)之前先查一次debug_lockdep_rcu_enabled()防止开机早期 lockdep 尚未初始化时的告警在(c)之后再查一次防止与运行时禁用 lockdep如systemctl enable lockdown一类的echo 0 /sys/kernel/lockdep操作产生竞态导致的假阳性。__warned静态变量则保证同一处代码只 splat 一次避免刷屏。未开启PROVE_RCU时整个宏编译为空do { } while (0 (c))。另外PROVE_RCU还附带一项读侧纪律检查rcu_sleep_check()include/linux/rcupdate.h在每次内核上下文切换点检查是否在 RCU 读侧临界区里非法切换了上下文例如在 vanilla RCU 读侧临界区CONFIG_PREEMPT_RCU未开启时内可睡眠或 RCU-sched 临界区内切换上下文都会触发RCU_LOCKDEP_WARN。4. 实战示例一个RCU 或锁或独占的解引用文档给出了一段经典的复合条件示例它完整展示了rcu_dereference_check()的多上下文表达力file rcu_dereference_check(fdt-fd[fd], lockdep_is_held(files-file_lock) || atomic_read(files-count) 1);该表达式以 RCU 安全的方式取走指针fdt-fd[fd]并在CONFIG_PROVE_RCU下验证本次调用至少满足以下三种情形之一处于 RCU 读侧临界区内这是隐含条件由宏追加的|| rcu_read_lock_held()提供——指针以 RCU 方式安全取走持有files-file_lock——该锁阻止指针被并发修改当前任务独占该files_structcount 1——没有其他任务可访问同样阻止修改。如果同一段代码只会被更新方代码调用不可能落在 RCU 读侧临界区内则更合适的写法是rcu_dereference_protected()file rcu_dereference_protected(fdt-fd[fd], lockdep_is_held(files-file_lock) || atomic_read(files-count) 1);两者语义差异要点rcu_dereference_protected()只验证条件 #2 和 #3不再把 RCU 读侧临界区视为合法上下文——若真在 RCU 读侧临界区内使用它而不满足这两个条件lockdep 会抱怨。因为它省略了所有内存屏障和编译器约束不产生READ_ONCE生成的代码更优代价是数据结构不能变这一前提必须无条件成立——文档明确警告只要 RCU 保护指针或其指向的数据可能被并发修改使用rcu_dereference_protected()就是非法的。从源码看include/linux/rcupdate.h__rcu_dereference_protected()直接返回原指针而不做READ_ONCE印证了它连读取本身都不加约束。对比之下rcu_access_pointer()include/linux/rcupdate.h保留了编译器约束防止重复/合并读取但省去屏障适合只取指针值不解引用的场景例如与 NULL 比较文档建议直接测试其返回值而非存入局部变量以免日后无心的改动引入意外解引用。5. 链表/哈希表遍历原语的可选 lockdep 表达式文档最后一部分说明与rcu_dereference()类似当 lockdep 开启时RCU 链表遍历原语list_for_each_entry_rcu等同样会检查是否处于 RCU 读侧临界区内但它们额外接受一个可选的 lockdep 表达式参数——提供该表达式后只有当表达式为假且不在任何 RCU 读侧临界区内时才会报警。文档给出的现实例子是 workqueue 的for_each_pwq()宏它的设计契约是必须处于 RCU 读侧临界区内或者持有wq-mutex。当前源码树中的实现kernel/workqueue.c与文档描述一致/** * for_each_pwq - iterate through all pool_workqueues of the specified workqueue * pwq: iteration cursor * wq: the target workqueue * * This must be called either with wq-mutex held or RCU read locked. * ... * The if/else clause exists only for the lockdep assertion and can be * ignored. */ #define for_each_pwq(pwq, wq) \ list_for_each_entry_rcu((pwq), (wq)-pwqs, pwqs_node, \ lockdep_is_held((wq-mutex)))宏把lockdep_is_held((wq-mutex))作为 lockdep 表达式传给list_for_each_entry_rcu该宏定义于 include/linux/rculist.h于是两种合法上下文RCU 读锁 或wq-mutex都不会触发 splat只有两者都不满足时才会报告。这个模式在整棵源码树中被广泛使用任何RCU 或某把锁二选一的迭代点都可以把持锁判断写进第四个参数。6. 适用前提与使用建议综合文档内容与源码使用这套机制时的注意事项如下配置前提CONFIG_PROVE_RCU由PROVE_LOCKING自动开启见 kernel/rcu/Kconfig.debug而PROVE_LOCKING依赖DEBUG_LOCK_ALLOC。生产内核若未开启这些选项rcu_dereference()家族中的 lockdep 检查、以及第 2 节中*_held()的真实查询能力都不生效——*_held()退化为保守的静态判断多数恒返回 1。保守返回是特性而非缺陷rcu_read_lock_held()等在不确定时返回 1因此不要用它们做生产路径的功能控制流判断它们专为调试断言如WARN_ON(!rcu_read_lock_held())设计。*_check表达式可任意rcu_dereference_check()的条件参数可以是任意布尔表达式但通常应包含一个 lockdep 表达式如lockdep_is_held()使其在 lockdep 下获得真实的依赖检查能力。raw/protected变体要克制文档对rcu_dereference_raw()、rcu_dereference_raw_check()的告诫是 Use sparingly, if at allrcu_dereference_protected()生成更优代码但一旦并发修改可能发生就是数据竞争。tracing 路径因担心 RCU 检查引发递归锁死需要的是rcu_dereference_raw_check()这类完全不查 lockdep 的变体见 include/linux/rcupdate.h 注释。版本语义文档中的*_check注释还提示自 v5.0 起 vanilla RCU 宽限期同时等待local_bh_disable()与preempt_disable()区域故synchronize_rcu()等语义已把rcu_read_lock_bh()、rcu_read_lock_sched()区域一并纳入——在当前源码树上阅读这些宏注释时不要把它移植到 v5.0 之前的内核版本。7. 小结Documentation/RCU/lockdep.rst 所描述的机制本质上是用 lockdep 把 RCU 读侧临界区升格为 lockdep 可见的依赖节点一方面各 flavor 的lockdep_mapkernel/rcu/update.c让死锁分析纳入 RCU 上下文另一方面*_held()查询、RCU_LOCKDEP_WARNinclude/linux/rcupdate.h与rcu_dereference_*家族宏把解引用 RCU 指针必须处于合法上下文变成了运行时可验证的断言。掌握rcu_dereference_check()的显式条件表达式和链表遍历原语的可选 lockdep 参数是编写既能通过 lockdep 审计、又保持并发正确性的内核代码的关键技能。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考