我正在读双倍链表的linux kernel implementation页.我不懂宏WRITE_ONCE(x, val)的用法.它在编译器中的定义如下.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);
}

我已经读到它是用来避免比赛条件的.

我有两个问题:

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

编辑:

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,一条非常简洁的 comments 指出其目的是:

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

...

确保编译器不会折叠、旋转或以其他方式 destruct 不需要排序或与提供所需排序的显式内存屏障或原子指令交互的访问.

但这些话是什么意思?


The problem

问题其实是复数的:

  1. 读/写"撕裂":用许多较小的内存访问替换单个内存访问.GCC可能(而且确实如此!)在某些情况下,用两个16位存储即时指令替换p = 0x0WRITE_ONCE(p, 0x01020304);0304;之类的内容,而不是假定将常量放入寄存器,然后再进行内存访问,等等.WRITE_ONCE将允许我们对GCC说,"不要那样做",比如:WRITE_ONCE(p, 0x01020304);

  2. C编译器已经不再保证单词访问是原子的.任何非种族自由的项目都可以达到miscompiled,并取得惊人的成绩.不仅如此,编译器还可能决定将某些值保留在循环内的寄存器中,从而导致多个引用,从而导致代码混乱,如下所示:

    for(;;) {
        owner = lock->owner;
        if (owner && !mutex_spin_on_owner(lock, owner))
            break;
        /* ... */
    }
  1. 在没有对共享内存进行"标记"访问的情况下,我们cannot会自动检测此类意外访问.try find such bugs次访问的自动化工具无法将它们与故意的快速访问区分开来.

The solution

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

对于通用解决方案,我们需要处理各种大小的内存访问.我们有各种各样的特定宽度,以及其他一切.我们还注意到,我们不需要特别标记已经在关键部分(why not?)中的内存访问.

对于1、2、4和8字节的大小,有适当的类型,volatile特别禁止GCC应用我们在(1)中提到的优化,以及other cases(编译器障碍下的最后一点).它还不允许GCC错误地编译(2)中的循环,因为它会在一个序列点上移动volatile个访问,这是C标准所不允许的.Linux uses我们称之为"易失性访问"(见下文),而不是将对象标记为易失性.我们通过将特定对象标记为volatile来解决问题,但这是(几乎?)从来都不是个好 Select .有many reasons种可能是有害的.

这就是如何在内核中为8位宽类型实现易失性(写)访问:

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

假设我们不知道exactly volatile做了什么——并且发现了isn't easy!(判断#5)——实现这一点的另一种方法是放置内存屏障:这正是Linux在大小不是1、2、4或8的情况下所做的,在调用后诉诸memcpy并在and之前放置内存屏障.内存障碍也很容易解决问题(2),但会带来巨大的性能损失.

我希望我已经涵盖了一个概述,而没有深入探讨C标准的解释,但如果您愿意,我可以花时间来做这件事.

Linux相关问答推荐

如何确定Linux上的最大静态TLS(线程本地存储)块大小?

使用来自 yocto build 而不是主机系统的 protoc

Ubuntu 20 不支持 MAP_FIXED_NOREPLACE

linux shell获取多文件交集

为什么`__vfprintf_internal`(`stdio.h`中的`printfn`)强制`$rbp`在我的x86-64机器上向前跳转6313864字节?

从 curl 命令输出中获取值

使用 sed 或 awk 在 linux 中将第一行中的一个单词替换为第二行中的另一个单词

另一个远程的 Git 合并分支

根据外部请求在 Netbeans 中启动 XDebug

在 Linux 中 Select 多个同名的可执行文件

如何在 Linux 上捕获原始 HID 输入?

如何/在哪里可以找到要修复的 Linux 内核错误?

如何比较两个压缩包的内容

./studio.sh 之后的 Android Studio 错误

比较文件的日期 bash

.NET Core 中的跨平台文件名处理

在linux上上几个目录

在 Unix 上计算每行/字段的字符出现次数

Linux cmd 在 jar 中搜索类文件,而不考虑 jar 路径

'grep' 的返回码在 Linux 上不如预期