Linux内核列表中的WRITE_ONCE

Gau*_*aut 34 c linux macros race-condition

我正在阅读doubled链表的linux内核实现.我不明白宏的用法WRITE_ONCE(x, val).它在compiler.h中定义如下:

#define WRITE_ONCE(x, val) x=(val)
Run Code Online (Sandbox Code Playgroud)

它在文件中使用了七次,例如

static inline void __list_add(struct list_head *new,
                  struct list_head *prev,
                  struct list_head *next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    WRITE_ONCE(prev->next, new);
}
Run Code Online (Sandbox Code Playgroud)

我读过它用于避免竞争条件.

我有两个问题:
1 /我认为宏在编译时被代码替换.那么这段代码与以下代码有何不同?这个宏如何避免竞争条件?

static inline void __list_add(struct list_head *new,
                  struct list_head *prev,
                  struct list_head *next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    prev->next = new;
}
Run Code Online (Sandbox Code Playgroud)

2 /如何知道何时应该使用它?例如,它用于__lst_add()但不用于__lst_splice():

static inline void __list_splice(const struct list_head *list,
                 struct list_head *prev,
                 struct list_head *next)
{
    struct list_head *first = list->next;
    struct list_head *last = list->prev;

    first->prev = prev;
    prev->next = first;

    last->next = next;
    next->prev = last;
}
Run Code Online (Sandbox Code Playgroud)

编辑:
这是一个关于这个文件的提交消息WRITE_ONCE,但它不能帮助我理解任何事情......

list:初始化list_head结构时使用WRITE_ONCE()执行
非RCU列表无锁空虚测试的代码依赖于INIT_LIST_HEAD()以原子方式写入列表头 - > next指针,特别是当从list_del_init()调用INIT_LIST_HEAD()时.因此,此提交将WRITE_ONCE()添加到此函数的指针存储中,这可能会影响头部 - >下一个指针.

Mic*_*kis 28

您引用的第一个定义是内核锁定验证器的一部分,即"lockdep".WRITE_ONCE(和其他人)不需要特殊待遇,但其原因是另一个问题的主题.

相关的定义将在这里,一个非常简洁的评论说明他们的目的是:

防止编译器合并或重新读取读取或写入.

...

确保编译器不折叠,主轴或以其他方式破坏不需要排序或与显式内存屏障或提供所需排序的原子指令交互的访问.

但这些词是什么意思呢?


问题

问题实际上是复数:

  1. 读/写"撕裂":用许多较小的内存访问替换单个内存访问.GCC可能(并且确实!)在某些情况下p = 0x01020304;用两个16位存储立即指令替换类似的东西- 而不是假设将常量放在寄存器中然后存储器访问,等等.WRITE_ONCE允许我们对海湾合作委员会说"不要这样做",如下:WRITE_ONCE(p, 0x01020304);

  2. C编译器已停止保证单词访问是原子的.任何非种族的程序都可能被错误编译,结果非常棒.不仅如此,编译器可能决定不在循环内的寄存器中保留某些值,从而导致多个引用可能会使代码陷入困境:

    for(;;) {
        owner = lock->owner;
        if (owner && !mutex_spin_on_owner(lock, owner))
            break;
        /* ... */
    }
  1. 在没有对共享内存进行"标记"访问的情况下,我们无法自动检测该类型的非预期访问.试图找到这种错误的自动化工具无法将它们与故意的有趣访问区分开来.

解决方案

我们首先注意到Linux内核需要使用GCC构建.因此,我们只需要一个编译器来处理解决方案,我们可以将其文档作为唯一指南.

对于通用解决方案,我们需要处理所有大小的内存访问.我们有各种类型的特定宽度,以及其他一切.我们还注意到,我们不需要专门标记已经在关键部分的内存访问(为什么不呢?).

对于1,2,4和8字节的大小,有适当的类型,volatile特别是不允许GCC应用我们在(1)中提到的优化,以及处理其他情况("编译器障碍" 下的最后一点) ").它也不允许GCC在(2)中错误编译循环,因为它会volatile在序列点上移动访问,而C标准不允许这样做.Linux 使用我们称之为"易失性访问"(见下文)而不是将对象标记为volatile.我们可以通过将特定对象标记为解决我们的问题volatile,但这(几乎?)永远不是一个好的选择.它有很多 可能有害的原因.

这是在内核中为8位宽类型实现易失性(写入)访问的方式:

*(volatile  __u8_alias_t *) p = *(__u8_alias_t  *) res;

假设我们并不确切知道什么volatile- 并且发现并不容易!(查看#5) - 实现这一目标的另一种方法是设置内存障碍:这正是Linux所做的,以防大小不是1,2,4或8,在使用memcpy和放置内存障碍之前通话结束后.内存障碍也很容易解决问题(2),但会产生很大的性能损失.

我希望我已经涵盖了一个概述而没有深入研究C标准的解释,但如果你愿意,我可以花时间去做.