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。