JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

保護機制與記憶體管理單元(MMU)

前四篇砌起了那個錯覺:分頁、分頁表、需求分頁的錯誤,以及讓它變快的 TLB。最後這一篇要替操盤整場戲的硬體取名——MMU——並揭示虛擬記憶體最深的禮物,從來不是多出來的空間,而是一道在每一次存取上、由機器親自強制執行的、程式與程式之間的牆。

認識那位指揮:MMU 究竟是什麼

在這一整階裡,我們不斷地指著「那個硬體」——它拆開位址、走訪分頁表、檢查那些位、組出實體位址。是時候替它取名了。記憶體管理單元(MMU)是晶片上一塊真實的邏輯,坐在處理器核心與記憶體之間的路徑上,並在程式用到的「每一個」位址被允許碰到 RAM 之前,先把它翻譯一遍。回想第一篇的房子比喻:每個程式都相信整棟建築是自己的,而 MMU 就是站在門口那位安靜的翻譯員,把每一個想像中的房間號碼換成真實的——若程式根本沒資格問,就回絕這個請求。

MMU 同時做兩件無法拆分的工作,而看清它們其實是「同一次」查表,正是本篇的核心。第一件是翻譯:透過分頁表虛擬位址變成實體位址,而快路徑由第四篇那個 TLB 來服務。第二件是保護:讀取同一筆分頁表項裡的權限位,判斷這一次特定的存取——讀、寫、或取指令——到底允不允許。因為兩個答案都出自「同一筆」,機器永遠不會有一個「翻譯了卻沒檢查」的位址。這正是為什麼我們誠實地把虛擬記憶體說成「位址翻譯加保護」,而絕不是「多出來的 RAM」。

保護位:在每次存取上強制的一道牆

我們在第二篇把權限位當成分頁表項裡一串旗標認識過;現在來看它們換來什麼。一筆典型的分頁表項帶著可讀位、可寫位、可執行位,外加一個表示「這頁到底有沒有對應」的有效位。每一次存取,MMU 都把這次存取的「種類」拿來和這些位比對。對唯讀頁做一次儲存、跳進一個標記為不可執行的頁、從未對應的頁做載入——每一種都過不了檢查,而 MMU 不會悄悄地繼續下去。它會引發一次錯誤、陷入作業系統,由作業系統決定要殺掉程式、送出一個訊號,還是出手修正。

這些位,正是為什麼單一個有臭蟲或心懷惡意的程式,無法塗改另一個程式的資料、或塗改作業系統本身。這就是行程隔離,而它不是靠彬彬有禮來維持,是靠硬體在實體層面拒絕非法存取。你或許製造過的那個經典當機——「區段錯誤(segmentation fault)」——正是此事:一個在野指標碰到某個虛擬位址,而那頁要嘛沒對應(有效位是關的)、要嘛被寫入保護,MMU 在那一次存取當下就攔住它,作業系統在它毀壞任何東西之前停下程式。注意它和第三篇那個分頁錯誤的對比:分頁錯誤是一次「合法」、只是該頁還不在 RAM 裡的存取,作業系統把它取進來就修好了;保護錯誤則是一次「非法」存取,作業系統通常的回應是把程式殺掉。

使用者/核心的界線與特權等級

保護位把程式擋在彼此的頁外;還有一個位把它們擋在作業系統的頁外。現代處理器至少在兩個特權等級下運行:使用者模式,是一般程式所住之處;以及更有權力的監督者(核心)模式,是作業系統所在。每一筆分頁表項都帶一個旗標,標記這頁屬不屬於核心,而 MMU 會回絕任何使用者模式對核心頁的存取。於是即使核心的頁就坐在程式看得見的同一個位址空間裡,程式也讀不到、寫不了——這條線由硬體劃下。

如果程式碰不到核心頁,那它到底怎麼請核心做事——開檔案、配置記憶體、往網路送出資料?透過唯一獲准的門:一次系統呼叫。一條特殊指令把處理器切進監督者模式,並跳到一個固定、由核心選定的進入點——程式無法自己挑要落在哪。核心做完那些特權工作,再在返回前掉回使用者模式。這道受控的跨越,是「唯一」往上的路,而這正是讓那條界線值得信任的原因:特權不是程式靠跳進某段聰明的程式碼就能自己授予的,因為通往核心模式的唯一一扇門,連目的地都一併替你挑好了。

讓它變快:TLB、快取,與多層走訪

若這一切檢查很慢,那它就毫無價值,所以 MMU 重重地仰賴第四篇那個 TLB——一個小小的、全關聯的快取,存著最近的「分頁對頁框」翻譯結果「以及它們的保護位」。在一次 TLB 命中上,MMU 一個週期內就拿到頁框號和權限檢查的結果,存取就此進行,完全沒有額外的記憶體往返。翻譯與保護,都從同一筆 TLB 項裡免費取得。這正是為什麼實務上虛擬記憶體的每次存取開銷通常是隱形的:常見情況是命中,而我們讓常見情況變快

翻譯和資料快取也咬合得很漂亮。一個常見的把戲,是讓快取查找和 TLB 查找「平行」開始:分頁偏移那幾個位——翻譯從不更動它們——拿來索引快取,同時 TLB 把分頁號變成頁框號,那個頁框號再供作比對用的標籤。快取命中和翻譯大致同時完成,於是位址翻譯藏在快取存取底下,而非疊加上去。(這個「虛擬索引、實體標籤」的安排有些我們不展開的限制,但那個想法——把兩次查找重疊起來——才是值得留下的部分。)

  1. 把虛擬位址拆成分頁號與偏移,並把分頁號送給 TLB。
  2. TLB 命中(常見情況):直接從相符的那筆 TLB 項讀出頁框號與保護位——翻譯與權限檢查,兩者約一個週期內完成。
  3. TLB 未命中:MMU 走訪第二篇那張多層分頁表,把分頁號切成數個欄位、一層一層地循樹而下——好幾次記憶體讀取,因為每一層本身都在記憶體裡。
  4. 若最後那一筆在場且這次存取被允許,就把翻譯載進 TLB 並繼續;若該頁在磁碟上,就觸發分頁錯誤;若這次存取非法,就觸發保護錯誤。

誠實的隱憂:協同設計、代價,與一個真實的漏洞

退一步,看整台機器。虛擬記憶體是一場硬體與作業系統的協同設計:MMU 是硬體,但它走訪的那些分頁表,是由作業系統建立並維護的;它引發的那些錯誤,由作業系統的程式碼處理;而那些策略——換出什麼、何時取進、如何共享——純粹是軟體。兩半都無法獨自運作。硬體快卻笨;它只懂走一張表、檢查一個位,僅此而已。作業系統聰明卻慢;它決定一切,卻不直接碰任何位址。這設計的優雅恰在這份分工,而它的脆弱,就住在兩者之間的接縫上。

也對代價誠實。一次 TLB 命中幾乎免費,但一次 TLB 未命中會觸發一趟好幾次相依記憶體讀取的分頁表走訪;而一次真的得抵達磁碟的分頁錯誤,要花上「數百萬」個週期——單單一次錯誤,就能讓引發它的那條指令所做的工相形見絀。這是整個階段反覆出現的誠實叮嚀:虛擬記憶體的平均開銷微不足道,但它的尾巴殘暴,而一個不停猛烈衝擊 TLB、或不斷觸發錯誤的工作負載,可能完全被翻譯主宰,而非被它真正的計算主宰。這個錯覺只在區域性成立時才便宜——正是快取那一階反覆敲進的教訓,如今穿著一件分頁大小的外衣。