写缓冲、重排序与内存屏障:DMB/DSB/ISB 与 Device/Normal 内存
更新时间:2026-09-19。前置:MMU 与 TLB、DMA 与 Cache 一致性。屏障问题的特点是「软件看顺序对、硬件实际顺序不对」,且偶发难复现。
本文要回答的问题
- 写一条寄存器后立刻执行下一条,硬件实际到达外设的顺序可能是什么?
- 写缓冲/store buffer、推测执行、访问合并如何造成重排?
- ARM DMB/DSB/ISB 各管什么,RISC-V 的 fence/fence.i 呢?
- Device 内存属性为什么天然有序,驱动里什么时候必须显式屏障?
一、为什么会「乱序到达」
bash
程序顺序: str R1,[DATA] ; 写数据
str R2,[START] ; 写启动位
实际可能:
CPU → store buffer → 互连 → 从机
└ 写缓冲可能合并/排空顺序变化
└ 若 DATA 在 Normal memory 可被缓冲/重排,START(Device)可能先到机制来源:
- 写缓冲/store buffer:写操作入队即释放 CPU,排空到总线有延迟;连续写可能合并。
- 推测/乱序执行:处理器可提前执行未提交的访问。
- 互连重排:AXI 允许不同 ID 的事务在系统中重排(见 outstanding 与 ID 保序)。
- 编译器重排:编译期把访存指令重排,需要编译屏障(
volatile、asm("":::"memory"))先挡住。
后果经典:先写数据寄存器再写「启动/使能」位,若启动位先到外设,外设会用旧数据动作;或写完使能就进入轮询,使能其实还在缓冲里。
二、ARM 的三道屏障
| 指令 | 作用 |
|---|---|
| DMB(Data Memory Barrier) | 保证屏障两侧的内存访问按指定 shareability 域有序(前先于后完成),但不等待完成、不影响取指 |
| DSB(Data Synchronization Barrier) | 更强:等屏障前所有内存访问真正完成才继续后面任何指令 |
| ISB(Instruction Synchronization Barrier) | 冲刷流水线,保证后续指令按「当前」上下文(如新页表/新系统寄存器)重新取指 |
典型场景:
- 改完页表/转换表:DSB(确保写入完成)+ ISB(让新映射生效)。
- 触发 DMA/写命令后等待其完成:DSB 确保写已离开内核到达外设/内存。
- 多核之间普通内存同步:DMB(配合 load-acquire/store-release 常已隐含)。
RISC-V 对应:普通访存排序用 fence rw,rw;指令/数据一致性(写完代码再执行)用 fence.i;I/O 与强序用 fence io,io。
三、内存类型从硬件上约束顺序(ARMv8 为例)
MMU 页表项的属性决定可不可重排,比纯靠屏障更根本:
- Device-nGnRnE / nGnRE / GRE:访问不可预测、不可重复(访问本身有副作用,如读 FIFO 会弹出数据)、不可提前执行;G=聚合(可否合并多个访问)、R=重排、E=提前写应答。外设寄存器应映射为 Device(最常用 nGnRnE/nGnRE),天然不被推测、不被合并。
- Normal Non-cacheable / Cacheable:普通内存与帧缓冲/DMA 区,允许预取、重排、合并、缓存,因此跨核/与 DMA 共享时要靠屏障或 acquire/release。
把寄存器映射成 Normal memory 是驱动大忌:编译器/CPU 可能把「读状态寄存器」优化掉或预取,导致 FIFO 多读、中断状态丢失。
四、驱动常用模式
c
// 1) 使能外设前,确保前面所有写已到达
writel(data, base + DATA_REG);
writel(ctrl | START, base + CTRL_REG);
wmb(); // 或依赖 Device 映射的顺序 + 平台屏障
// 2) 写命令后等待:DSB 保证命令真正发出再轮询完成位
writel(GO, base + CMD);
dsb(st); // store 到达系统
while (!(readl(base+STATUS) & DONE)) cpu_relax();
// 3) 寄存器「写后读」是常见的强制排空手段
writel(val, base + CFG);
readl(base + CFG); // 读同/相关寄存器迫使前面写先完成(强序设备)五、常见的坑
- 只在逻辑上「先写数据再写使能」:Normal/缓冲内存下使能先到,外设动作旧数据。
- 以为 volatile 就够:
volatile只挡编译器重排与缓存到寄存器,不挡 CPU/硬件运行时重排,仍需 DMB/DSB。 - 缺 DSB 就轮询完成位:命令还在写缓冲,状态永远不 ready,表现为随机超时。
- 改页表后只 DSB 不 ISB:流水线仍执行旧映射取指,行为诡异。
- 把寄存器映射成 Normal cacheable:状态/FIFO 读被预取或缓存,中断与数据错乱。
- 屏障作用域选错:DMB 用了
nsh(内共享)却要跨所有代理/与 DMA 同步,应选sy/osh。 - 多核发布结构只有数据屏障无获取/释放语义:看到数据的核没看到锁,需 release/acquire 成对。
- 自修改代码写完不 fence.i:取到旧指令(RISC-V)/ 未 ISB(ARM)。
- AXI 不同 ID 重排未在 RTL 处理:从机/桥对同地址多 ID 顺序假设错误,见 ID 保序。
- 过度加屏障:把 DSB 放进热路径/每寄存器访问,性能显著下降;应按需用最轻量(wmb/rmb/acquire-release)。
- 中断使能与清除竞态:写中断使能与在 ISR 清 pending 之间缺屏障,丢/重入中断。
相关与延伸
地址转换见 MMU/TLB;与 DMA 共享内存见 DMA 对齐与一致性;总线侧重排见 AXI outstanding/ID。
总结
写操作先进 store buffer 即放行、互连又可能按 ID 重排,因此「程序顺序」不等于「到达外设顺序」;解决分两层——硬件层把寄存器映射成 Device-nGnRnE 这类不可预测/不可重排/不可推测的内存类型,软件层在关键点用 ARM 的 DMB(保访存序)、DSB(等写真正完成)、ISB(刷流水线取指)或 RISC-V 的 fence/fence.i,并记住 volatile 只挡编译器不挡硬件——配数据与使能、改页表、触发 DMA 后轮询是三个最容易因漏屏障而偶发死锁的地方。