Lockdep-RCU Splat

Lockdep-RCU 于 2010 年初添加到 Linux 内核中 (http://lwn.net/Articles/371986/)。此工具检查 RCU API 的一些常见误用,最值得注意的是在没有适当保护的情况下,使用 rcu_dereference() 系列访问 RCU 保护的指针。当检测到此类误用时,会发出 lockdep-RCU splat。

lockdep-RCU splat 的常见原因是某人在没有 (1) 处于正确类型的 RCU 读取侧临界区或 (2) 持有正确的更新侧锁的情况下访问 RCU 保护的数据结构。 因此,这个问题可能很严重:它可能导致随机内存覆盖或更糟。当然,可能存在误报,这就是现实世界。

因此,让我们看一个来自 3.0-rc5 的 RCU lockdep splat 示例,该示例早已修复

=============================
WARNING: suspicious RCU usage
-----------------------------
block/cfq-iosched.c:2776 suspicious rcu_dereference_protected() usage!

可能有助于我们调试的其他信息

rcu_scheduler_active = 1, debug_locks = 0
3 locks held by scsi_scan_6/1552:
#0:  (&shost->scan_mutex){+.+.}, at: [<ffffffff8145efca>]
scsi_scan_host_selected+0x5a/0x150
#1:  (&eq->sysfs_lock){+.+.}, at: [<ffffffff812a5032>]
elevator_exit+0x22/0x60
#2:  (&(&q->__queue_lock)->rlock){-.-.}, at: [<ffffffff812b6233>]
cfq_exit_queue+0x43/0x190

stack backtrace:
Pid: 1552, comm: scsi_scan_6 Not tainted 3.0.0-rc5 #17
Call Trace:
[<ffffffff810abb9b>] lockdep_rcu_dereference+0xbb/0xc0
[<ffffffff812b6139>] __cfq_exit_single_io_context+0xe9/0x120
[<ffffffff812b626c>] cfq_exit_queue+0x7c/0x190
[<ffffffff812a5046>] elevator_exit+0x36/0x60
[<ffffffff812a802a>] blk_cleanup_queue+0x4a/0x60
[<ffffffff8145cc09>] scsi_free_queue+0x9/0x10
[<ffffffff81460944>] __scsi_remove_device+0x84/0xd0
[<ffffffff8145dca3>] scsi_probe_and_add_lun+0x353/0xb10
[<ffffffff817da069>] ? error_exit+0x29/0xb0
[<ffffffff817d98ed>] ? _raw_spin_unlock_irqrestore+0x3d/0x80
[<ffffffff8145e722>] __scsi_scan_target+0x112/0x680
[<ffffffff812c690d>] ? trace_hardirqs_off_thunk+0x3a/0x3c
[<ffffffff817da069>] ? error_exit+0x29/0xb0
[<ffffffff812bcc60>] ? kobject_del+0x40/0x40
[<ffffffff8145ed16>] scsi_scan_channel+0x86/0xb0
[<ffffffff8145f0b0>] scsi_scan_host_selected+0x140/0x150
[<ffffffff8145f149>] do_scsi_scan_host+0x89/0x90
[<ffffffff8145f170>] do_scan_async+0x20/0x160
[<ffffffff8145f150>] ? do_scsi_scan_host+0x90/0x90
[<ffffffff810975b6>] kthread+0xa6/0xb0
[<ffffffff817db154>] kernel_thread_helper+0x4/0x10
[<ffffffff81066430>] ? finish_task_switch+0x80/0x110
[<ffffffff817d9c04>] ? retint_restore_args+0xe/0xe
[<ffffffff81097510>] ? __kthread_init_worker+0x70/0x70
[<ffffffff817db150>] ? gs_change+0xb/0xb

v3.0-rc5 中 block/cfq-iosched.c 的第 2776 行如下

if (rcu_dereference(ioc->ioc_data) == cic) {

这种形式表示它必须在一个普通的 vanilla RCU 读取侧临界区中,但上面的“其他信息”列表表明情况并非如此。相反,我们持有三个锁,其中一个可能与 RCU 相关。也许该锁确实保护了该引用。如果是这样,修复方法是通知 RCU,也许可以通过更改 __cfq_exit_single_io_context() 以将 struct request_queue “q” 作为参数从 cfq_exit_queue() 中获取,这将允许我们按如下方式调用 rcu_dereference_protected

if (rcu_dereference_protected(ioc->ioc_data,
                              lockdep_is_held(&q->queue_lock)) == cic) {

通过此更改,如果从 RCU 读取侧临界区内或持有 ->queue_lock 的情况下调用此代码,则不会发出 lockdep-RCU splat。 特别是,这将抑制上面的 lockdep-RCU splat,因为 ->queue_lock 已被持有(参见上面列表中的 #2)。

另一方面,也许我们确实需要一个 RCU 读取侧临界区。 在这种情况下,临界区必须跨越 rcu_dereference() 返回值的使用,或者至少直到引用计数增加或类似情况。 一种处理方法是添加 rcu_read_lock()rcu_read_unlock() 如下

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 读取侧临界区内,这将再次抑制上面的 lockdep-RCU splat。

但在这种特殊情况下,我们实际上并没有解引用从 rcu_dereference() 返回的指针。 相反,该指针只是与 cic 指针进行比较,这意味着 rcu_dereference() 可以被 rcu_access_pointer() 替换,如下所示

if (rcu_access_pointer(ioc->ioc_data) == cic) {

因为在没有保护的情况下调用 rcu_access_pointer() 是合法的,所以此更改也会抑制上面的 lockdep-RCU splat。