WRITE_ONCE 在 linux 内核列表中

WRITE_ONCE in linux kernel lists

我在看双链表的linux kernel implementation。我不明白宏WRITE_ONCE(x, val)的用法。在compiler.h中定义如下:

#define WRITE_ONCE(x, val) x=(val)

在文件中使用了七次,如

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);
}

我读到它用于避免竞争条件。

我有两个问题:
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;
}

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;
}

编辑:
这是有关此文件和 WRITE_ONCE 的提交消息,但它对我理解任何内容都没有帮助...

list: Use WRITE_ONCE() when initializing list_head structures
Code that does lockless emptiness testing of non-RCU lists is relying on INIT_LIST_HEAD() to write the list head's ->next pointer atomically, particularly when INIT_LIST_HEAD() is invoked from list_del_init(). This commit therefore adds WRITE_ONCE() to this function's pointer stores that could affect the head's ->next pointer.

您引用的第一个定义是 kernel lock validator 的一部分,又名“lockdep”。 WRITE_ONCE(和其他人)不需要特殊处理,但原因是另一个问题的主题。

相关的定义是 here,一个非常简洁的注释表明它们的目的是:

Prevent the compiler from merging or refetching reads or writes.

...

Ensuring that the compiler does not fold, spindle, or otherwise mutilate accesses that either do not require ordering or that interact with an explicit memory barrier or atomic instruction that provides the required ordering.

但是这些话是什么意思?


问题

问题实际上是复数:

  1. Read/write “撕裂”:用许多较小的内存访问替换单个内存访问。 GCC 可能(并且确实!)在某些情况下用两个 16 位 store-immediate 指令替换 p = 0x01020304; 之类的东西——而不是假设将常量放在寄存器中,然后进行内存访问,等等。 WRITE_ONCE 将允许我们对 GCC 说,“不要那样做”,就像这样:WRITE_ONCE(p, 0x01020304);

  2. C 编译器已停止保证字访问是原子的。任何 non-race-free 的程序都可以 miscompiled 并产生惊人的结果。不仅如此,编译器可能会决定 将某些值保留在循环内的寄存器中,从而导致多个引用可能会像这样弄乱代码:

    for(;;) {
        owner = lock->owner;
        if (owner && !mutex_spin_on_owner(lock, owner))
            break;
        /* ... */
    }
  1. 在没有“标记”共享内存访问的情况下,我们无法自动检测此类无意访问。试图 find such bugs 的自动化工具无法将它们与故意的不正当访问区分开来。

解决方案

我们首先注意到 Linux 内核需要使用 GCC 构建。因此,我们只需要一个编译器来解决这个问题,我们可以使用它的 documentation 作为唯一的指南。

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

对于 1、2、4 和 8 字节的大小,有适当的类型,并且 volatile 特别禁止 GCC 应用我们在 (1) 中提到的优化,以及照顾other cases (last bullet point under "COMPILER BARRIERS"). It also disallows GCC of miscompiling the loop in (2), because it would move the volatile access across a sequence point, and that's disallowed by the C standard. Linux uses what we call a "volatile access" (see below) instead of tagging an object as volatile. We could solve our problem by marking the specific object as volatile, but this is (almost?) never a good choice. There are many reasons 它可能有害。

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

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

假设我们不知道确切地 volatile 做什么 - 并找出 isn't easy! (查看#5) - 另一种方法来完成这个将放置内存屏障:如果大小不是 1、2、4 或 8,这正是 Linux 所做的,求助于 memcpy 并在 [=57= 之前放置内存屏障]and 调用后。内存障碍也很容易解决问题 (2),但会导致很大的性能损失。

我希望我已经介绍了一个概述,没有深入研究 C 标准的解释,但如果您愿意,我可以花时间去做。