第七章:屏障与无常
涉及内核源码: ,
一
林小源学会了原子操作,他以为并发问题已经解决了。
然后他看到了一个 bug。
一个进程在初始化一个数据结构:先写入数据,再设置标志位表示"数据就绪"。代码的顺序清清楚楚,白纸黑字。但在另一个 CPU 核心上,消费者看到了"数据就绪"的标志,却读到了未初始化的数据。
"怎么可能?"林小源脱口而出。
"你看到的顺序,"一个声音说,"在硬件层面可能根本不存在。"
那声音来自一个幽灵般存在——半透明的,像一层薄纱悬挂在代码和硬件之间。它的身躯上刻满了密密麻麻的时序图,箭头交错纵横,有些指向同一个方向,有些背道而驰。
"我是内存屏障。"幽灵说,"或者说,我是驯服'无常'的力量。"
"无常?"
"你以为代码从上到下执行,先写的先到,后写的后到。"幽灵的声音带着一种疲惫的悲悯,"但编译器会重排指令的顺序——它觉得这样更快。CPU 会乱序执行操作——它的流水线太深了,不可能停下来等。缓存会延迟写入的可见性——一个核心的写入可能要过很久才能被另一个核心看到。"
幽灵停顿了一下,然后说:"你以为的'顺序',是一种幻觉。在多核的世界里,只有屏障才能创造真正的顺序。"
二
林小源跟着幽灵来到了一片混沌之地。
这里没有稳定的结构,只有无数条指令在空中飞舞——有些向前飞,有些向后飞,有些甚至在半空中悬停。他看到一条写入指令刚刚落地,另一条读取指令就已经冲到了它的前面。
"这是编译器的领域。"幽灵说,"编译器是第一个'无常'的制造者。它会重排内存访问的顺序,只要它认为单线程程序看不出区别。但在多线程的环境下,这种重排可能导致灾难。"
幽灵伸出一只半透明的手,指向虚空中的一道裂缝。
"。"它说,"这是你对抗编译器的第一件武器。它告诉编译器:'这个变量可能在你不知道的情况下被改变,每次都要从内存读取,不要优化掉任何写入。'"
"但它不是线程安全的保证?"林小源问。
"不是。"幽灵的声音变得严肃," 只做一件事:禁止编译器缓存变量值到寄存器,禁止优化掉读写操作。它不保证原子性,不保证顺序性,不保证缓存一致性。真正的安全需要更强的武器。"
幽灵的身体上浮现出一行代码:#define barrier() asm volatile("" ::: "memory")。
"编译器屏障。"它说,"这是第二件武器。它不生成任何机器指令——零开销——但它告诉编译器:'这条线之前的所有内存访问,必须在这条线之前完成;这条线之后的所有内存访问,必须在这条线之后开始。'编译器不能跨越这道屏障重排任何内存操作。"
" clobber 是什么意思?"林小源盯着那行代码问。
"它告诉编译器:'这条汇编可能读写任何内存。'编译器必须把所有缓存的值写回内存,不能跨越屏障重排内存操作。这是编译器屏障的核心机制。"
三
"但是,"林小源说,"编译器屏障只影响编译器,对吧?CPU 仍然可能乱序执行。"
幽灵露出了一个赞许的微笑——如果那团半透明的雾气能微笑的话。
"你学得很快。"它说,"是的,编译器屏障只是一道纸墙。真正的敌人是 CPU 的乱序执行。要驯服它,你需要硬件内存屏障。"
幽灵的身体突然变得凝实了一些。它的表面浮现出三条发光的纹路,像三道闪电劈开了混沌。
"RISC-V 有三条屏障指令。"它的声音变得铿锵有力,"fence w, w——写屏障,之前的写操作必须在之后的写操作之前对外可见。fence r, r——读屏障,之前的读操作必须在之后的读操作之前完成。fence rw, rw——全屏障,之前的读写都完成后,才执行之后的读写。"
林小源看着那三条纹路,感到一种庄严的力量从其中散发出来。这不再是纸上谈兵的理论,而是硬件级别的秩序——一道真正的墙,横亘在混沌和秩序之间。
"内核把这些封装成了宏。"幽灵继续说,"mb()、、——通用屏障,适用于所有场景。、、——SMP 屏障,仅在多核环境下生效。在单核配置下,它们退化为编译器屏障——因为单核不需要硬件屏障。"
"经典例子呢?"林小源问。
幽灵的身体上浮现出两个影子:一个生产者,一个消费者。生产者先写入数据 data = 42,然后调用 ,再设置标志 ready = 1。消费者在循环中等待 ready 变为 1,然后调用 ,最后读取 data。
"没有屏障,"幽灵说,"消费者可能看到 ready = 1,但读到的 data 还是 0——因为 CPU 或缓存延迟了 data 的可见性。有了屏障, 保证 data = 42 在 ready = 1 之前对外可见, 保证看到 ready = 1 后读到的 data 是 42。"
四
林小源在幽灵的引导下继续深入。他注意到幽灵的身体上还刻着一些更精细的工具——不是粗暴的全屏障,而是精确到单次内存访问的守护者。
" 和 。"幽灵指着那些工具说,"它们不阻止 CPU 重排——那是屏障的事——但它们保证一件事:编译器只生成一次内存访问。"
"为什么要强调'只生成一次'?"
幽灵没有直接回答,而是展示了一段代码:
if (*p > 0) {
return *p; // 可能读到不同的值!
}"编译器可能优化这段代码,让它读两次 *p。"幽灵说,"第一次在 if 判断中,第二次在 语句中。如果另一个核心在这两次读取之间修改了 *p,你可能在 if 中看到正数,在 中看到负数——或者更糟,看到一个半写入的值。"
"READ_ONCE(*p) 强制只读一次。"它继续说,"读到的值被保存在一个临时变量中,后续操作都使用这个临时变量。这样就避免了'两次读取之间值变化'的问题。"
" 类似——它保证写入是原子的,不会被拆分成多次写入。在 32 位系统上写入 64 位值时,没有 ,编译器可能把一次写入拆成两次,导致其他核心看到一个'半新半旧'的值。"
林小源默默记住了这些工具。他现在明白了:并发安全不是靠一件武器就能解决的,而是需要层层防护——编译器屏障阻止编译器重排,硬件屏障阻止 CPU 重排,/ 阻止编译器优化。每一层都不可或缺。
五
"最后,"幽灵的声音变得柔和了一些,"让我给你看一个综合的例子——一个无锁标志。"
幽灵的身体上浮现出一个精密的结构体:一个原子标志和一个数据字段。写入端先用 写入数据,再用编译器屏障,最后用 atomic_store(release) 设置标志。读取端先用 atomic_load(acquire) 检查标志,再用编译器屏障,最后用 读取数据。
"四层保护。"幽灵说," 保证数据写入不被拆分。 保证编译器不重排数据和标志的写入。atomic_store(release) 保证 CPU 不重排。atomic_load(acquire) 保证读到最新值。缺一不可。"
林小源看着那个结构体,突然感到了一种敬畏。这不是某个天才灵光一闪的发明,而是无数内核开发者在无数次崩溃、死锁、数据损坏之后总结出来的经验。每一条规则的背后,都有一个真实的 bug,一个真实的系统崩溃,一个真实的工程师在凌晨三点的绝望。
"屏障是内核最难理解的概念之一。"幽灵最后说,"它涉及三个层面的问题:编译器重排、CPU 乱序执行、缓存一致性。在 RISC-V 和 ARM 这类较弱内存模型上,顺序必须由 acquire/release、原子操作或 明确建立。内核是跨平台的——同样的代码在一种架构上碰巧正确,在另一种架构上可能崩溃。"
"这就是为什么,"幽灵的声音越来越远,"内核的屏障宏在不同架构上有不同的实现。你以为的'通用',只是内核在替你遮挡架构差异的风雨。"
林小源站在原地,看着幽灵渐渐消散在混沌之中。他的周围,那些飞舞的指令仍在交错纵横,但他现在知道了一道墙的位置——一道由 、、、 构筑的墙。
那是秩序的最后一道防线。
/*
* 没有 volatile: 编译器可能优化成只读一次
*/
static int flag_normal = 0;
static int wait_normal(void) {
int count = 0;
while (flag_normal == 0) {
count++;
/* 编译器可能优化成死循环! */
}
return count;
}
/*
* 有 volatile: 每次循环都从内存读取
*/
static volatile int flag_volatile = 0;
static int wait_volatile(void) {
int count = 0;
while (flag_volatile == 0) {
count++;
/* 编译器不会优化掉这个读取 */
}
return count;
}
/* 模拟 MMIO(内存映射 I/O)寄存器 */
static volatile uint32_t *const UART_STATUS =
(volatile uint32_t *)0xDEADBEEF; /* 假设地址 */
/* 模拟中断标志 */
static volatile int irq_received = 0;
printf("=== volatile ===\n\n");
printf("volatile 的作用:\n");
printf(" 1. 禁止编译器缓存变量值到寄存器\n");
printf(" 2. 禁止编译器优化掉读/写操作\n");
printf(" 3. 禁止编译器重排 volatile 变量的访问\n\n");
printf("volatile 的常见用途:\n");
printf(" - MMIO 寄存器: 硬件随时可能改变值\n");
printf(" - 中断处理程序共享的变量\n");
printf(" - 信号处理程序共享的变量\n\n");
printf("volatile 不保证:\n");
printf(" - 原子性 (用 atomic_t 或锁)\n");
printf(" - 顺序性 (用内存屏障)\n");
printf(" - 缓存一致性 (用屏障或原子操作)\n\n");
/* 展示 volatile 的必要性 */
flag_volatile = 0;
printf("flag_volatile 地址: %p\n", (void *)&flag_volatile);
printf("volatile 确保每次 while 循环都重新读取这个地址\n");#include <stdio.h>
#include <stdint.h>
/*
* 没有 volatile: 编译器可能优化成只读一次
*/
static int flag_normal = 0;
static int wait_normal(void) {
int count = 0;
while (flag_normal == 0) {
count++;
/* 编译器可能优化成死循环! */
}
return count;
}
/*
* 有 volatile: 每次循环都从内存读取
*/
static volatile int flag_volatile = 0;
static int wait_volatile(void) {
int count = 0;
while (flag_volatile == 0) {
count++;
/* 编译器不会优化掉这个读取 */
}
return count;
}
/* 模拟 MMIO(内存映射 I/O)寄存器 */
static volatile uint32_t *const UART_STATUS =
(volatile uint32_t *)0xDEADBEEF; /* 假设地址 */
/* 模拟中断标志 */
static volatile int irq_received = 0;
int main() {
printf("=== volatile ===\n\n");
printf("volatile 的作用:\n");
printf(" 1. 禁止编译器缓存变量值到寄存器\n");
printf(" 2. 禁止编译器优化掉读/写操作\n");
printf(" 3. 禁止编译器重排 volatile 变量的访问\n\n");
printf("volatile 的常见用途:\n");
printf(" - MMIO 寄存器: 硬件随时可能改变值\n");
printf(" - 中断处理程序共享的变量\n");
printf(" - 信号处理程序共享的变量\n\n");
printf("volatile 不保证:\n");
printf(" - 原子性 (用 atomic_t 或锁)\n");
printf(" - 顺序性 (用内存屏障)\n");
printf(" - 缓存一致性 (用屏障或原子操作)\n\n");
/* 展示 volatile 的必要性 */
flag_volatile = 0;
printf("flag_volatile 地址: %p\n", (void *)&flag_volatile);
printf("volatile 确保每次 while 循环都重新读取这个地址\n");
return 0;
}/* 编译器屏障: 阻止编译器跨越此点重排内存访问 */
#define barrier() asm volatile("" ::: "memory")
/*
* 没有屏障: 编译器可能重排
*/
static int a_normal = 0, b_normal = 0;
static void write_normal(void) {
a_normal = 1;
b_normal = 1;
/* 编译器可能重排为: b=1; a=1; */
}
/*
* 有屏障: 保证顺序
*/
static int a_barrier = 0, b_barrier = 0;
static void write_barrier(void) {
a_barrier = 1;
barrier(); /* a 的写入必须在 b 之前完成 */
b_barrier = 1;
}
printf("=== 编译器屏障 ===\n\n");
printf("barrier() 的作用:\n");
printf(" - 阻止编译器跨越屏障重排内存访问\n");
printf(" - 强制编译器将所有缓存的值写回内存\n");
printf(" - 不生成任何机器指令(零开销)\n\n");
printf("实现:\n");
printf(" #define barrier() asm volatile(\"\" ::: \"memory\")\n\n");
printf("\"memory\" clobber 的含义:\n");
printf(" - 告诉编译器: 这条汇编可能读写任何内存\n");
printf(" - 编译器必须把所有缓存的值写回内存\n");
printf(" - 编译器不能跨越屏障重排内存操作\n\n");
printf("内核中的使用:\n");
printf(" - 锁的实现: 保证临界区的内存访问不溢出到锁外\n");
printf(" - per-cpu 变量: 保证读取最新的值\n");
printf(" - 设备驱动: 确保寄存器访问的顺序\n");#include <stdio.h>
#include <stdint.h>
/* 编译器屏障: 阻止编译器跨越此点重排内存访问 */
#define barrier() asm volatile("" ::: "memory")
/*
* 没有屏障: 编译器可能重排
*/
static int a_normal = 0, b_normal = 0;
static void write_normal(void) {
a_normal = 1;
b_normal = 1;
/* 编译器可能重排为: b=1; a=1; */
}
/*
* 有屏障: 保证顺序
*/
static int a_barrier = 0, b_barrier = 0;
static void write_barrier(void) {
a_barrier = 1;
barrier(); /* a 的写入必须在 b 之前完成 */
b_barrier = 1;
}
int main() {
printf("=== 编译器屏障 ===\n\n");
printf("barrier() 的作用:\n");
printf(" - 阻止编译器跨越屏障重排内存访问\n");
printf(" - 强制编译器将所有缓存的值写回内存\n");
printf(" - 不生成任何机器指令(零开销)\n\n");
printf("实现:\n");
printf(" #define barrier() asm volatile(\"\" ::: \"memory\")\n\n");
printf("\"memory\" clobber 的含义:\n");
printf(" - 告诉编译器: 这条汇编可能读写任何内存\n");
printf(" - 编译器必须把所有缓存的值写回内存\n");
printf(" - 编译器不能跨越屏障重排内存操作\n\n");
printf("内核中的使用:\n");
printf(" - 锁的实现: 保证临界区的内存访问不溢出到锁外\n");
printf(" - per-cpu 变量: 保证读取最新的值\n");
printf(" - 设备驱动: 确保寄存器访问的顺序\n");
return 0;
}编译器屏障只影响编译器,不影响 CPU。CPU 仍然可能乱序执行。
/*
* RISC-V 内存屏障指令:
*
* fence rw, rw — 全屏障: 之前的读写都完成后,才执行之后的读写
* fence r, r — 读屏障: 之前的读操作完成后,才执行之后的读操作
* fence w, w — 写屏障: 之前的写操作完成后,才执行之后的写操作
*
* RISC-V 是较弱的内存模型,跨 CPU 或 MMIO 顺序必须显式表达。
*/
/* 写屏障: 之前的写操作必须在之后的写操作之前对外可见 */
static inline void wmb(void) {
#if defined(__riscv)
asm volatile("fence w, w" ::: "memory");
#else
__sync_synchronize();
#endif
}
/* 读屏障: 之前的读操作必须在之后的读操作之前完成 */
static inline void rmb(void) {
#if defined(__riscv)
asm volatile("fence r, r" ::: "memory");
#else
__sync_synchronize();
#endif
}
/* 全屏障: 读+写 */
static inline void mb(void) {
#if defined(__riscv)
asm volatile("fence rw, rw" ::: "memory");
#else
__sync_synchronize();
#endif
}
/*
* 经典例子: 生产者-消费者
*
* 生产者:
* data = 42; // 写数据
* wmb(); // 确保数据写入在标志之前
* flag = 1; // 写标志
*
* 消费者:
* while (!flag); // 等待标志
* rmb(); // 确保读标志在读数据之前
* use(data); // 读数据 — 保证看到 42
*/
static volatile int data = 0;
static volatile int ready = 0;
static void producer(void) {
data = 42;
wmb(); /* 确保 data=42 在 ready=1 之前对外可见 */
ready = 1;
}
static void consumer(void) {
while (!ready) {
/* 等待 */
}
rmb(); /* 确保看到 ready=1 后,读到的 data 是 42 */
printf("data = %d (应该是 42)\n", data);
}
printf("=== 硬件内存屏障 ===\n\n");
printf("RISC-V 内存模型:\n");
printf(" - 普通内存访问允许更多重排\n");
printf(" - release/acquire 或 fence 用来建立顺序\n");
printf(" - MMIO 与跨 CPU 同步必须谨慎使用屏障\n\n");
printf("屏障类型:\n");
printf(" fence w, w (写屏障): 之前的写完成后,才执行之后的写\n");
printf(" fence r, r (读屏障): 之前的读完成后,才执行之后的读\n");
printf(" fence rw, rw (全屏障): 之前的读写完成后,才执行之后的读写\n\n");
printf("Linux 内核通常把这些封装成 mb/rmb/wmb/smp_* 宏\n\n");
/* 演示 */
producer();
consumer();
printf("\n注意: RISC-V 和 ARM 一类弱内存模型必须显式表达顺序\n");
printf("内核用统一的屏障宏隐藏架构差异\n");#include <stdio.h>
#include <stdint.h>
/*
* RISC-V 内存屏障指令:
*
* fence rw, rw — 全屏障: 之前的读写都完成后,才执行之后的读写
* fence r, r — 读屏障: 之前的读操作完成后,才执行之后的读操作
* fence w, w — 写屏障: 之前的写操作完成后,才执行之后的写操作
*
* RISC-V 是较弱的内存模型,跨 CPU 或 MMIO 顺序必须显式表达。
*/
/* 写屏障: 之前的写操作必须在之后的写操作之前对外可见 */
static inline void wmb(void) {
#if defined(__riscv)
asm volatile("fence w, w" ::: "memory");
#else
__sync_synchronize();
#endif
}
/* 读屏障: 之前的读操作必须在之后的读操作之前完成 */
static inline void rmb(void) {
#if defined(__riscv)
asm volatile("fence r, r" ::: "memory");
#else
__sync_synchronize();
#endif
}
/* 全屏障: 读+写 */
static inline void mb(void) {
#if defined(__riscv)
asm volatile("fence rw, rw" ::: "memory");
#else
__sync_synchronize();
#endif
}
/*
* 经典例子: 生产者-消费者
*
* 生产者:
* data = 42; // 写数据
* wmb(); // 确保数据写入在标志之前
* flag = 1; // 写标志
*
* 消费者:
* while (!flag); // 等待标志
* rmb(); // 确保读标志在读数据之前
* use(data); // 读数据 — 保证看到 42
*/
static volatile int data = 0;
static volatile int ready = 0;
static void producer(void) {
data = 42;
wmb(); /* 确保 data=42 在 ready=1 之前对外可见 */
ready = 1;
}
static void consumer(void) {
while (!ready) {
/* 等待 */
}
rmb(); /* 确保看到 ready=1 后,读到的 data 是 42 */
printf("data = %d (应该是 42)\n", data);
}
int main() {
printf("=== 硬件内存屏障 ===\n\n");
printf("RISC-V 内存模型:\n");
printf(" - 普通内存访问允许更多重排\n");
printf(" - release/acquire 或 fence 用来建立顺序\n");
printf(" - MMIO 与跨 CPU 同步必须谨慎使用屏障\n\n");
printf("屏障类型:\n");
printf(" fence w, w (写屏障): 之前的写完成后,才执行之后的写\n");
printf(" fence r, r (读屏障): 之前的读完成后,才执行之后的读\n");
printf(" fence rw, rw (全屏障): 之前的读写完成后,才执行之后的读写\n\n");
printf("Linux 内核通常把这些封装成 mb/rmb/wmb/smp_* 宏\n\n");
/* 演示 */
producer();
consumer();
printf("\n注意: RISC-V 和 ARM 一类弱内存模型必须显式表达顺序\n");
printf("内核用统一的屏障宏隐藏架构差异\n");
return 0;
}/* include/asm-generic/barrier.h */
/* 通用屏障 */
#define mb() asm volatile("fence rw, rw" ::: "memory")
#define rmb() asm volatile("fence r, r" ::: "memory")
#define wmb() asm volatile("fence w, w" ::: "memory")
/* 仅编译器屏障(无硬件指令) */
#define smp_mb() barrier() /* 在 SMP 上是全屏障 */
#define smp_rmb() barrier() /* 在 SMP 上是读屏障 */
#define smp_wmb() barrier() /* 在 SMP 上是写屏障 */
/* 解除屏障: 用于解除之前的屏障 */
#define smp_mb__before_atomic() barrier()
#define smp_mb__after_atomic() barrier()
/*
* 在 RISC-V 上:
* - smp_mb() 会展开成 fence rw, rw 或等价序列
* - smp_rmb() 会展开成读屏障
* - smp_wmb() 会展开成写屏障
*
* 在 ARM 上:
* - smp_mb() 是真正的 dmb 指令
* - smp_rmb() 是 dmb ishld
* - smp_wmb() 是 dmb ishst
*//*
* READ_ONCE / WRITE_ONCE 的作用:
*
* 1. 保证编译器只生成一次内存访问
* 2. 保证访问是原子的(对于自然对齐的类型)
* 3. 不阻止 CPU 重排(需要配合屏障)
*/
#define READ_ONCE(x) (*(volatile typeof(x) *)&(x))
#define WRITE_ONCE(x, v) (*(volatile typeof(x) *)&(x) = (v))
static int shared_val = 0;
static int read_unsafe(int *p) {
/* 编译器可能读两次! */
if (*p > 0) {
return *p; /* 可能读到不同的值 */
}
return 0;
}
static int read_safe(int *p) {
int val = READ_ONCE(*p);
if (val > 0) {
return val; /* 保证读到同一个值 */
}
return 0;
}
printf("=== READ_ONCE / WRITE_ONCE ===\n\n");
WRITE_ONCE(shared_val, 42);
printf("WRITE_ONCE(shared_val, 42)\n");
printf("READ_ONCE(shared_val) = %d\n\n", READ_ONCE(shared_val));
printf("为什么需要 READ_ONCE/WRITE_ONCE:\n\n");
printf("1. 防止编译器合并读取:\n");
printf(" 普通读:\n");
printf(" if (*p > 0) return *p;\n");
printf(" 编译器可能优化为只读一次 p,但也可能读两次\n");
printf(" READ_ONCE 强制只读一次\n\n");
printf("2. 防止编译器优化掉读取:\n");
printf(" while (flag == 0) { }\n");
printf(" 如果 flag 不是 volatile,编译器可能优化成死循环\n");
printf(" READ_ONCE(flag) 强制每次都从内存读取\n\n");
printf("3. 防止编译器写入拆分:\n");
printf(" 对 64 位值在 32 位系统上写入可能被拆成两次\n");
printf(" WRITE_ONCE 强制原子写入\n\n");
printf("注意: READ_ONCE/WRITE_ONCE 不保证跨 CPU 可见性\n");
printf("需要配合内存屏障使用\n");#include <stdio.h>
#include <stdint.h>
/*
* READ_ONCE / WRITE_ONCE 的作用:
*
* 1. 保证编译器只生成一次内存访问
* 2. 保证访问是原子的(对于自然对齐的类型)
* 3. 不阻止 CPU 重排(需要配合屏障)
*/
#define READ_ONCE(x) (*(volatile typeof(x) *)&(x))
#define WRITE_ONCE(x, v) (*(volatile typeof(x) *)&(x) = (v))
static int shared_val = 0;
static int read_unsafe(int *p) {
/* 编译器可能读两次! */
if (*p > 0) {
return *p; /* 可能读到不同的值 */
}
return 0;
}
static int read_safe(int *p) {
int val = READ_ONCE(*p);
if (val > 0) {
return val; /* 保证读到同一个值 */
}
return 0;
}
int main() {
printf("=== READ_ONCE / WRITE_ONCE ===\n\n");
WRITE_ONCE(shared_val, 42);
printf("WRITE_ONCE(shared_val, 42)\n");
printf("READ_ONCE(shared_val) = %d\n\n", READ_ONCE(shared_val));
printf("为什么需要 READ_ONCE/WRITE_ONCE:\n\n");
printf("1. 防止编译器合并读取:\n");
printf(" 普通读:\n");
printf(" if (*p > 0) return *p;\n");
printf(" 编译器可能优化为只读一次 p,但也可能读两次\n");
printf(" READ_ONCE 强制只读一次\n\n");
printf("2. 防止编译器优化掉读取:\n");
printf(" while (flag == 0) { }\n");
printf(" 如果 flag 不是 volatile,编译器可能优化成死循环\n");
printf(" READ_ONCE(flag) 强制每次都从内存读取\n\n");
printf("3. 防止编译器写入拆分:\n");
printf(" 对 64 位值在 32 位系统上写入可能被拆成两次\n");
printf(" WRITE_ONCE 强制原子写入\n\n");
printf("注意: READ_ONCE/WRITE_ONCE 不保证跨 CPU 可见性\n");
printf("需要配合内存屏障使用\n");
return 0;
}/*
* 综合使用 volatile、屏障、原子操作实现一个无锁标志
*/
#define READ_ONCE(x) (*(volatile typeof(x) *)&(x))
#define WRITE_ONCE(x, v) (*(volatile typeof(x) *)&(x) = (v))
#define barrier() asm volatile("" ::: "memory")
typedef struct {
atomic_int flag;
int data;
} flag_data_t;
/* 写入端 */
static void flag_set(flag_data_t *fd, int val) {
WRITE_ONCE(fd->data, val);
barrier(); /* 编译器屏障: data 在 flag 之前写入 */
atomic_store_explicit(&fd->flag, 1, memory_order_release);
}
/* 读取端 */
static int flag_get(flag_data_t *fd, int *val) {
if (!atomic_load_explicit(&fd->flag, memory_order_acquire)) {
return 0; /* 还没有数据 */
}
barrier(); /* 编译器屏障: flag 在 data 之前读取 */
*val = READ_ONCE(fd->data);
return 1;
}
/* 可重置的标志 */
static void flag_clear(flag_data_t *fd) {
atomic_store_explicit(&fd->flag, 0, memory_order_release);
}
printf("=== 无锁标志 ===\n\n");
flag_data_t fd = { .flag = ATOMIC_VAR_INIT(0), .data = 0 };
/* 写入数据 */
flag_set(&fd, 42);
printf("写入: data=42\n");
/* 读取数据 */
int val;
if (flag_get(&fd, &val)) {
printf("读取: data=%d (正确!)\n\n", val);
}
/* 重置 */
flag_clear(&fd);
printf("重置标志\n");
if (!flag_get(&fd, &val)) {
printf("读取: 标志未设置 (正确!)\n\n");
}
printf("关键点:\n");
printf(" 1. WRITE_ONCE 保证 data 的写入不被拆分或优化\n");
printf(" 2. barrier() 保证编译器不重排 data 和 flag 的写入\n");
printf(" 3. atomic_store(release) 保证 CPU 不重排\n");
printf(" 4. atomic_load(acquire) 保证读到最新值\n");
printf(" 5. READ_ONCE 保证读取不被优化或缓存\n\n");
printf("这四层保护缺一不可:\n");
printf(" - 编译器可能重排 → 需要 barrier()\n");
printf(" - CPU 可能重排 → 需要 release/acquire\n");
printf(" - 编译器可能优化 → 需要 READ/WRITE_ONCE\n");
printf(" - 缓存可能不一致 → 需要屏障刷新\n");#include <stdio.h>
#include <stdatomic.h>
#include <stdint.h>
/*
* 综合使用 volatile、屏障、原子操作实现一个无锁标志
*/
#define READ_ONCE(x) (*(volatile typeof(x) *)&(x))
#define WRITE_ONCE(x, v) (*(volatile typeof(x) *)&(x) = (v))
#define barrier() asm volatile("" ::: "memory")
typedef struct {
atomic_int flag;
int data;
} flag_data_t;
/* 写入端 */
static void flag_set(flag_data_t *fd, int val) {
WRITE_ONCE(fd->data, val);
barrier(); /* 编译器屏障: data 在 flag 之前写入 */
atomic_store_explicit(&fd->flag, 1, memory_order_release);
}
/* 读取端 */
static int flag_get(flag_data_t *fd, int *val) {
if (!atomic_load_explicit(&fd->flag, memory_order_acquire)) {
return 0; /* 还没有数据 */
}
barrier(); /* 编译器屏障: flag 在 data 之前读取 */
*val = READ_ONCE(fd->data);
return 1;
}
/* 可重置的标志 */
static void flag_clear(flag_data_t *fd) {
atomic_store_explicit(&fd->flag, 0, memory_order_release);
}
int main() {
printf("=== 无锁标志 ===\n\n");
flag_data_t fd = { .flag = ATOMIC_VAR_INIT(0), .data = 0 };
/* 写入数据 */
flag_set(&fd, 42);
printf("写入: data=42\n");
/* 读取数据 */
int val;
if (flag_get(&fd, &val)) {
printf("读取: data=%d (正确!)\n\n", val);
}
/* 重置 */
flag_clear(&fd);
printf("重置标志\n");
if (!flag_get(&fd, &val)) {
printf("读取: 标志未设置 (正确!)\n\n");
}
printf("关键点:\n");
printf(" 1. WRITE_ONCE 保证 data 的写入不被拆分或优化\n");
printf(" 2. barrier() 保证编译器不重排 data 和 flag 的写入\n");
printf(" 3. atomic_store(release) 保证 CPU 不重排\n");
printf(" 4. atomic_load(acquire) 保证读到最新值\n");
printf(" 5. READ_ONCE 保证读取不被优化或缓存\n\n");
printf("这四层保护缺一不可:\n");
printf(" - 编译器可能重排 → 需要 barrier()\n");
printf(" - CPU 可能重排 → 需要 release/acquire\n");
printf(" - 编译器可能优化 → 需要 READ/WRITE_ONCE\n");
printf(" - 缓存可能不一致 → 需要屏障刷新\n");
return 0;
}道藏笔记
内核启示
屏障是内核最难理解的概念之一。 它涉及三个层面的问题:
- 编译器重排 — 用 / / 解决
- CPU 乱序执行 — 用硬件内存屏障 (
fence rw,rw/fence r,r/fence w,w) 解决 - 缓存一致性 — 用原子指令或屏障指令解决
在 RISC-V 和 ARM 这类较弱内存模型上,顺序必须由 acquire/release、原子操作或 明确建立。内核是跨平台的——同样的代码在一种架构上碰巧正确,在另一种架构上可能崩溃。这就是为什么内核的屏障宏在不同架构上有不同的实现。
/ 的精妙之处在于:它们不仅防止编译器优化,还保证了单次内存访问的原子性。在内核中,你会看到大量这样的模式:
while (READ_ONCE(flag) == 0)
cpu_relax();这不是多余的写法,而是并发编程的必需品。没有 ,编译器可能将 缓存到寄存器中,导致循环永远看不到 的变化。在用户态,这只是一个性能 bug;在内核态,这可能是死锁。
七种根基已经圆满。但林小源的修炼还没有结束——预处理器、内存布局、ELF 格式,还有三种根基等着他去领悟。
每一种根基都让他更接近一个真相:他为什么会被封印,以及如何挣脱封印。
屏障试炼
在轮询共享变量时,内核常用哪个宏保证每次都重新读取内存?