記憶體映射輸入輸出(memory-mapped I/O)
/ MEM-ory mapped I-O /
一個只懂得讀寫記憶體位址的 CPU,要怎麼跟印表機或磁碟對話?訣竅是一場偽裝:把裝置假扮成記憶體。記憶體映射輸入輸出把每個裝置的控制暫存器,安排到 CPU 用來存取一般記憶體的同一個位址空間裡。要命令一個裝置,CPU 只要對一個魔法位址做一次普通的載入或儲存——而那裡讀寫到的不是記憶體格子,而是裝置控制器內部的一個暫存器。這就像一間旅館,有些房號其實不是房間而是服務鈕:按 500 號的鈕就是叫房務。
具體來說,硬體保留一段位址範圍,使它們不通往 RAM 而通往裝置控制器。把值 1 寫到位址 0xFE000000 也許會把磁碟控制器裡的「開始傳輸」位元設起來;讀位址 0xFE000004 也許會傳回一個說「忙碌」或「完成」的狀態字。CPU 不需要特殊指令——它用在記憶體上的同一個載入與儲存,用在裝置上也行得通。另一種做法稱為埠映射或埠 I/O,給裝置一個獨立的位址空間,只能用特殊的 in 與 out 指令存取;今天記憶體映射 I/O 較為常見,因為它重用了一般指令集。
兩個誠實的提醒。第一,裝置暫存器不是一般記憶體:讀一個可能有副作用(讀狀態暫存器可能會把它清掉),而且值會自己改變,所以這段記憶體絕不能被快取或推測式存取——軟體會把這些區域特別標記。第二,一塊位址空間被裝置吃掉了,這在歷史上正是 32 位元機器無法用滿 4 GB RAM 的原因之一:有些位址早被 I/O 佔走了。
一段小驅動程式把一個位元組寫到裝置位於 0x3F8 的資料暫存器,然後反覆讀 0x3F8+5 的狀態暫存器,直到「傳送就緒」位元被設起來。對 CPU 而言這些只是儲存與載入;對 UART 控制器而言它們意味著「送出這個字元」和「你準備好了嗎?」。
同樣的載入/儲存指令之所以能碰到裝置,是因為它的暫存器就住在看似平凡的位址上。
記憶體映射的裝置區域必須標記為不可快取。若快取握著狀態暫存器的舊副本,CPU 就會讀到舊值、錯過裝置的更新——這是經典又令人費解的驅動程式臭蟲。