Linux内核中的原子操作读写实现

17

最近我查看了Linux内核实现原子读写的代码,然后出现了一些问题。

首先是ia64架构下的相关代码:

typedef struct {
    int counter;
} atomic_t;

#define atomic_read(v)      (*(volatile int *)&(v)->counter)
#define atomic64_read(v)    (*(volatile long *)&(v)->counter)

#define atomic_set(v,i)     (((v)->counter) = (i))
#define atomic64_set(v,i)   (((v)->counter) = (i))
  1. 针对读写操作,似乎采用了直接访问变量的方法。除非在其他地方做了一些魔法,否则我无法理解在汇编领域中这个操作如何保证原子性。或许一个显而易见的答案是这个操作会转换成一个汇编指令,但即使这样,在考虑到不同的内存缓存级别(或者其他优化)时,这还有什么保障呢?

  2. 在读取宏定义中,使用了一个强制类型转换技巧来使用volatile类型。有人知道这对原子性有什么影响吗?(注意在写入操作中没有使用该类型)

3个回答

14
我认为您对“原子”和“易失性”这两个词的使用存在误解(非常模糊)。“原子”实际上只意味着单词将被原子地读取或写入(一步完成,并保证该内存位置的内容始终是一个写入或另一个写入,而不是介于两者之间的某些内容)。而“易失性”关键字告诉编译器永远不要假定由于先前的读取/写入而在该位置上的数据(基本上不要优化掉读取)。
“原子”和“易失”在这里并不意味着存在任何形式的内存同步。没有涉及到任何关于内存和高速缓存一致性的保证。这些函数在软件级别上基本上是原子的,硬件可以根据自己的意愿进行优化和改变。
现在为什么只需要读取:每种体系结构的内存模型都是不同的。许多体系结构可以保证针对特定字节偏移量或长度为x字的数据的原子读取或写入,并且从CPU到CPU都是不同的。Linux内核包含许多定义,用于不需要在保证原子读/写的平台上进行任何原子调用(例如,CMPXCHG)。
至于“易失性”,除非您正在访问内存映射的IO,否则通常情况下不需要它,但这取决于何时/在何处/为什么调用“原子读取”和“原子写入”宏。许多编译器会为易失变量生成内存屏障/栅栏(尽管这在C规范中并没有设置)。虽然这通常意味着所有对此变量的读/写现在都正式免除了几乎任何编译器优化,但在这种情况下,通过创建“虚拟”易失性变量,仅对该特定的读取/写入实例进行优化和重新排序是被禁止的。

1
几乎(全部?)CPU 保证对于任何字长的内存主操作(加载或存储)的原子访问,针对普通内存(用于普通对象的内存)自然对齐(在许多情况下也适用于大多数不对齐的访问,适合缓存行)。 - curiousguy

3
阅读对于大多数主要体系结构来说是原子性的,只要它们被对齐到它们的大小的倍数(并且不比给定类型的读取大小更大),请参见Intel架构手册。另一方面,写入可能是不同的,Intel指出,在x86下,单字节写和对齐写可以是原子的,在IPF(IA64)下,所有内容都使用获取和释放语义,这将使其保证原子性,请参见thisvolatile 防止编译器将值缓存在本地,强制在任何访问它的地方检索它。

1
在现代内核中,volatile 是通过一个名为 READ_ONCE() / WRITE_ONCE 的宏来使用的。我理解的是编译器在技术上允许多次读/写该值。例如,如果代码将读取值复制到本地变量中,然后在不同的位置使用该变量,则必须将其描述为更多防止该值被本地缓存。完整描述请参见:https://lwn.net/Articles/508991/。 - sourcejedi

2
如果您为特定架构编写代码,可以做出特定于该架构的假设。我猜 IA-64 将这些内容编译为单个指令。
除非计数器跨越缓存行边界,否则缓存不应该成为问题。但如果需要 4/8 字节对齐,则不会发生这种情况。
当机器指令转换为两个内存访问时,需要一个“真正”的原子操作指令。这适用于增量(读取、增加、写入)或比较并交换。
"volatile" 影响编译器可以执行的优化。例如,它防止编译器将多个读取转换为一个读取。但在机器指令级别上,它无效。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接