Lockdep-RCU Splat

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

lockdep-RCU splat 的常见原因是有人在没有以下两种情况之一时访问了受 RCU 保护的数据结构:(1)处于正确的 RCU 读取侧临界区内,或者(2)持有正确的更新侧锁。因此,这个问题可能很严重:它可能导致随机的内存覆盖或更糟糕的情况。当然,也可能出现误报,毕竟这是真实的世界。

让我们来看一个 3.0-rc5 中的 RCU lockdep splat 示例,该 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) {

这种形式表示它必须处于普通的 RCU 读取侧临界区中,但上面的“其他信息”列表表明情况并非如此。相反,我们持有三个锁,其中一个可能与 RCU 相关。也许这个锁确实保护了这个引用。如果是这样,修复方法是通知 RCU,或许可以通过将 struct request_queue “q” 作为参数传递给 __cfq_exit_single_io_context() 从 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。