
Linux 内核 Lockdep-RCU Splat解读 suspicious RCU usage 告警与三种标准修法【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linuxLockdep-RCU 是 Linux 内核中专门审计 RCU API 误用的调试机制当代码在不满足保护条件时调用rcu_dereference()等宏它会打印一份称为 lockdep-RCU splat 的告警。本篇以仓库文档 lockdep-splat.rst 为主线完整剖析一份真实的历史 splat 报告逐行解释告警各字段的含义并结合 kernel/locking/lockdep.c 与 include/linux/rcupdate.h 的源码给出三种可复制的标准修复手法读完你可以独立判断并消除内核驱动、块设备等子系统中出现的 RCU 误用告警。Lockdep-RCU 是什么、为何重要Lockdep-RCU 于 2010 年初加入 Linux 内核它检查 RCU API 的若干常见误用其中最典型的一类是未处于恰当的 RCU 读侧临界区、也未持有对应的更新侧锁时就调用rcu_dereference()系列宏访问 RCU 保护的指针。一旦检测到此类误用内核就打印 lockdep-RCU splat。Splat 的常见成因是代码访问了 RCU 保护的数据结构却既没有1处于正确类型的 RCU 读侧临界区也没有2持有正确的更新侧锁。正如文档所述这类问题可能相当严重——轻则导致随机内存覆写重则更糟。当然现实中也可能出现误报所以阅读 splat 时需要同时结合held locks清单做交叉验证。从源码结构看这套检查由两部分组成RCU 侧宏。include/linux/rcupdate.h 中的rcu_access_pointer()、L741 处的rcu_dereference_protected()、L751 处的rcu_dereference()等宏在调试配置下会回调 lockdep校验调用点的保护条件Lockdep 侧告警打印。检查失败最终进入 kernel/locking/lockdep.c 的lockdep_rcu_suspicious()它正是 splat 输出的来源。读懂一份真实的 lockdep-RCU splat下面是一份来自 v3.0-rc5 的历史告警示例该问题早已修复文档用它作为标准教材。第一部分是告警头指明出事文件、行号与可疑宏 WARNING: suspicious RCU usage ----------------------------- block/cfq-iosched.c:2776 suspicious rcu_dereference_protected() usage!第二部分是 other info that might help us debug this 区块rcu_scheduler_active 1, debug_locks 0 3 locks held by scsi_scan_6/1552: #0: (shost-scan_mutex){..}, at: [ffffffff8145efca] scsi_scan_host_selected0x5a/0x150 #1: (eq-sysfs_lock){..}, at: [ffffffff812a5032] elevator_exit0x22/0x60 #2: ((q-__queue_lock)-rlock){-.-.}, at: [ffffffff812b6233] cfq_exit_queue0x43/0x190第三部分是完整的调用栈stack backtrace: Pid: 1552, comm: scsi_scan_6 Not tainted 3.0.0-rc5 #17 Call Trace: [ffffffff810abb9b] lockdep_rcu_dereference0xbb/0xc0 [ffffffff812b6139] __cfq_exit_single_io_context0xe9/0x120 [ffffffff812b626c] cfq_exit_queue0x7c/0x190 [ffffffff812a5046] elevator_exit0x36/0x60 [ffffffff812a802a] blk_cleanup_queue0x4a/0x60 [ffffffff8145cc09] scsi_free_queue0x9/0x10 [ffffffff81460944] __scsi_remove_device0x84/0xd0 [ffffffff8145dca3] scsi_probe_and_add_lun0x353/0xb10 [ffffffff817da069] ? error_exit0x29/0xb0 [ffffffff817d98ed] ? _raw_spin_unlock_irqrestore0x3d/0x80 [ffffffff8145e722] __scsi_scan_target0x112/0x680 [ffffffff812c690d] ? trace_hardirqs_off_thunk0x3a/0x3c [ffffffff817da069] ? error_exit0x29/0xb0 [ffffffff812bcc60] ? kobject_del0x40/0x40 [ffffffff8145ed16] scsi_scan_channel0x86/0xb0 [ffffffff8145f0b0] scsi_scan_host_selected0x140/0x150 [ffffffff8145f149] do_scsi_scan_host0x89/0x90 [ffffffff8145f170] do_scan_async0x20/0x160 [ffffffff8145f150] ? do_scsi_scan_host0x90/0x90 [ffffffff810975b6] kthread0xa6/0xb0 [ffffffff817db154] kernel_thread_helper0x4/0x10 [ffffffff81066430] ? finish_task_switch0x80/0x110 [ffffffff817d9c04] ? retint_restore_args0xe/0xe [ffffffff81097510] ? __kthread_init_worker0x70/0x70 [ffffffff817db150] ? gs_change0xb/0xb逐字段解读对照 kernel/locking/lockdep.c 中lockdep_rcu_suspicious()的实现可以确认 splat 的产出顺序先打印WARNING: suspicious RCU usage头与出事的file:line再打印 other info 区块rcu_scheduler_active与debug_locks两个全局状态随后由lockdep_print_held_locks(curr)列出当前进程持有的锁最后dump_stack()输出调用栈。几个值得注意的字段rcu_scheduler_active反映调度器是否已激活 RCU帮助区分告警发生在系统启动早期还是正常运行期debug_lockslockdep 调试是否启用。当前实现中若该值为 0还会追加一行 Possible false positive due to lockdep disabling via debug_locks 0 的提示见 lockdep.c L6908-L6913这也是文档提醒存在误报可能的由来之一held locks 列表这是交叉验证的关键。本例中进程持有 3 把锁#2处的((q-__queue_lock)-rlock){-.-.}表明request_queue的queue_lock自旋锁正在被cfq_exit_queue()持有。花括号中的状态字符描述锁类别是否可递归、获取时硬中断/软中断的开启状态at:之后是该锁最近一次获取点的代码位置调用栈第一帧lockdep_rcu_dereference直接指出检查由哪个宏触发其下方帧__cfq_exit_single_io_context→cfq_exit_queue→elevator_exit→blk_cleanup_queue→ …则还原了触发路径scsi 扫描线程scsi_scan_6在移除设备、清理块队列时走到了这段代码。文档同时给出了触发告警的原始代码——v3.0-rc5 中 block/cfq-iosched.c 的第 2776 行if (rcu_dereference(ioc-ioc_data) cic) {rcu_dereference()这种写法隐含的前提是必须处于最普通的 RCU 读侧临界区但上面的 held-locks 清单表明调用点并不满足该前提——此时持有的是三把普通锁。这就引出了核心问题这三把锁中是否有一把其实可以替代读侧临界区来保护这次读取三种标准修复手法针对上述 splat文档给出了三种互相独立的修法实际排障时可按锁是否真正保护该指针 → 是否真的解引用 → 能否降级为指针比较的顺序选择。修法一改用rcu_dereference_protected()声明由锁保护若 held-locks 中的某把锁确实保护了对该指针的访问正确做法是把这一点告知 RCU把__cfq_exit_single_io_context()改为接受cfq_exit_queue()传下来的struct request_queue *q随后用rcu_dereference_protected()显式声明保护锁if (rcu_dereference_protected(ioc-ioc_data, lockdep_is_held(q-queue_lock)) cic) {做出这一变更后只要代码运行在 RCU 读侧临界区内、或持有-queue_lock时调用lockdep-RCU 就不会再报 splat。具体到本例-queue_lock确实正被持有对应 held-locks 列表中的#2所以此修改会直接消除上面的告警。rcu_dereference_protected()宏定义可参见 rcupdate.h L741。修法二补上 RCU 读侧临界区另一种可能是该指针访问确实需要一个 RCU 读侧临界区。此时临界区必须覆盖rcu_dereference()返回值的使用至少持续到对目标对象引用计数加一为止。文档给出的示例改法rcu_read_lock(); if (rcu_dereference(ioc-ioc_data) cic) { spin_lock(ioc-lock); rcu_assign_pointer(ioc-ioc_data, NULL); spin_unlock(ioc-lock); } rcu_read_unlock();这样rcu_dereference()始终处于读侧临界区之内同样能消除告警。注意临界区要包住对返回指针的后续使用而不是只包住rcu_dereference()这一行本身。修法三降级为rcu_access_pointer()裸访问本例还有一个更细微的事实代码并没有真正解引用rcu_dereference()返回的指针而是将它与cic做地址比较。既然只是读指针值本身、不跟随它访问对象那么该宏可以直接换成不需要任何保护条件的rcu_access_pointer()if (rcu_access_pointer(ioc-ioc_data) cic) {rcu_access_pointer()是三种宏中要求最低的它只要求读取看起来是合法值的指针本身不访问被指向对象、允许读到旧值因此可以在无保护状态下调用宏定义见 rcupdate.h L625。这一改法最轻量是只比较、不解引用场景下的首选。如何选择有锁真正保护该指针且希望 lockdep 继续帮你守护这个不变式 →修法一逻辑上确实需要 RCU 语义例如后续要解引用或需要保证看到新值的发布顺序→修法二只是读取指针值做比较不需要任何并发语义 →修法三。源码级印证splat 的生成路径把告警的产出过程与源码对应起来便于以后阅读新内核版本输出格式可能略有演进的 splatrcu_dereference()等宏在启用调试时进入 lockdep 检查include/linux/rcupdate.h中的宏展开本例第一帧lockdep_rcu_dereference即此阶段检查失败后汇入lockdep_rcu_suspicious()kernel/locking/lockdep.c#L6893它进入nbcon_cpu_emergency_enter()独占控制台用pr_warn()打印 WARNING: suspicious RCU usage、出事文件行号并输出rcu_scheduler_active/debug_locks状态该函数还包含两类附加诊断当前 CPU 处于离线状态时打印 RCU used illegally from offline CPU!处于扩展静止状态extended quiescent state例如空闲态 RCU-free window时打印 RCU used illegally from extended quiescent state!——后者解释了为什么 RCU 在空闲窗口内对读侧锁视而不见详见 lockdep.c L6915-L6934 的注释最后lockdep_print_held_locks(curr)与dump_stack()分别产出 held-locks 清单与调用栈与上文的解读一一对应。小结Lockdep-RCU splat 的本质是rcu_dereference()家族宏的保护前提读侧临界区或更新侧锁在运行时被验证为不成立。排查时抓住三样东西——出事的文件行号、held-locks 清单、调用栈第一帧——即可在三种修法rcu_dereference_protected()声明锁保护、rcu_read_lock()/rcu_read_unlock()补临界区、rcu_access_pointer()降级裸访问中做出正确选择。文档全文见 lockdep-splat.rst相关 RCU 文档位于 Documentation/RCU/ 目录。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考