第一百八十章:内存屏障
大乘期涉及内核源码:
一
从回调链的宫殿出来后,林小源发现自己站在一条狭窄的山脊上。山脊两侧是万丈深渊,深渊中翻滚着混沌的雾气。
一位身穿白衣的剑客负手而立,背对着他。剑客的腰间悬着三柄剑——一柄赤红,一柄湛蓝,一柄纯白。
"你是谁?"林小源问道。
剑客转过身来,面容冷峻。"我是内存屏障。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。"
标记之试
内核中常用来标记并发单次访问、防止编译器乱优化的两个宏是什么?
/*
* 内存屏障:
*
* 为什么需要:
* 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");#include <stdio.h>
/*
* 内存屏障:
*
* 为什么需要:
* 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);
}
int main() {
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");
return 0;
}道藏笔记
内核启示
你写代码时觉得指令按顺序执行,但 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 时,本章用来保证写入顺序的屏障是什么?