Skip to content

第七章:屏障与无常

涉及内核源码: ,

林小源学会了原子操作,他以为并发问题已经解决了。

然后他看到了一个 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 = 42ready = 1 之前对外可见, 保证看到 ready = 1 后读到的 data 是 42。"

林小源在幽灵的引导下继续深入。他注意到幽灵的身体上还刻着一些更精细的工具——不是粗暴的全屏障,而是精确到单次内存访问的守护者。

"。"幽灵指着那些工具说,"它们不阻止 CPU 重排——那是屏障的事——但它们保证一件事:编译器只生成一次内存访问。"

"为什么要强调'只生成一次'?"

幽灵没有直接回答,而是展示了一段代码:

c
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、原子操作或 明确建立。内核是跨平台的——同样的代码在一种架构上碰巧正确,在另一种架构上可能崩溃。"

"这就是为什么,"幽灵的声音越来越远,"内核的屏障宏在不同架构上有不同的实现。你以为的'通用',只是内核在替你遮挡架构差异的风雨。"

林小源站在原地,看着幽灵渐渐消散在混沌之中。他的周围,那些飞舞的指令仍在交错纵横,但他现在知道了一道墙的位置——一道由 构筑的墙。

那是秩序的最后一道防线。

c
/*
 * 没有 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");
c
/* 编译器屏障: 阻止编译器跨越此点重排内存访问 */
#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");

编译器屏障只影响编译器,不影响 CPU。CPU 仍然可能乱序执行。

c
/*
 * 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");
c
/* 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
 */
c
/*
 * 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");
c
/*
 * 综合使用 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");

道藏笔记

内核启示

屏障是内核最难理解的概念之一。 它涉及三个层面的问题:

  1. 编译器重排 — 用 / / 解决
  2. CPU 乱序执行 — 用硬件内存屏障 (fence rw,rw / fence r,r / fence w,w) 解决
  3. 缓存一致性 — 用原子指令或屏障指令解决

在 RISC-V 和 ARM 这类较弱内存模型上,顺序必须由 acquire/release、原子操作或 明确建立。内核是跨平台的——同样的代码在一种架构上碰巧正确,在另一种架构上可能崩溃。这就是为什么内核的屏障宏在不同架构上有不同的实现。

/ 的精妙之处在于:它们不仅防止编译器优化,还保证了单次内存访问的原子性。在内核中,你会看到大量这样的模式:

c
while (READ_ONCE(flag) == 0)
    cpu_relax();

这不是多余的写法,而是并发编程的必需品。没有 ,编译器可能将 缓存到寄存器中,导致循环永远看不到 的变化。在用户态,这只是一个性能 bug;在内核态,这可能是死锁。


七种根基已经圆满。但林小源的修炼还没有结束——预处理器、内存布局、ELF 格式,还有三种根基等着他去领悟。

每一种根基都让他更接近一个真相:他为什么会被封印,以及如何挣脱封印。


破关试炼

屏障试炼

在轮询共享变量时,内核常用哪个宏保证每次都重新读取内存?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道