代码之家  ›  专栏  ›  技术社区  ›  Gaut

linux内核列表中的WRITE_ONCE

  •  48
  • Gaut  · 技术社区  · 9 年前

    我正在阅读 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:初始化list_head结构时使用WRITE_ONCE()
    对非RCU列表进行无锁空性测试的代码依赖于 在INIT_LIST_HEAD()上写入列表头的->下一个指针 原子上,尤其是从调用INIT_LIST_HEAD()时 list_del_init()。因此,此提交将WRITE_ONCE()添加到此 函数的指针存储可能会影响头部的->下一个指针。

    1 回复  |  直到 9 年前
        1
  •  43
  •   Michael F    3 年前

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

    相关定义为 here ,一句非常简短的评论说明了它们的目的:

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

    ...

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

    但这些话是什么意思?


    问题

    问题实际上是复数的:

    1. 读/写“撕裂”:用许多较小的内存访问替换单个内存访问。GCC可能(而且确实如此!)在某些情况下,替换类似 p = 0x01020304; 使用两个16位存储立即指令,而不是假定将常量放入寄存器,然后进行内存访问,等等。 写入一次 允许我们对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)中的循环,因为它会移动 不稳定的 跨序列点访问,这是C标准不允许的。Linux系统 uses 我们称之为“易失性访问”(见下文),而不是将对象标记为易失性。我们 能够 通过将特定对象标记为 不稳定的 ,但这是(几乎?)从来都不是一个好选择。有 many reasons 这可能是有害的。

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

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

    假设我们不知道 确切地 什么 不稳定的 做-并找出 isn't easy! (查看#5)-实现这一点的另一种方法是设置内存障碍:这正是Linux在大小不是1、2、4或8的情况下所做的 memcpy 并在 在通话后。内存障碍也很容易解决问题(2),但会带来很大的性能损失。

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