正确处理和使用 rcu_dereference() 的返回值¶
正确处理和使用地址和数据依赖关系对于正确使用 RCU 等机制至关重要。为此,从 rcu_dereference()
系列原语返回的指针带有地址和数据依赖性。这些依赖性从 rcu_dereference()
宏加载指针开始,延伸到稍后使用该指针计算后续内存访问的地址(表示地址依赖性)或后续内存访问写入的值(表示数据依赖性)。
大多数情况下,这些依赖性都会被保留,允许您自由使用来自 rcu_dereference()
的值。例如,解引用(前缀“*”)、字段选择(“->”)、赋值(“=”)、取地址(“&”)、类型转换以及常数的加减都非常自然和安全地工作。但是,由于当前编译器不考虑地址或数据依赖性,仍然可能出现问题。
请遵循这些规则,以保留来自您对 rcu_dereference()
及其相关函数的调用的地址和数据依赖性,从而使您的 RCU 读取器正常工作
您必须使用
rcu_dereference()
系列原语之一来加载受 RCU 保护的指针,否则 CONFIG_PROVE_RCU 将会报错。更糟糕的是,由于编译器和 DEC Alpha 可以玩的游戏,您的代码可能会看到随机的内存损坏错误。如果没有rcu_dereference()
原语之一,编译器可以重新加载该值,并且您的代码会因为单个指针的两个不同值而遇到麻烦!如果没有rcu_dereference()
,DEC Alpha 可以加载指针,解引用该指针,并返回在指针存储之前的初始化之前的数据。(正如稍后指出的,在最近的内核中,READ_ONCE() 也防止 DEC Alpha 玩这些把戏。)此外,
rcu_dereference()
中的 volatile 强制转换会阻止编译器推断出生成的指针值。请参阅标题为“编译器知道太多的示例”的部分,其中编译器实际上可以推断出指针的确切值,从而导致错误排序。在添加数据但读取器访问该结构时永远不会删除数据的特殊情况下,可以使用 READ_ONCE() 而不是
rcu_dereference()
。在这种情况下,使用 READ_ONCE() 承担了 v4.15 中删除的 lockless_dereference() 原语的角色。您只允许在指针值上使用
rcu_dereference()
。编译器对整数值了解得太多,以至于无法信任它在整数操作中传递依赖性。有极少数例外,即您可以暂时将指针转换为 uintptr_t,以便设置位和清除该指针必须为零的低位。这显然意味着指针必须具有对齐约束,例如,这对于 char* 指针通常不起作用。
XOR 位以转换指针,如一些经典的伙伴分配器算法中所做的那样。
重要的是在对它进行任何其他操作之前将该值转换回指针。
使用 “+” 和 “-” 中缀算术运算符时避免抵消。例如,对于给定的变量 “x”,避免对 char* 指针使用 “(x-(uintptr_t)x)”。编译器有权将零替换为此类表达式,以便后续访问不再依赖于
rcu_dereference()
,这再次可能由于错误排序而导致错误。当然,如果 “p” 是来自
rcu_dereference()
的指针,并且 “a” 和 “b” 是恰好相等的整数,则表达式 “p+a-b” 是安全的,因为它的值仍然必然依赖于rcu_dereference()
,从而保持正确的顺序。如果您使用 RCU 来保护 JIT 编译的函数,以便将 “()” 函数调用运算符应用于从
rcu_dereference()
获取(直接或间接)的值,则您可能需要直接与硬件交互以刷新指令缓存。当新 JIT 编译的函数使用先前 JIT 编译的函数使用的相同内存时,就会在某些系统上出现此问题。解引用时不要使用关系运算符(“==”、“!=”、“>”、“>=”、“<” 或 “<=”)的结果。例如,以下(非常奇怪的)代码是有错误的
int *p; int *q; ... p = rcu_dereference(gp) q = &global_q; q += p > &oom_p; r1 = *q; /* BUGGY!!! */
和以前一样,这个有错误的原因是关系运算符通常使用分支编译的。和以前一样,虽然诸如 ARM 或 PowerPC 之类的弱内存机器在这些分支之后对存储进行排序,但是可以推测加载,这可能会再次导致错误排序错误。
在将从
rcu_dereference()
获取的指针与非 NULL 值进行比较时要非常小心。正如 Linus Torvalds 解释的那样,如果两个指针相等,则编译器可以将您正在比较的指针替换为从rcu_dereference()
获取的指针。例如p = rcu_dereference(gp); if (p == &default_struct) do_default(p->a);
因为编译器现在知道 “p” 的值正好是变量 “default_struct” 的地址,所以它可以自由地将此代码转换为以下代码
p = rcu_dereference(gp); if (p == &default_struct) do_default(default_struct.a);
在 ARM 和 Power 硬件上,现在可以推测从 “default_struct.a” 的加载,因此它可能发生在
rcu_dereference()
之前。这可能导致由于错误排序而导致的错误。但是,在以下情况下,比较是可以的
比较是针对 NULL 指针进行的。如果编译器知道该指针为 NULL,您最好不要解引用它。如果比较不相等,则编译器不会更聪明。因此,将来自
rcu_dereference()
的指针与 NULL 指针进行比较是安全的。指针在比较后永远不会被解引用。由于没有后续的解引用,编译器不能使用它从比较中学到的任何东西来重新排序不存在的后续解引用。在扫描受 RCU 保护的循环链接列表时,经常会出现这种比较。
请注意,如果在 RCU 读取端临界区之外完成指针比较,并且永远不会解引用该指针,则应使用
rcu_access_pointer()
代替rcu_dereference()
。在大多数情况下,最好直接测试rcu_access_pointer()
的返回值,而无需将其分配给变量,以避免意外的解引用。在 RCU 读取端临界区内,没有理由使用
rcu_access_pointer()
。比较是针对引用“很久以前”初始化的内存的指针进行的。这是安全的原因是,即使发生错误排序,错误排序也不会影响比较之后的访问。那么“很久以前”到底有多久?以下是一些可能性
编译时。
启动时。
模块代码的模块初始化时间。
kthread 代码在 kthread 创建之前。
在之前获取我们现在持有的锁期间。
对于计时器处理程序,在
mod_timer()
时间之前。
还有许多其他可能性涉及 Linux 内核的各种原语,这些原语导致代码在稍后时间被调用。
被比较的指针也来自
rcu_dereference()
。在这种情况下,两个指针都依赖于一个rcu_dereference()
或另一个,所以无论哪种方式,您都会得到正确的排序。也就是说,这种情况可能会使某些 RCU 使用错误更容易发生。至少在测试期间发生的话,这可能是一件好事。在标题为“放大的 RCU 使用错误的示例”的部分中显示了这种 RCU 使用错误的示例。
比较之后的所有访问都是存储,因此控制依赖性会保留所需的排序。也就是说,很容易弄错控制依赖性。有关更多详细信息,请参阅 Documentation/memory-barriers.txt 的“控制依赖性”部分。
指针不相等且编译器没有足够的信息来推断指针的值。请注意,
rcu_dereference()
中的 volatile 强制类型转换通常会阻止编译器知道太多信息。但是,请注意,如果编译器知道指针只取两个值中的一个,那么不相等比较将为编译器提供推断指针值所需的精确信息。
禁用编译器可能提供的任何值推测优化,特别是当您使用从先前运行收集的数据进行反馈优化的时侯。此类值推测优化会按设计重新排序操作。
此规则有一个例外:利用分支预测硬件的值推测优化在强排序系统(例如 x86)上是安全的,但在弱排序系统(例如 ARM 或 Power)上则不安全。请明智地选择您的编译器命令行选项!
放大的 RCU 使用错误的示例¶
由于更新程序可以与 RCU 读取器并发运行,因此 RCU 读取器可能会看到过时和/或不一致的值。如果 RCU 读取器需要新的或一致的值(有时确实如此),则需要采取适当的预防措施。要了解这一点,请考虑以下代码片段
struct foo {
int a;
int b;
int c;
};
struct foo *gp1;
struct foo *gp2;
void updater(void)
{
struct foo *p;
p = kmalloc(...);
if (p == NULL)
deal_with_it();
p->a = 42; /* Each field in its own cache line. */
p->b = 43;
p->c = 44;
rcu_assign_pointer(gp1, p);
p->b = 143;
p->c = 144;
rcu_assign_pointer(gp2, p);
}
void reader(void)
{
struct foo *p;
struct foo *q;
int r1, r2;
rcu_read_lock();
p = rcu_dereference(gp2);
if (p == NULL)
return;
r1 = p->b; /* Guaranteed to get 143. */
q = rcu_dereference(gp1); /* Guaranteed non-NULL. */
if (p == q) {
/* The compiler decides that q->c is same as p->c. */
r2 = p->c; /* Could get 44 on weakly order system. */
} else {
r2 = p->c - r1; /* Unconditional access to p->c. */
}
rcu_read_unlock();
do_something_with(r1, r2);
}
您可能会惊讶于结果(r1 == 143 && r2 == 44)是可能的,但您不应该感到惊讶。毕竟,更新程序可能在 reader() 加载到 “r1” 和加载到 “r2” 之间被第二次调用。事实上,由于编译器和 CPU 的一些重新排序也可能发生相同的结果,但这并不是重点。
但是,如果读取器需要一致的视图呢?
那么一种方法是使用锁,例如,如下所示
struct foo {
int a;
int b;
int c;
spinlock_t lock;
};
struct foo *gp1;
struct foo *gp2;
void updater(void)
{
struct foo *p;
p = kmalloc(...);
if (p == NULL)
deal_with_it();
spin_lock(&p->lock);
p->a = 42; /* Each field in its own cache line. */
p->b = 43;
p->c = 44;
spin_unlock(&p->lock);
rcu_assign_pointer(gp1, p);
spin_lock(&p->lock);
p->b = 143;
p->c = 144;
spin_unlock(&p->lock);
rcu_assign_pointer(gp2, p);
}
void reader(void)
{
struct foo *p;
struct foo *q;
int r1, r2;
rcu_read_lock();
p = rcu_dereference(gp2);
if (p == NULL)
return;
spin_lock(&p->lock);
r1 = p->b; /* Guaranteed to get 143. */
q = rcu_dereference(gp1); /* Guaranteed non-NULL. */
if (p == q) {
/* The compiler decides that q->c is same as p->c. */
r2 = p->c; /* Locking guarantees r2 == 144. */
} else {
spin_lock(&q->lock);
r2 = q->c - r1;
spin_unlock(&q->lock);
}
rcu_read_unlock();
spin_unlock(&p->lock);
do_something_with(r1, r2);
}
一如既往,为工作选择合适的工具!
编译器了解太多的示例¶
如果从 rcu_dereference()
获取的指针与某些其他指针比较不相等,则编译器通常不知道第一个指针的值可能是什么。这种知识的缺乏阻止编译器执行否则可能会破坏 RCU 所依赖的排序保证的优化。并且 rcu_dereference()
中的 volatile 强制类型转换应阻止编译器猜测该值。
但是如果没有 rcu_dereference()
,编译器知道的比您预期的要多。请考虑以下代码片段
struct foo {
int a;
int b;
};
static struct foo variable1;
static struct foo variable2;
static struct foo *gp = &variable1;
void updater(void)
{
initialize_foo(&variable2);
rcu_assign_pointer(gp, &variable2);
/*
* The above is the only store to gp in this translation unit,
* and the address of gp is not exported in any way.
*/
}
int reader(void)
{
struct foo *p;
p = gp;
barrier();
if (p == &variable1)
return p->a; /* Must be variable1.a. */
else
return p->b; /* Must be variable2.b. */
}
因为编译器可以看到对“gp”的所有存储,它知道“gp”的唯一可能值是“variable1”和“variable2”。因此,reader() 中的比较即使在不相等的情况下也会告诉编译器“p”的精确值。这允许编译器使返回值独立于从“gp”加载,反过来破坏此加载和返回值加载之间的顺序。这可能会导致在弱排序系统上,“p->b”返回初始化前的垃圾值。
简而言之,当您要取消引用生成的指针时,rcu_dereference()
不是可选的。
您应该使用 rcu_dereference() 系列的哪个成员?¶
首先,请避免使用 rcu_dereference_raw(),也请避免使用 rcu_dereference_check()
和 rcu_dereference_protected()
,第二个参数的常量值为 1(或为真)。有了这个注意事项,这里有一些关于在各种情况下使用 rcu_dereference()
的哪个成员的指导。
如果访问需要在 RCU 读取侧临界区内,请使用
rcu_dereference()
。使用新的合并 RCU 类型,使用rcu_read_lock()
、任何禁用下半部分的内容、任何禁用中断的内容或任何禁用抢占的内容来进入 RCU 读取侧临界区。请注意,自旋锁临界区也是隐含的 RCU 读取侧临界区,即使它们是可抢占的,就像使用 CONFIG_PREEMPT_RT=y 构建的内核中一样。如果访问可能一方面在 RCU 读取侧临界区内,另一方面受(例如)my_lock 的保护,请使用
rcu_dereference_check()
,例如p1 = rcu_dereference_check(p->rcu_protected_pointer, lockdep_is_held(&my_lock));
如果访问可能一方面在 RCU 读取侧临界区内,另一方面受 my_lock 或 your_lock 的保护,请再次使用
rcu_dereference_check()
,例如p1 = rcu_dereference_check(p->rcu_protected_pointer, lockdep_is_held(&my_lock) || lockdep_is_held(&your_lock));
如果访问在更新侧,因此始终受 my_lock 的保护,请使用
rcu_dereference_protected()
p1 = rcu_dereference_protected(p->rcu_protected_pointer, lockdep_is_held(&my_lock));
这可以扩展为处理如上文 #3 中的多个锁,两者都可以扩展为检查其他条件。
如果保护由调用者提供,因此此代码未知,那么这是 rcu_dereference_raw() 适用的罕见情况。此外,当 lockdep 表达式过于复杂时,rcu_dereference_raw() 可能是合适的,除非在那种情况下更好的方法可能是仔细研究你的同步设计。尽管如此,在数据锁定的情况下,大量锁或引用计数器中的任何一个都足以保护指针,因此 rcu_dereference_raw() 确实有其用武之地。
但是,考虑到当前内核中的使用数量,它的位置可能比人们预期的要小得多。其同义词 rcu_dereference_check( ... , 1) 和其近亲 rcu_dereference_protected(... , 1) 也是如此。
对 RCU 保护指针的稀疏检查¶
稀疏静态分析工具会检查对 RCU 保护的指针的非 RCU 访问,这可能会由于涉及发明加载的编译器优化以及可能存在的加载撕裂而导致“有趣的”错误。例如,假设有人错误地执行了类似以下的操作
p = q->rcu_protected_pointer;
do_something_with(p->a);
do_something_else_with(p->b);
如果寄存器压力很高,编译器可能会优化掉 “p”,将代码转换为类似以下内容
do_something_with(q->rcu_protected_pointer->a);
do_something_else_with(q->rcu_protected_pointer->b);
如果 q->rcu_protected_pointer 在此期间发生更改,这可能会让您的代码严重失望。这也不是一个理论问题:早在 1990 年代初,正是这种错误让 Paul E. McKenney(以及他的几位无辜同事)损失了三天的周末。
加载撕裂当然会导致取消引用一对指针的混合,这也可能会让您的代码严重失望。
只需使代码按如下方式读取即可避免这些问题
p = rcu_dereference(q->rcu_protected_pointer);
do_something_with(p->a);
do_something_else_with(p->b);
不幸的是,在审查期间,这些类型的错误可能非常难以发现。这就是稀疏工具发挥作用的地方,以及 “__rcu” 标记。如果使用 “__rcu” 标记指针声明(无论是在结构中还是作为形式参数),则会告诉稀疏工具,如果直接访问此指针,则会发出警告。如果使用 rcu_dereference()
及其朋友访问未标记 “__rcu” 的指针,它也会使稀疏工具发出警告。例如,->rcu_protected_pointer 可以声明如下
struct foo __rcu *rcu_protected_pointer;
“__rcu” 的使用是选择性的。如果您选择不使用它,则应忽略稀疏警告。