ARM 硬件層的屏障指令
DMB (Data Memory Barrier):保證在它之前的內存訪問(符合給定域/類型)在它之后的內存訪問之前對可見性排序。常用域:
ish
(Inner Shareable),sy
(system-wide,最強)。DSB (Data Synchronization Barrier):比 DMB 更強,等到之前的訪問“完成”才繼續執行下一條指令;常用于設備寄存器編程后需要確保“已生效”的情景。
ISB (Instruction Synchronization Barrier):刷新取指流水,常用于修改系統寄存器后,或自修改代碼等需要讓新指令可見的場景。
Acquire/Release 指令(ARMv8):
LDAR
(獲取/讀-獲取),STLR
(釋放/寫-釋放),提供更精細的有序性而不必全棧DMB
。
Linux 內核里的內存屏障原語(SMP 語義)
這些在 SMP 有效,在 UP 可能是 no-op(但保持可讀性與可移植性):
smp_mb()
:全柵欄(讀寫都排序)。smp_rmb()
/smp_wmb()
:僅讀-讀/寫-寫排序(以及與相對方向的最小保障,取決于架構實現)。smp_store_release(p, v)
/smp_load_acquire(p)
:推薦!映射到 ARM 的STLR/LDAR
,開銷更低,適合無鎖隊列/環形緩沖區。READ_ONCE(x)
/WRITE_ONCE(x, v)
:防止編譯器優化/拆分訪問,但不提供跨 CPU 的排序;常與 acquire/release 或smp_*
組合用。原子操作:大多自帶適當的屏障語義(比如
atomic_xxx_return
通常是 full barrier),但具體要看接口文檔;不要想當然。
鎖類原語自帶屏障:
spin_lock/unlock
:進入/退出臨界區相當于 acquire/release 屏障。mutex_lock/unlock
、rcu_read_lock/unlock
等也帶有已定義的順序保證。
設備/MMIO 與 I/O 屏障
不要用
*(volatile u32 *)addr = v;
直接訪問 MMIO;統一使用內核提供的readl/writel
(或ioread32/iowrite32
)。readl()/writel()
:大多數架構上帶必要的 I/O 可見性屏障(比如在writel()
前插入wmb()
或在readl()
后插入rmb()
等),確保 MMIO 與普通內存的順序。但它們的強度和位置是“架構相關”的。readl_relaxed()/writel_relaxed()
:省略默認的 I/O 屏障,只做單次 MMIO 訪問。需要你顯式加mb()/rmb()/wmb()
或專用 I/O 屏障來保證順序。mmiowb()
:在某些架構/場景下用來約束向不同設備的寫入順序(通常在解鎖自旋鎖后確保批量 MMIO 寫順序對設備可見)。經驗法則:
如果你對設備寫 doorbell/控制寄存器,且之前更新了普通內存中的描述符/數據:先
wmb()
(或dma_wmb()
),再writel()
doorbell。如果要讀取設備狀態后再讀普通內存里的緩沖區:先
readl()
取狀態,再rmb()
,再讀內存(或使用配套的 acquire 讀方案)。
DMA 與緩存一致性(非一致平臺尤需注意)
可用 一致性 DMA(coherent)或 非一致性 DMA(streaming)。
非一致性路徑:
CPU → 設備(CPU 填好 buffer/描述符):
dma_sync_single_for_device(dev, dma_addr, len, DMA_TO_DEVICE)
(或在某些輕量場景用dma_wmb()
之后寫 doorbell)writel()
doorbell/啟動寄存器
設備 → CPU(設備寫回數據):
中斷/輪詢得知完成
dma_sync_single_for_cpu(dev, dma_addr, len, DMA_FROM_DEVICE)
再讀 buffer(必要時配合
dma_rmb()
/rmb()
)
一致性 DMA路徑(cache 一致):
常用順序:更新內存 →
dma_wmb()
→writel()
;讀回數據前dma_rmb()
。
簡化口訣:“寫前 wmb,讀后 rmb;doorbell 前 wmb;拿狀態后 rmb。”
常見并發模式示例
生產者(CPU0)/消費者(CPU1)單向通信(無鎖隊列一類)
/* 生產者:先寫數據,再發布索引 */
buf[idx] = data;
smp_store_release(&tail, idx + 1);/* 消費者:先拿到已發布的索引,再讀數據 */
int t = smp_load_acquire(&tail);
if (head < t) {item = buf[head];head++;
}
store_release
確保對buf
的寫在發布tail
之前對其他 CPU 可見;load_acquire
確保拿到新的tail
后,再讀buf
一定能看到對應數據。
中斷上下文與線程上下文
線程設置“已就緒標志”,中斷處理里讀取:
/* 線程上下文 */
WRITE_ONCE(flag, 1);
smp_wmb(); /* 或者把下一條換成 store_release */
writel(START, dev->doorbell);/* 中斷上下文 */
status = readl(dev->status);
smp_rmb(); /* 或 load_acquire 來讀取 flag 等 */
if (READ_ONCE(flag))handle();
更新 MMIO 描述符 + Doorbell
/* 更新環形隊列的描述符(普通內存) */
desc->len = len;
desc->addr = dma_addr;
/* 確保描述符寫入先于設備可見 */
dma_wmb();
/* 通知設備 */
writel(DBELL_KICK, dev->db_reg);
內存類型與順序直覺
Normal(緩存able):ARM 默認弱內存模型,讀寫可能亂序,需要柵欄/acq-rel。
Device(nGnRnE / nGnRE / GRE):對同一設備 MMIO 訪問有先后規則,但CPU 對普通內存與 MMIO 的相對順序不一定天然符合你的期望;因此 Linux 以
readl/writel
+ 柵欄來提供統一模型。不要用
volatile
代替屏障;volatile
只約束編譯器,不約束 CPU 重排序。
該用誰?最簡決策表
目的 | 用法 |
線程間發布數據 | smp_store_release / smp_load_acquire |
強制完全排序 | smp_mb() |
只約束讀序/寫序 | smp_rmb() / smp_wmb() |
訪問 MMIO(設備寄存器) | readl()/writel()(性能敏感才考慮 _relaxed + 顯式屏障) |
Doorbell 前確保內存已可見 | dma_wmb() 或 wmb(),然后 writel() |
設備寫回后 CPU 讀緩沖 | dma_sync_single_for_cpu() 或 dma_rmb()/rmb() 之后再讀 |
上鎖/解鎖的順序保證 | spin_lock=acquire,spin_unlock=release |
常見坑
只用
WRITE_ONCE/READ_ONCE
卻沒有配對的 acquire/release 或smp_mb()
——在 ARM 上會出錯。用
_relaxed
版本的readl/writel
卻忘記加mb()/wmb()/rmb()
。在非一致性 DMA 平臺忘了
dma_sync_single_for_*
,導致讀到臟緩存或設備看不到最新內存。把
dsb sy
當成“萬金油”濫用,性能大殺器;優先考慮acquire/release
語義。