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

分頁錯誤與需求分頁

分頁表可以說某一頁根本不在記憶體裡。那個小小的「不存在」位元,解鎖了虛擬記憶體裡最大膽的把戲:別載入一個程式——讓它自己一頁一頁地分頁進來,恰好在每一頁第一次被碰到的那一刻。

那個說「不在這裡」的位元

看完上一篇導覽,你已經能閉著眼睛讀分頁表了:一個虛擬位址拆成一個分頁號與一個位移,分頁號去索引表格,對應的分頁表項交回實體頁框號,位址轉換於是收尾。我們當時略過了那個項裡的一個小欄位。每個項都帶著一個有效位元(valid bit)——有時叫做存在位元(present bit)——它回答一個直白的問題:這一頁此刻是不是真的就坐在實體記憶體裡?

當有效位元是 1,轉換就照你學過的那樣進行。但當它是 0,項裡的頁框號就毫無意義——根本沒有頁框,因為那一頁不在記憶體裡。硬體無法憑空捏造資料,於是它做了唯一誠實的事:把這條指令當場煞停,並舉起一個特別的例外。那個例外就是分頁錯誤,而儘管名字嚇人,它根本不是個錯誤。它是一個被計畫好的、例行的事件——是分頁表在說:「這一頁在程式的世界裡是存在的,但你得先去把它取回來,我才能給你一個真正的位址。」

需求分頁:不到需要絕不載入

這就是有效位元讓我們得以實現的那個大膽點子。當你啟動一個程式,最直覺的計畫是把整個東西——程式碼、資料、全部——在它執行前從磁碟複製進頁框裡。但大多數程式在某一次執行中,根本碰不到它大部分的分頁:永遠不會觸發的錯誤處理碼、一個你只填了一半的巨大陣列、一頁你從沒打開的說明畫面。把那些全都複製進來,是白費的功夫,也是白費的記憶體。所以我們改成什麼都不複製進來,把每一頁都標成無效,然後照樣讓程式開跑。這就是需求分頁:一頁只在被需求時才進入記憶體——只在程式第一次伸手去拿它、並引發錯誤的那一刻。

想想搬進一間新房子。勤快的計畫第一天就把每個箱子都拆開。懶惰卻聰明的計畫則把箱子留在車庫裡,只在你第一次想要某樣東西的那一刻才拆——想喝茶時拆出水壺,伸手要書時拆出書。你最後拆開的箱子少得多,而你拆的那些,正好就是你會用到的。需求分頁正是把這份懶惰套用到記憶體上,這也是為什麼一個好幾 GB 的應用程式能在零點幾秒內啟動:它並不載入整個程式,而是只載入頭幾頁,再讓其餘的隨著錯誤陸續抵達而流入。

錯誤發生時,實際發生了什麼

分頁錯誤是一段精細的硬體/作業系統編舞,因為那條引發錯誤的指令是在半途被攔下的,稍後還必須像什麼都沒發生過一樣繼續執行。硬體偵測到無效的項並陷入;接著作業系統接手,扛起所有粗活;然後控制權回到那條引發錯誤的指令本身,它如今重跑一次,順順地通過。把步驟走一遍,整個機制就會喀一聲就定位。

  1. CPU 轉換一個位址,發現分頁表項的有效位元是 0,於是舉起一個分頁錯誤例外——在這條指令提交任何結果之前就放棄它。
  2. 控制權陷入作業系統的分頁錯誤處理常式,它檢查:這是一個程式被允許擁有的合法分頁,還是一次真正的非法存取(一個亂指的指標)?非法的在這裡就被終結——這正是「記憶體區段錯誤」(segmentation fault)的由來。
  3. 對於一次合法的錯誤,作業系統找出一個空閒頁框(若記憶體已滿,就驅逐某個別的分頁),並開始把那一頁缺失的內容從磁碟讀進來——可能是一個記憶體映射檔、程式的執行檔,或是置換區(swap)。
  4. 磁碟讀取要花上數百萬個週期,所以作業系統在這期間把 CPU 交給另一個行程——引發錯誤的程式只是睡著了。當資料抵達,作業系統把真正的頁框號寫進那個項裡,並把有效位元翻成 1。
  5. 作業系統從例外返回到同一條指令。這一次,轉換找到了一個有效的項,那個載入或儲存正常完成,而程式完全不知道自己曾經暫停過。

注意這份分工,因為它正是整件事的重點。硬體負責又快又常見的路徑:轉換、檢查有效位元,命中時在幾奈秒內產出一個實體位址軟體——也就是作業系統——負責那條罕見又緩慢的路徑:它決定要驅逐哪個頁框、跟磁碟對話、並更新表格。這種分工是虛擬記憶體的靈魂:它是一場硬體/作業系統協同設計,兩半之中任何一半,都無法獨自變出這個錯覺。

工作集,以及分頁變醜的時候

需求分頁之所以行得通,靠的是與快取同一個賭注:區域性。在任何一小段時間窗裡,一個程式只碰它分頁中的一小撮——它正在跑的那個迴圈、它正在咀嚼的那批資料。那一小撮就是它的工作集。只要工作集能舒舒服服地放進實體記憶體,錯誤就只會在一開始發生(把每一部分各分頁進來一次),之後幾乎再也不會。程式以全速的記憶體速度執行,而需求分頁的代價就攤平到幾近於零。

但推得太過頭,錯覺就會碎裂。如果你跑了太多程式,使得它們合起來的工作集再也放不下,作業系統就被迫驅逐某個程式馬上又要用到的分頁——那一頁立刻引發錯誤,又驅逐了另一個程式需要的分頁,那一頁也引發錯誤。機器把所有時間都花在把分頁來回搬運於磁碟之間,幾乎沒有時間在跑你的程式碼。這叫抖動(thrashing),是虛擬記憶體最糟糕的失效模式:吞吐量崩跌好幾個數量級,磁碟燈一直亮著,一切都慢如爬行。它誠實地提醒我們:分頁買到的是一個浩瀚的位址空間,而不是免費的 RAM——把記憶體超賣得夠離譜,解藥本身就變成了病。

為什麼這很重要,以及接下來呢

退一步,看看光是一個有效位元就買到了多少。需求分頁讓一個程式只需把工作集駐留就能執行,讓系統能跑那些總大小遠超實體記憶體的程式,讓好幾個行程共用一份程式庫分頁的唯讀副本,也讓一個巨大的檔案被當成彷彿就是記憶體一樣對待。這些都不是「額外的 RAM」——這是分頁表誠實地說出什麼存在,而作業系統悄悄地按需求把其餘的補上。虛擬記憶體就是位址轉換加上這種懶惰的、由錯誤驅動的載入,兩者一同運作。

但有一筆代價是我們一直禮貌地忽略著的:每一次轉換,不論有沒有錯誤,都需要分頁表——而它住在記憶體裡。所以一個沒被幫助的載入會碰記憶體兩次,一次去讀表、一次去讀資料,使每一次記憶體存取都加倍。這即使在沒有任何分頁缺失時,也會毀掉效能。下一篇導覽正是用一個小小的、快速的最近轉換快取來修這件事:TLB,其中的 TLB 未命中是日常的代價,只要保持罕見,就是讓這整個錯覺快到能與之共存的關鍵。