volatile 和 memory barrier 的组合用法
为什么 volatile
不够?
先讲一个核心观点:
volatile
只防止 编译器优化,但不阻止 CPU 的乱序执行(reorder)!
这在 多核系统、SoC、DSP 交互、或者访问 共享 memory-mapped IO(如 DP 寄存器) 时,会引发隐蔽 bug。
举个例子:想设置两个寄存器
*(volatile uint32_t*)REG_A = 0x01; // 开启
*(volatile uint32_t*)REG_B = 0x02; // 配置
你以为执行顺序是 A → B,但 CPU 可能 重排序 成 B → A!
为什么?
因为:
-
volatile
保证每条语句都执行,但不保证顺序 -
现代 CPU(尤其 ARM)为性能会对内存访问做乱序执行
-
你以为“先开再设”的动作,硬件上可能变成“先设再开”
所以引入:memory barrier(内存屏障)
内存屏障(Memory Barrier / Memory Fence)用于强制 CPU 在指令之间的顺序,防止重排序。
关键类型的内存屏障
屏障指令 | 作用 |
---|---|
__sync_synchronize() (GCC builtin) | 全屏障,阻止所有内存访问乱序 |
dmb , dsb , isb (ARM 汇编) | 数据/系统/指令级内存屏障 |
mb() , rmb() , wmb() (Linux 内核) | 全屏障、只读屏障、只写屏障 |
std::atomic_thread_fence (C++11) | C++ 标准的 memory fence,用于控制跨线程顺序 |
volatile + memory barrier 的组合用法示例
场景:通知 DSP 某个命令写完了
// 寄存器地址映射
volatile uint32_t* REG_CMD = (uint32_t*)0xF9008000;
volatile uint32_t* REG_FLAG = (uint32_t*)0xF9008004;// 步骤1:写命令
*REG_CMD = 0xABCD;// 步骤2:内存屏障,确保写入命令完成后再标记完成
__sync_synchronize(); // 或 Linux 下的 smp_wmb();// 步骤3:设置 flag
*REG_FLAG = 0x1;
这就能确保 REG_CMD 的写操作在 REG_FLAG 设置前完成。
DSP 或硬件看到 REG_FLAG=1
,就知道前面的指令已经 ready。
volatile 是防编译器,memory barrier 是防 CPU
类型 | 编译器优化 | CPU重排序 | 使用对象 |
---|---|---|---|
volatile | ✅ 禁止 | ❌ 不管 | 变量/寄存器 |
memory barrier | ❌ 不管 | ✅ 阻止 | 线程间、寄存器顺序 |
volatile ≠ 内存屏障! 但它们经常需要配合使用,尤其是在多核场景或驱动中。
Linux 驱动中的实际例子
writel(val, reg_addr); // 写入寄存器(可能会被缓冲)
wmb(); // 保证这个写操作被 flush 到总线
-
writel()
本身可能是 relaxed 的 -
wmb()
保证 写操作在它之前的写入一定完成
在 QCOM Display / DP 驱动中的典型用法:
你可能看到这样的片段:
writel(DP_CTL_ENABLE, dp_base + DP_CTL);
mb(); // 或 wmb()writel(START, dp_base + DP_START);
确保“先 enable 控制器、再发送 start 命令”,不会被 CPU 重排序。
总结口诀
volatile
是给 编译器 的提示,
memory barrier 是给 CPU 的命令。
两者配合,用于确保:“写完再标记”
“先设寄存器 A,再设寄存器 B”
“先检查条件,再做动作”