MMIO 屏障紀律(MMIO barrier discipline)
當你寫控制硬體的程式碼時,你通常假設機器精確地照你寫的、按順序去做。但編譯器與 CPU 被允許重排、合併、甚至省略它們認為不影響結果的記憶體操作——對普通 RAM 是不錯的假設,對裝置暫存器卻是場災難。MMIO 屏障紀律就是一組規則,強迫裝置存取精確地照所寫、以正確順序發生,使硬體看到驅動程式所意圖的序列。
有兩個不同的問題。第一,編譯器不可把 MMIO 存取最佳化掉或重排:迴圈中對狀態暫存器的讀取必須每次都真的重新讀硬體,而非重用一個被快取的值——這正是 volatile 的用途,而核心的 writel/readl 存取器強制它,讓你不必自己手刻 volatile 指標。第二,即使編譯器照順序送出寫入,CPU 與匯流排仍可能讓它們以亂序完成。設想:你把一個資料緩衝區的位址寫進一個暫存器,再把一個「go」位元寫進另一個;若「go」的寫入比位址的寫入先到達裝置,裝置就會從一個過時的位址開始讀。在它們之間放一個記憶體屏障——寫入用 wmb(),或 I/O 存取器與舊程式碼中 mmiowb 之類函式所隱含的屏障——強迫第一個寫入在第二個之前對裝置可見。
這份紀律之所以重要,是因為少了它,驅動程式可能紙上正確、實務上壞掉,臭蟲只在最佳化下或在某些 CPU 上出現,除起錯來令人抓狂。誠實的核心:光有 volatile 並不夠——volatile 阻止的是編譯器重排與快取,但它不阻止 CPU 或匯流排重排,所以你需要真正的屏障來處理存取之間的排序,也需要 volatile 風格的存取器讓編譯器不把它們消除。請用核心的 readl/writel 與屏障輔助函式;不要用一個赤裸的 volatile 指標自創一套。
// 錯誤的順序可能讓裝置從過時位址開始: writel(buf_addr, base + ADDR); // 告訴裝置位置在哪 wmb(); // 屏障:讓 ADDR 先到達 writel(GO_BIT, base + CTRL); // 然後才說「go」 // 光有 volatile 只阻止編譯器重用值, // 但只有屏障才為裝置排序這兩個寫入。
在位址寫入與「go」寫入之間放屏障,保證裝置以所意圖的順序看到它們。
volatile 只阻止編譯器的重排與快取,不阻止 CPU/匯流排的重排——它不是屏障,也不是原子的。請用核心的 readl/writel 存取器加上明確的屏障(wmb/rmb)來處理裝置存取之間的排序。