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

配置記憶體與碎裂問題

MMU 給了每個行程一整塊連續記憶體,用基底和界限圍起來。現在來看那道難題:當十幾個行程來來去去,誰決定每個行程拿到哪個空隙——而空閒記憶體又為什麼會不斷碎成一堆小到無法使用的洞,即使加總起來明明還很多?

出租房間的難題

上一篇導覽裡,MMU 給了每個行程剛好一個基底和一個界限,這逼著每個行程塞進一整段不間斷的 RAM——也就是我們說過的連續配置。這讓畫面保持簡單,卻丟給核心一個房地產難題。RAM 是一條長長的房間走廊。行程上門,想要一間某種大小的房,住一陣子,然後離開、把房間交還回來。核心扮演房東,必須決定要把走廊的哪一段給每位新來的客人——而它早期做的選擇,會悄悄塑造稍後還剩下多少可用的空間。

想像一個忙碌的早晨過後的走廊。系統一開始是一整段大空地;行程 A、B、C、D 並排住了進來。接著 B 和 D 退租離開了,於是現在走廊看起來是這樣:A 佔用、然後是 B 留下的一個洞、然後 C 佔用、然後是 D 留下的一個洞、然後一路空到底。核心會保存一份這些空閒洞的清單。當下一個行程上門、需要比如 40 KB 時,房東就掃過那份清單,找一個夠大的洞。有趣的問題不在於某個洞放不放得下——而在於當好幾個洞都行時,要挑哪一個。

最先適配、最佳適配、最差適配

挑洞有三種經典策略,而它們就如名字所說的那樣。最先適配first fit)從頭掃起,抓住第一個夠大的洞,不管後面還有什麼——它很快,因為一找到就停。最佳適配best fit)掃過整份清單,挑出仍然放得下的「最小」的洞,理由是這樣現在浪費的空間最少。最差適配worst fit)則反其道而行:它挑「最大」的洞,盼望那塊大剩料能繼續可用,而不是變成一片沒用的小碎屑。三者回答的是同一個問題——哪個空隙?——卻各憑一股不同的直覺。

Free holes (in order):  [ 100 KB ] ... [ 50 KB ] ... [ 30 KB ] ... [ 80 KB ]
Request: a process needs 45 KB

  first fit -> 100 KB hole   (first one that fits; leftover 55 KB)
  best  fit ->  50 KB hole   (smallest that fits; leftover  5 KB)  <- tiny scrap
  worst fit -> 100 KB hole   (largest hole;       leftover 55 KB)

The 30 KB hole can never hold this 45 KB request -- too small.
同一個 45 KB 的請求,由三種策略各自擺放。最佳適配把當下的剩料壓到最小,但那塊 5 KB 的碎屑往往小到誰都再也用不上。

這裡有一個誠實的意外,讓多數人跌破眼鏡:最佳適配儘管名字充滿希望,通常並不是最佳的。因為它每次都留下盡可能最小的剩料,它往往把走廊撒滿一堆未來任何行程都用不上的細小碎屑,而且每一次都得付出掃完整份清單的代價。數十年的模擬指向最先適配才是實務上的贏家——省空間上幾乎和最佳適配一樣好,又因為提早停下而更快。最差適配在兩方面通常都表現不佳。這個教訓值得記住:一條看起來局部整齊的配置規則,整體上可能很浪費。

兩種浪費

不管你選哪種適配,記憶體都會以兩種截然不同的方式漏成浪費,而把它們區分清楚是值得的。外部碎裂external fragmentation)就是我們剛剛看到的走廊問題:空閒空間是有的,但它散落在被佔用區塊之間的許多小洞裡,於是沒有任何單一一個洞大到能容下下一個請求,即使空閒空間的「總量」明明綽綽有餘。空間就在那兒;只是被剁成了小到無法出租的碎片。這是連續配置的詛咒,而最佳適配靠製造細小碎屑讓它更糟。

內部碎裂internal fragmentation)則是相反的那種浪費:浪費在「已經發出去的區塊內部」的空間。它發生於核心以固定大小的塊來配置、而你的請求又沒能剛好填滿那一塊的時候。當記憶體以 20 KB 為單位發放,而你只要 18 KB,你拿到的卻是一整塊 20 KB——你不需要的那 2 KB 被鎖在你的配置內部,誰也用不上,卻又不會以空閒洞的形式現身。沒有人能租它,因為它名義上是你的;它就這麼被浪費掉了。被浪費的空間困在「之內」,而不是擱淺在「之間」。

把房間滑攏:緊縮與置換

如果外部碎裂已經把你的空閒空間剁成沒用的小碎屑,一種解法是把所有被佔用的區塊都往一端滑攏,讓那些洞在另一端合併成單一一個大洞。這就是緊縮compaction)——就像請每位房客都往大廳方向挪一挪,好讓空房在後頭聚成一間可以出租的大套房。它之所以行得通,全靠上一篇導覽給的禮物:既然 MMU 做的是執行期重定位,搬動一個行程就跟複製它的位元組、改一下它的基底暫存器一樣便宜——不必改寫程式碼。但緊縮並非免費:複製好幾 MB 的記憶體得花上真實的時間,而且在挪移期間那些行程不能執行,所以核心會省著用。

當 RAM 單純地不夠用時,還有第二招更大膽的動作:把一個目前沒在執行的行程,連同它整份記憶體映像複製出去、寫到磁碟上一塊保留的區域,騰出它的房間給別人;稍後再把它複製回來、繼續執行。這就是置換swapping),而它用的那塊磁碟區域就是置換空間。關鍵在於:當一個被換出的行程被換回來時,執行期繫結代表它可以被放到一個和離開時完全不同的實體位置——核心只要設一個新的基底,它就照常執行,彷彿什麼都沒發生。置換讓所有行程的記憶體需求總量,得以超過你真正擁有的實體 RAM,靠的是把閒置的行程停泊到大得多、也慢得多的磁碟上。

為什麼這整條路是死巷(以及出路)

退一步看,真正的麻煩就很清楚了:這篇導覽裡每一個痛點,都追溯到我們繼承來的那一個假設——一個行程必須佔據單一一整塊連續區塊。正是它讓外部碎裂得以發生,正是它逼出了那些彆扭的適配抉擇,也正是它讓緊縮變得必要。聰明的適配策略和偶爾的緊縮只是在管理症狀;它們從不治本,因為病根就是「連續性」本身。只要一個行程需要一整段不間斷的 RAM,散成洞的空閒空間有時就會無法使用,不管你挑得多巧妙。

所以這篇導覽誠實的結論,是一個指向前方的問題。如果我們不再堅持一個行程要住成一整塊呢?如果我們能把一個行程的記憶體剁成許多大小相等的固定小片,再把那些小片撒進「任何」空閒框格裡,不管它們相隔多遠,那麼外部碎裂就會乾脆消失——任何一片空閒都能容下任何一片程式。這個激進的主意就是分頁paging),也是記憶體管理接下來要去的方向。它並非免費(MMU 現在需要一整張表,而不只是一個基底,而你也得換進一點內部碎裂),但它化解了本篇導覽的核心問題。

在那一躍之前,接下來兩篇導覽會從程式自己這一側,把連續記憶體的故事講完。第四篇追蹤連結器和載入器究竟如何建出我們一直在擺放的那個可執行映像——分開編譯的各片和函式庫如何被縫在一起、再被丟進一個選定的區塊裡。第五篇則用分段(segmentation)從另一個角度重訪「單一區塊」這個假設,分段一次給一個行程好幾對基底與界限——程式碼、資料、堆疊等每個邏輯部分各一對。把今天的詞彙帶在身邊:適配策略、兩種碎裂、緊縮與置換,是本階接下來所說的語言。