從兩個位址世界,到一位翻譯員
在第一篇導覽裡,我們畫下了這個階段的那道大分界:程式所說的邏輯位址(彷彿從零開始計算,活在它自己私有的世界裡),對上真正行走在匯流排上、送往 RAM 晶片的實體位址。我們也說過,要讓這一切在執行期間真正運作,必須有某樣東西在每一次記憶體參考時把一者轉成另一者。那個某樣東西是一塊硬體,而整篇導覽談的就是它。
來認識 記憶體管理單元,也就是 MMU。想像一位飛快又不知疲倦的翻譯員,站在一個只會說「程式語言」的人,和一個只懂「真實貨架號碼語言」的倉管員之間。每當程式說「取貨號 1000」,翻譯員立刻把它轉換成真實貨架並把請求傳過去——若程式伸手去拿一個它無權碰的貨架,就把它的手拍開。MMU 就是那位翻譯員,以硬體形式內建在 CPU 裡。它以矽晶片的速度工作,在你的程式執行的同時進行,所以這份代價是隱形的。
為什麼它非得是硬體?回想上一篇導覽:現代系統採用執行時的位址綁定,程式從不被改寫成真實位址,所以邏輯到實體的轉換必須在每一次存取時重新發生。一支程式每秒碰觸記憶體達數十億次。若作業系統得用軟體去轉譯每一次參考,每一次記憶體存取都會慢如龜爬。MMU 的存在,正是為了讓這個轉譯本質上是免費的——它是讓「安全共用一台機器」這件事根本得以實現的硬體基礎。
兩個數字,描述你的整個房間
最簡單的 MMU 只需要兩個暫存器。想像旅館給客人一間房:「你的空間從 90000 號門開始,寬度是 5000 道門。」兩個數字就完整描述了客人能去哪裡——從哪裡開始、有多大。客人可以在裡面自由走動,但保全會在邊界處攔下他。這就是基底與界限暫存器。基底暫存器(也叫重定位暫存器)存放你的空間從哪裡開始;界限暫存器存放它有多大。
美妙之處在於:這同樣的兩個數字一次完成兩件工作。它們帶來 重定位——加上基底,就把一個邏輯位址轉成正確的實體位置,於是程式可以被載入到任何有空閒 RAM 的地方,而完全不必更動它的程式碼。它們也帶來 保護——檢查界限,就保證程式永遠無法指名一個落在它自己區域之外的位址。MMU 在每一次存取時,依固定順序執行這兩件事:先檢查;唯有檢查通過,它才相加。順序很重要,因為你必須在從越界位址算出實體位址之前,就先攔下這次越界。
logical address from CPU --> [ MMU ]
step 1 (protect): is addr < limit ?
| |
yes no
| |
step 2 (relocate): phys = addr + base TRAP to OS
| (illegal access)
v
to RAM chips
base = 90000 limit = 5000
addr 1200 -> 1200 < 5000 ok -> phys 91200
addr 6000 -> 6000 < 5000 NO -> trap一步一步,走完一個位址
讓我們用單一行程把它具體化。假設作業系統已把行程 P 放進實體記憶體,基底 = 90000、界限 = 5000。這代表 P 被允許產生邏輯位址 0 到 4999,MMU 會把它們對映到實體 90000 到 94999,此外都不行。現在看看,當 P 執行一道「載入邏輯位址 1200 處的值」的指令時,會發生什麼。
- CPU 執行 P 的程式碼,產生邏輯位址 1200,並把它交給 MMU。程式純粹把這想成「我自己世界裡的 1200 號位置」。
- MMU 拿 1200 和界限 5000 相比。由於 1200 小於 5000,這次存取落在 P 的區域之內,獲准繼續。
- MMU 把基底 90000 加到邏輯位址上:90000 + 1200 = 91200。這就是真正的實體位址。
- 實體位址 91200 被送到 RAM 晶片,晶片回傳存放在那裡的位元組。晶片只看到 91200,它從不知道 1200 是什麼;而 P 那一邊,也從不知道 91200 是什麼。
現在來看不愉快的那條路。假設 P 也許因為一個臭蟲,試圖載入邏輯位址 6000。MMU 拿 6000 和界限 5000 相比,發現 6000 已達或超過界限,於是拒絕。它不去加基底、也不碰記憶體,而是向作業系統觸發一個陷阱——一個定址錯誤。作業系統通常的回應,是用你或許熟知的「分段錯誤」(segmentation fault)把 P 結束掉。請注意這份保護是滴水不漏的:P 無法搆到實體 96000(那可能屬於另一個行程),因為這次檢查發生在任何加法之前,由硬體完成,迴圈裡沒有任何軟體插手。
誰有權設定這道圍籬?
一道圍籬的強度,取決於「誰可以挪動它」這條規則。如果一支程式能載入它自己的基底與界限暫存器,它就能逕自把自己的房間擴大到涵蓋整棟大樓——保護瞬間崩潰。所以載入這些暫存器是一項特權操作:它使用一條只能在核心模式下執行的特權指令。這正是你在更早階段認識的雙模式運作,如今在做真正的工作。使用者模式的程式在物理上就無法更改自己的邊界;唯有受信任的核心可以。
這直接連回更早階段的情境切換。當核心把 CPU 從行程 P 切換到行程 Q 時,P 的基底與界限就不再描述正在執行的行程了——它們描述的是錯的房間。所以每一次情境切換都包含一個步驟:核心用 Q 的值,重新載入基底與界限暫存器(在現代分頁系統裡則改成一個分頁表指標)。萬一它忘了,Q 就會帶著 P 的圍籬執行,可能讀到 P 的記憶體。MMU 的保護是真實的,但它的正確程度,只取決於作業系統在每次切換時載入給它的那些值。
這個簡單方案買到了什麼,又買不到什麼
基底與界限式的 MMU 是教科書的起點,而以它如此低廉的代價來說,它確實威力強大。它帶來動態重定位:因為轉譯在每次存取時即時發生,作業系統可以在一個行程被暫停時,把它撿起來放到一個新的實體位址,只要改動它的基底即可——不必改寫程式碼,不必重新編譯。正是這份自由,讓作業系統日後得以把行程挪在一起以收回散落的空間,也能把閒置的行程暫時挪到磁碟、再換回一個不同的位置。
但要誠實面對它的一個大假設:它給每個行程剛好一個基底和一個界限,所以每個行程都必須佔據單一、不間斷的一段實體記憶體。這就是連續配置,也是這個模型的硬性天花板。一個行程不能被拆到兩個分開的空洞裡,也不能越過它的界限、長進它鄰居外側剛好空著的空間。下一篇導覽會說明,這單單一個假設如何直接引出碎裂問題——也就是「空閒記憶體總量充足,實務上卻無用」的原因。