RCU 和 lockdep 检查

所有类型的 RCU 都提供 lockdep 检查,以便 lockdep 知道每个任务何时进入和离开任何类型的 RCU 读取端临界区。每种类型的 RCU 都会被单独跟踪(但请注意,在 2.6.32 及更早版本中并非如此)。这使得 lockdep 的跟踪可以包含 RCU 状态,这有时可以在调试死锁等问题时提供帮助。

此外,RCU 还提供了以下检查 lockdep 状态的原语

rcu_read_lock_held() for normal RCU.
rcu_read_lock_bh_held() for RCU-bh.
rcu_read_lock_sched_held() for RCU-sched.
rcu_read_lock_any_held() for any of normal RCU, RCU-bh, and RCU-sched.
srcu_read_lock_held() for SRCU.
rcu_read_lock_trace_held() for RCU Tasks Trace.

这些函数是保守的,因此如果不确定,它们将返回 1(例如,如果未设置 CONFIG_DEBUG_LOCK_ALLOC)。这可以防止诸如 WARN_ON(!rcu_read_lock_held()) 之类的代码在禁用 lockdep 时给出误报。

此外,单独的内核配置参数 CONFIG_PROVE_RCU 启用对 rcu_dereference() 原语的检查

rcu_dereference(p)

检查 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 读取器和更新器调用的代码中很有用。

rcu_dereference_bh_check(p, c)

将显式检查表达式 “c” 与 rcu_read_lock_bh_held() 一起使用。这在 RCU-bh 读取器和更新器调用的代码中很有用。

rcu_dereference_sched_check(p, c)

将显式检查表达式 “c” 与 rcu_read_lock_sched_held() 一起使用。这在 RCU-sched 读取器和更新器调用的代码中很有用。

srcu_dereference_check(p, c)

将显式检查表达式 “c” 与 srcu_read_lock_held() 一起使用。这在 SRCU 读取器和更新器调用的代码中很有用。

rcu_dereference_raw(p)

不检查。(请谨慎使用,如果可能的话尽量不要使用。)

rcu_dereference_raw_check(p)

根本不做 lockdep。(请谨慎使用,如果可能的话尽量不要使用。)

rcu_dereference_protected(p, c)

使用显式检查表达式 “c”,并省略所有屏障和编译器约束。当数据结构无法更改时,例如,在仅由更新程序调用的代码中,这很有用。

rcu_access_pointer(p)

返回指针的值并省略所有屏障,但保留防止重复或合并的编译器约束。这在测试指针本身的值时很有用,例如,与 NULL 比较。

rcu_dereference_check() 检查表达式可以是任何布尔表达式,但通常会包含 lockdep 表达式。对于一个适度复杂的示例,请考虑以下情况

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,则验证此表达式是否在以下情况下使用:

  1. RCU 读取端临界区(隐式),或者

  2. 持有 files->file_lock,或者

  3. 在未共享的 files_struct 上。

在情况 (1) 中,指针以 RCU 安全的方式为普通 RCU 读取端临界区获取,在情况 (2) 中,->file_lock 防止任何更改发生,最后,在情况 (3) 中,当前任务是唯一访问 file_struct 的任务,再次防止任何更改发生。如果上述语句仅从更新器代码中调用,则可以改为编写如下:

file = rcu_dereference_protected(fdt->fd[fd],
                                 lockdep_is_held(&files->file_lock) ||
                                 atomic_read(&files->count) == 1);

这将验证上述情况 #2 和 #3,并且,即使在 RCU 读取端临界区中使用了此语句,lockdep 也会报错,除非满足这两种情况中的一种。因为 rcu_dereference_protected() 省略了所有屏障和编译器约束,因此它比其他类型的 rcu_dereference() 生成更好的代码。另一方面,如果受 RCU 保护的指针或它指向的受 RCU 保护的数据可以并发更改,则使用 rcu_dereference_protected() 是非法的。

rcu_dereference() 类似,当启用 lockdep 时,RCU 列表和 hlist 遍历原语会检查是否从 RCU 读取端临界区内调用。但是,可以将 lockdep 表达式作为额外的可选参数传递给它们。有了这个 lockdep 表达式,这些遍历原语只会在 lockdep 表达式为 false 并且它们是从任何 RCU 读取端临界区之外调用的情况下报错。

例如,工作队列 for_each_pwq() 宏旨在 RCU 读取端临界区内或持有 wq->mutex 的情况下使用。因此,它的实现如下:

#define for_each_pwq(pwq, wq)
        list_for_each_entry_rcu((pwq), &(wq)->pwqs, pwqs_node,
                                lock_is_held(&(wq->mutex).dep_map))