x86_64下PCIe BAR映射写合并页后如何刷新写合并缓冲区
核心结论
sfence/mfence单独使用无法保证写合并(WC)缓冲区的待写数据被刷到PCIe设备,仅能保证存储操作的执行顺序。clflush完全不适用于该场景,不要在WC映射的MMIO区域使用它。- 要强制排空WC缓冲、保证写操作到达PCIe设备,正确做法是完成写操作后读一次同一块WC映射BAR空间内的地址。
具体说明
1. fence指令与“全局可见”的实际定义
Intel手册中提到的“全局可见”,针对不同内存类型的含义有明确区别:
- 对普通回写(WB)内存,指数据离开核心私有存储缓冲区,进入全局缓存一致性域,可被其他核心、缓存一致性代理感知;
- 对WC这类无缓存、不参与缓存一致性的MMIO内存,fence指令仅约束核心内部的指令执行顺序:
sfence保证它之前的所有写操作,一定比它之后的写操作先离开核心,但不会主动触发未填满的WC缓冲向外发送数据。
WC缓冲区是核心私有的临时合并区域,设计目标就是攒够64字节的整缓存行再发起总线事务,减少PCIe带宽浪费。如果数据没攒满、也没碰到触发排空的硬件事件,数据会一直停在缓冲里,既不会对其他核心可见,也不会到达PCIe设备。sfence/mfence本身不在Intel定义的WC缓冲排空触发事件列表里,仅插入fence无法保证数据刷出。
2. clflush不适用的原因
clflush的作用对象是CPU缓存层级中的WB缓存行,作用是把指定缓存行的脏数据刷回内存、并失效所有核心中对应的缓存行副本。WC类型的映射页根本不会进入CPU的L1/L2/L3缓存,在WC内存上执行clflush的行为是未定义的,完全无法实现WC缓冲刷新的目的。
3. 正确的WC缓冲刷新方式
要保证所有对WC映射BAR的写操作真正发送到PCIe链路,只需要在所有写操作完成后,执行一次同一BAR映射区域内的任意地址读操作即可。
x86的内存模型强制保证:针对MMIO区域的读操作,必须等之前所有对同域的写操作已经发送到总线之后才会执行,这个动作会直接触发核心排空对应域的WC缓冲区。
注意:如果读BAR上的设备寄存器会触发不期望的设备侧副作用,可以选择读BAR上专门预留的、无副作用的状态寄存器,或者映射一块专门的无副作用保留地址空间用来做刷写动作。
如果你的场景不需要立刻把写推到设备,只需要保证多段写操作的先后顺序(比如第一批写必须比第二批写先到设备),那在两段写中间插入sfence就足够满足序的要求,不需要额外读操作。
另外wbinvd指令确实也能排空所有WC缓冲,但它会刷掉整个CPU所有核心的全部缓存,性能代价极大,除非特殊极端场景否则绝对不要用。
内容的提问来源于stack exchange,提问作者Jack Humphries

