Skip to content

第一百八十章:内存屏障

大乘期

涉及内核源码:

从回调链的宫殿出来后,林小源发现自己站在一条狭窄的山脊上。山脊两侧是万丈深渊,深渊中翻滚着混沌的雾气。

一位身穿白衣的剑客负手而立,背对着他。剑客的腰间悬着三柄剑——一柄赤红,一柄湛蓝,一柄纯白。

"你是谁?"林小源问道。

剑客转过身来,面容冷峻。"我是内存屏障。Memory Barrier。在你眼中,代码是按顺序执行的。但在CPU和编译器眼中,一切都可以重排——为了性能,它们会打乱指令的顺序。"

他拔出赤红色的剑,横在身前。"这是写屏障smp_wmb()。在我之前的写入,必须在我之后的写入之前完成。"

又拔出湛蓝的剑。"这是读屏障smp_rmb()。在我之前的读取,必须在我之后的读取之前完成。"

最后拔出纯白的剑。"这是全屏障smp_mb()。在我之前的所有操作,必须在我之后的所有操作之前完成。"

三柄剑同时出鞘,剑气纵横,将山脊两侧的雾气劈开。林小源看到雾气之下,无数指令如流水般奔涌——有的在前进,有的在倒退,混乱不堪。

"没有屏障的世界就是这样。"剑客冷冷说道,"指令随意重排,数据顺序全乱。你以为你先写了data再写了flag,但CPU可能先写了flag再写data。另一个CPU读到flag=1,去读data,读到的却是旧值。"

"但别把我当成绝对天条。"剑客把剑尖插入山石,石面浮出一段小字:memory-barriers.txt is not a specification。"这份文档是 Linux 屏障使用指南,不是硬件规范。它告诉你各类屏障能依赖的最低功能;若仍有疑问,要回到 LKMM 和具体架构实现。"

"所以屏障不是越多越好?"

"越多越慢,越少越错。真正的本事,是知道哪两个操作之间需要建立顺序。"

破关试炼

屏障初试

Linux memory-barriers.txt 明确说自己不是硬件什么,而是屏障使用指南?

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

"但前辈,不是所有架构都需要屏障吧?"林小源想起了之前的修炼。

剑客点头。"x86有强内存模型——读取和写入不会重排,只有写-读可能重排。所以x86的smp_wmb()和smp_rmb()实际上是空操作,只需要smp_mb()。"

"ARM呢?"

"ARM是弱内存模型——任何操作都可能重排。所以ARM需要更多的屏障,每次内存访问都要小心翼翼。"剑客将三柄剑收回鞘中,"但你写的是内核代码,必须在所有架构上正确。所以即使在x86上,该用屏障的地方也不能省。"

"屏障还要成对。"剑客抬手,山脊两侧出现两个 CPU 的影子。"一个 CPU 用 release 发布数据,另一个 CPU 要用 acquire 接住;生产者用 smp_store_release() 让 item 先于索引可见,消费者用 smp_load_acquire() 保证看到索引后再读 item。单独一堵墙,挡不住另一边的乱序。"

"锁也带屏障吗?"

"很多锁获取、释放、睡眠和唤醒原语都有隐含的顺序效果,但不能乱猜。比如 wake_up() 不是任何时候都能替代屏障,只有真的唤醒等待者时才有相应保证。"

破关试炼

成对之试

生产者用 release 发布数据时,消费者通常要用哪类语义接住它?

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

"还有一种屏障——编译器屏障barrier()。"剑客从袖中取出一面铜镜,"它只阻止编译器重排,不阻止CPU重排。编译器在优化时可能调整指令顺序,barrier()告诉编译器:'到此为止,不许越过这条线。'"

林小源接过铜镜,看到镜中映出两行代码——一行在屏障前,一行在屏障后,中间有一道清晰的分界线。

"屏障是看不见的墙。"剑客最后说道,"你感觉不到它的存在,但没有它,并发程序就是一场灾难。记住,性能可以优化,正确性不能妥协。"

"还有 。"剑客把铜镜翻面,镜中出现两个印章,"它们不是完整屏障,却能阻止编译器把并发访问撕裂、合并或反复加载。写并发代码时,普通 C 访问常常表达不出你的真实意图;该标记的访问不标记,KCSAN 也会把它当作潜在 data race。"

破关试炼

标记之试

内核中常用来标记并发单次访问、防止编译器乱优化的两个宏是什么?

答对后才能继续滑动和进入下一章。
c
/*
 * 内存屏障:
 *
 * 为什么需要:
 *   CPU 可能重排指令
 *   编译器可能重排指令
 *   多核之间缓存不一致
 *
 * 类型:
 *   smp_wmb() — 写屏障
 *     确保写入顺序
 *     屏障前的写入在屏障后的写入前完成
 *
 *   smp_rmb() — 读屏障
 *     确保读取顺序
 *     屏障前的读取在屏障后的读取前完成
 *
 *   smp_mb() — 全屏障
 *     确保所有操作顺序
 *     屏障前的操作在屏障后的操作前完成
 *
 *   barrier() — 编译器屏障
 *     阻止编译器重排
 *     不阻止 CPU 重排
 *
 * 使用场景:
 *   锁的实现
 *   无锁数据结构
 *   设备驱动
 *   RCU
 */

/* 模拟内存屏障 */
int data = 0;
int flag = 0;

/* 生产者 (写入) */
void producer(void) {
    printf("生产者:\n");
    data = 42;
    printf("  data = %d\n", data);
    /* smp_wmb() — 确保 data 在 flag 之前写入 */
    flag = 1;
    printf("  flag = %d\n", flag);
}

/* 消费者 (读取) */
void consumer(void) {
    printf("消费者:\n");
    printf("  flag = %d\n", flag);
    /* smp_rmb() — 确保 flag 在 data 之前读取 */
    printf("  data = %d\n", data);
}

printf("=== 内存屏障 — 顺序的保障 ===\n\n");

printf("内存屏障确保操作顺序:\n\n");

printf("--- 写屏障 (smp_wmb) ---\n");
printf("屏障前的写入在屏障后的写入前完成\n\n");
producer();

printf("\n--- 读屏障 (smp_rmb) ---\n");
printf("屏障前的读取在屏障后的读取前完成\n\n");
consumer();

printf("\n--- 全屏障 (smp_mb) ---\n");
printf("屏障前的操作在屏障后的操作前完成\n\n");

printf("--- 屏障类型 ---\n");
printf("smp_wmb():\n");
printf("  写屏障\n");
printf("  确保写入顺序\n\n");
printf("smp_rmb():\n");
printf("  读屏障\n");
printf("  确保读取顺序\n\n");
printf("smp_mb():\n");
printf("  全屏障\n");
printf("  确保所有操作顺序\n\n");
printf("barrier():\n");
printf("  编译器屏障\n");
printf("  只阻止编译器重排\n\n");

printf("--- 使用场景 ---\n");
printf("1. 锁的实现:\n");
printf("   获取锁: smp_mb()\n");
printf("   释放锁: smp_mb()\n\n");
printf("2. 无锁数据结构:\n");
printf("   更新指针前: smp_wmb()\n");
printf("   读取指针后: smp_rmb()\n\n");
printf("3. 设备驱动:\n");
printf("   写寄存器: wmb()\n");
printf("   读寄存器: rmb()\n\n");

printf("--- x86 的特点 ---\n");
printf("x86 有强内存模型:\n");
printf("  读取不会重排\n");
printf("  写入不会重排\n");
printf("  只有写-读可能重排\n\n");
printf("所以 x86 的 smp_wmb/smp_rmb 是空操作\n");
printf("只需要 smp_mb\n");

道藏笔记

内核启示

你写代码时觉得指令按顺序执行,但 CPU 和编译器为了性能会偷偷重排。你先写 data 再写 flag,CPU 可能先写了 flag——另一个 CPU 看到 flag=1 去读 data,读到的还是旧值。内存屏障就是阻止这种重排的墙。

smp_wmb() 保证屏障前的写入在屏障后的写入之前完成,smp_rmb() 管读顺序,smp_mb() 全管。barrier() 是编译器屏障,只阻止编译器重排不阻止 CPU。x86 是强内存模型,读写本身不太会重排,smp_wmb/smp_rmb 实际上是空操作;ARM 是弱内存模型,每次访问都要小心翼翼。但内核代码要跨架构正确,该用的地方不能省。

更现代的写法常用 release/acquire 成对表达发布与获取:smp_store_release() 发布,smp_load_acquire() 接收。/ 用来标记并发单次访问,避免编译器把访问优化成你没写过的样子。屏障文档本身也提醒:它是指南,不是硬件规范;复杂场景要回到 LKMM。

内存屏障是守护者——确保顺序,保证正确。


破关试炼

内存屏障之试

生产者先写 data 再置 flag 时,本章用来保证写入顺序的屏障是什么?

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

以修仙之名,悟内核之道