正确处理和使用 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 位来转换指针,就像在一些经典的 buddy-allocator 算法中所做的那样。
在用它做任何其他事情之前,将值强制转换回指针非常重要。
使用“+”和“-”中缀算术运算符时,避免取消。例如,对于给定的变量“x”,对于 char* 指针,避免使用“(x-(uintptr_t)x)”。编译器有权用零替换此类表达式,因此后续访问不再依赖于
rcu_dereference()
,同样可能由于错误排序而导致错误。当然,如果“p”是来自
rcu_dereference()
的指针,并且“a”和“b”是恰好相等的整数,则表达式“p+a-b”是安全的,因为它的值仍然必须依赖于rcu_dereference()
,从而保持正确的顺序。如果您使用 RCU 来保护 JITed 函数,以便将“()”函数调用运算符应用于从
rcu_dereference()
(直接或间接)获得的值,则可能需要直接与硬件交互以刷新指令缓存。当新 JITed 函数使用先前 JITed 函数使用的相同内存时,某些系统会出现此问题。解引用时不要使用关系运算符(“==”、“!=”、“>”、“>=”、“<”或“<=”)的结果。例如,以下(非常奇怪的)代码有错误
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(或者就此而言,为 true)。在排除这些注意事项之后,以下是一些关于在各种情况下使用哪个 rcu_dereference()
成员的指导
如果访问需要在 RCU 读取端临界区内,请使用
rcu_dereference()
。使用新的统一 RCU 风格,使用rcu_read_lock()
、禁用 bottom halves 的任何操作、禁用中断的任何操作或禁用抢占的任何操作来进入 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”的使用是选择性的。如果您选择不使用它,则应忽略稀疏警告。