c - Linux内核列表中的WRITE_ONCE

标签 c linux macros race-condition

我正在阅读 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 的一部分。 ,又名“锁定”。 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. 读/写“撕裂”:用许多较小的内存访问替换单个内存访问。 GCC 可能(并且确实!)在某些情况下用两个 16 位立即存储指令替换 p = 0x01020304; 之类的东西 - 而不是可能将常量放在寄存器中然后进行内存访问,并且等等。 WRITE_ONCE 可以让我们对 GCC 说“不要那样做”,就像这样:WRITE_ONCE(p, 0x01020304);

  2. C 编译器已停止保证字访问是原子的。任何非种族无关的程序都可以是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 (“编译器障碍”下的最后一个要点)。它还禁止 GCC 错误编译 (2) 中的循环,因为它会将 volatile 访问移动到一个序列点,这是 C 标准所不允许的。 Linux uses我们称之为“ volatile 访问”(见下文),而不是将对象标记为 volatile 。我们可以通过将特定对象标记为 volatile 来解决我们的问题,但这(几乎?)绝不是一个好的选择。有many reasons这可能是有害的。

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

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

假设我们不知道 确切 volatile 做了什么 - 并找出 isn't easy! (查看#5) - 另一种实现此目的的方法是放置内存屏障:这正是 Linux 在大小不是 1、2、4 或 8 时所做的,诉诸 memcpy 并在调用之后在 之前放置内存屏障。内存屏障也很容易解决问题 (2),但会导致很大的性能损失。

我希望我已经涵盖了一个概述,而不深入研究 C 标准的解释,但如果您愿意,我可以花时间来做。

关于c - Linux内核列表中的WRITE_ONCE,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34988277/

相关文章:

c - 发生错误时,scanf 返回 1 而不是 0

linux - 如何限制 tabnine 的内存使用量?

php - 我尝试在我的浏览器本地主机中打开一个 php 文件,但每次我双击它时它都会让我保存它

c - 从 C 文件中读取 64 位十六进制值

c - 在c中 float 奇怪的不精确错误

c - 有没有比使用 sscanf 更好的方法来实现这个结果?

Linux 日期 : invalid date

c++ - 模板化类型宏参数中的括号,我不能使用可变参数宏

c++ - 为什么 token 连接在 C++ 中不起作用?

function - emacs 中有 apply-function-to-region-lines 吗?