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

在多個行程之間配置頁框

第一到第三篇導覽問的是「該趕走哪一頁」。這一篇往上跳一層:當許多行程共用一池頁框時,每個行程該分到幾個?又是誰有權從誰那裡偷走一個頁框?這些答案——均分還是按比例、全域置換還是局部置換——決定了整台機器是順暢運轉,還是傾入崩潰。

從「哪一頁」到「幾個頁框」

前三篇導覽都把一個行程孤立來看:給定固定數目的頁框與一條參考字串,策略該趕走哪一頁?FIFO、那個最佳基準、LRU與時鐘演算法回答的全是同一個窄問題。但一台真實的機器從來不會只跑一個行程。幾十個行程——你的瀏覽器、編輯器、音樂播放器、一堆背景常駐程式——同時都活著,而它們共用同一份實體 RAM。所以有個策略本身回答不了的、更前面的問題:在這整池頁框當中,每個行程能把幾個當成自己的?這就是頁框配置,也是本篇導覽的主題。

為什麼這個數目這麼要緊?因為正如第一篇導覽所示,一個行程握有的頁框數,是它分頁錯誤率的第二個大旋鈕——而一次錯誤比一次 RAM 存取慢上數萬倍。給一個行程太少頁框,就算配上完美的置換策略,它也會不停出錯;給得大方一點,錯誤就幾乎消失。麻煩在於頁框是零和的:你交給瀏覽器的每一個頁框,都是編輯器拿不到的那一個。頁框配置就是「把稀缺的共用資源切分好,讓整個系統(而非某一個行程)跑得順」的這門藝術。

均分對上按比例分

假設作業系統有 100 個空閒頁框,要分給它正在跑的那些行程。最簡單的方案是均分配置:如果有 5 個行程,就各發 100 / 5 = 20 個頁框。在「公平」這個遊樂場式的意義上,它美妙極了——人人拿到一樣的份額。但它對「每個行程實際上有多大」一無所知。一個只有 10 頁足跡的小小狀態列小工具,泡在 20 個它永遠用不到的頁框裡;而一個 200 頁的影像編輯器卻被 20 個餓著、幾乎每一步都出錯。均分把痛苦平均散開,而這往往意味著把痛苦散得很糟。

精神上更公平的方案是按比例配置:依照每個行程位址空間的大小(或對其需求的某種估計)成比例地分給它一份頁框。如果行程 A 想要 30 頁、行程 B 想要 70 頁,而有 100 個頁框可用,那麼 A 拿到大約 30 個、B 拿到大約 70 個,而不是各自死板地分 50 個。這讓頁框與胃口相稱,於是大行程不會挨餓,小行程也不會被塞進用不到的頁框。多數真實的配置器都倚賴某種按比例的想法,有時還再用行程優先權加權,好讓重要的工作分到更豐厚的一份。

Pool = 100 frames.  Two processes, by address-space size:
   A wants 30 pages
   B wants 70 pages

Equal allocation:
   A -> 50 frames   B -> 50 frames     (ignores size)

Proportional allocation (share = size / total size):
   total = 30 + 70 = 100
   A -> floor(30/100 * 100) = 30 frames
   B -> floor(70/100 * 100) = 70 frames

Both schemes must respect the per-process minimum,
and both must be RECOMPUTED when a new process arrives
or an old one exits (the pool gets resliced).
在一池 100 個頁框上的均分對上按比例配置。按比例讓頁框與每個行程的大小相稱;兩者都必須遵守指令集的最小值,並隨著行程組合變化而重新計算。

無論用哪種方式,配置都不是一次定終身的決定。執行中行程的組合一直在變——一個新程式啟動、另一個結束、某個工作因為載入了大檔而膨脹。每一次變化都意味著這池頁框必須重新切分,而一個一秒前還很舒服的行程,可能突然發現自己的配額正在縮水。正是這種變動,使得配置與置換無法分開來設計:你給出多少頁框,會直接餵進「置換策略得多常執行」這件事。

全域對上局部置換:誰能從誰那裡偷

配置設定了起始份額,但只要一個行程出錯、又沒有空閒頁框,第二個問題就會引爆:它去哪裡找犧牲者?這就是全域對上局部置換的抉擇,而它可以說比「均分對上按比例」更舉足輕重。在局部置換之下,一個出錯的行程只能趕走自己的某一頁。因此它的頁框數是固定的——它可以調換自己哪些分頁常駐,卻永遠無法以別的行程為代價把自己的地盤擴大或縮小。每個行程都被砌在自己那間公寓裡。

全域置換之下,一個出錯的行程可以趕走整個系統裡的任何頁框,包括目前屬於其他行程的頁框。一個飢渴的行程就直接拿走它所需的,於是頁框數隨需求自由浮動。這通常帶來更好的整體處理量——頁框流向最需要它們的地方——也是多數真實系統採用的做法。但它有個尖銳的缺點:一個行程的錯誤率如今取決於鄰居的行為,而不只取決於它自己的參考字串。一個行為惡劣、貪吃記憶體的行程,可以悄悄地從其他所有行程那裡偷走頁框,把原本跑得好好的行程一起拖垮。

一個行程到底需要多少?工作集

這一切引出了真正的問題:一個行程此刻到底真正需要多少頁框?太少就輾轉;太多就浪費了鄰居本可使用的 RAM。誠實的答案是:需求並非恆定——它隨著程式經歷各個階段而起落。關鍵的洞見來自 Denning,那就是局部性:在任何一刻,一個程式並不是把它的存取均勻地撒在整個位址空間上;它是在一小簇分頁裡密集地工作——它身處的那個迴圈、它正掃描的那個陣列、手邊那寥寥幾個資料結構。那一簇就是它當下的局部性,並隨程式前行而移動。

工作集把這個想法變成作業系統可以量測的東西。挑一個「最近若干次存取」的視窗——比方說最近 10,000 次分頁參考——工作集就是「在那個視窗內被碰過的相異分頁」的集合。它是對程式當下局部性的一個移動估計。如果作業系統給一個行程足夠的頁框來裝下它整個工作集,它就只會在局部性轉移時出錯(這既罕見又無可避免);如果給得更少,這個行程就會在「它一直需要、卻留不住常駐」的那些分頁上不停出錯。所以工作集大小,是對「需要幾個頁框?」遠比均分或純按比例配置更好的答案,因為它追蹤的是程式「現在」在做什麼,而不是它「總共」有多大。

這裡潛藏著一條美妙而簡單的配置規則。把所有可執行行程的工作集大小加總起來;稱它為 D,也就是總需求。如果 D 舒舒服服地小於你擁有的頁框數,每個行程都裝得下自己的工作集,機器就跑得順。但如果 D 爬到超過頁框數,那麼依定義,總有某個行程必須在沒有完整工作集的情況下執行,麻煩之門便就此敞開。總需求與實體頁框之間的那道缺口,正是輾轉現象的前提條件——也就是本階段下一篇、也是最後一篇導覽的主題。

用錯誤率來掌舵,以及配置修不了的事

在滑動視窗上量測精確的工作集代價高昂,所以真實系統常改用一個更便宜的回饋迴路:分頁錯誤頻率(PFF)控制。這個想法很直接。作業系統為每個行程的錯誤率設定一個可接受的區間。如果一個行程的錯誤率爬到上界之上,那它顯然頁框不夠——就多給它一些。如果它的錯誤率掉到下界之下,那它的頁框比所需的還多——就收回一些給別人。你不需要精確知道工作集;你只要盯著症狀(錯誤)並調整劑量(頁框),直到病人安頓進健康的區間裡。

  1. 週期性地量測每個行程最近的分頁錯誤率(每單位時間,或每次參考的錯誤數)。
  2. 如果一個行程高於上界門檻,它就是頁框挨餓:多配給它一些頁框(從空閒池取得,或從一個跑在自己下界門檻以下的行程取得)。
  3. 如果一個行程低於下界門檻,它就是供應過剩:回收它的一部分頁框,好讓別人能用。
  4. 如果需求高到「不把某個行程逼到挨餓就無法讓任何行程降到門檻以下」,那就是這份工作量的頁框根本不夠——此時作業系統必須降低多重程式設計的程度(暫停或換出一整個行程),而不是繼續反覆搬移。

最後那一步,正是配置那個關鍵而令人謙卑的極限。要誠實面對頁框配置能做與不能做的事:它能把一份「足夠」的池子分得好,卻變不出根本不存在的頁框。當可執行行程的需求加總起來確實超過實體 RAM 時,沒有任何配置策略——無論均分、按比例、全域、局部、還是 PFF 掌舵——能給每個行程它的工作集,因為頁框根本不夠分。唯一真正的解藥,是同時跑更少的行程:暫停一個、釋放它的頁框,讓存活下來的得以喘息。也別忘了,虛擬記憶體從來沒讓程式變快;它是讓程式得以執行而已,而當頁框稀缺到這種地步時,它會讓程式戲劇性地變慢。發現這種崩潰、為它的回饋螺旋命名、並把系統拉出來,正是最後一篇導覽接手之處:輾轉現象。