Home
探索 Uedu
學生控制台
註冊會員/登入
研究知情同意中心
教師控制台
課程設定
支援與訊息
Uptime 數據

UeduGPTs

--

Jupyters

4

UG26 CISOSE26
臺北 AQI 46 · 臺中 AQI 26 · 臺南 AQI 21 · 高雄 AQI 33

AI 回覆桌面通知

AI 助教回覆完成時顯示桌面通知

聊天訊息通知

同學在討論區發送訊息時通知

聲音通知

每當有新通知時播放提示音

記憶體管理

記憶體管理

從邏輯位址到虛擬記憶體:作業系統如何讓 16GB 撐起遠超容量的工作負載

你的電腦只有 16GB,為什麼能同時開 50 個分頁?

你大概有過這樣的經驗:一邊掛著瀏覽器的數十個分頁、一邊跑 Spotify、一邊開著 IDE 編譯程式,工作管理員顯示記憶體用量已經逼近上限,但系統依然撐住沒崩潰。如果你把每個程式宣稱「需要」的記憶體加總起來,數字往往遠超過機器實際安裝的容量。這怎麼可能?

答案藏在作業系統最精巧的一層魔術裡:記憶體管理(memory management)。它讓每個程式都以為自己獨佔一整片廣闊、連續的記憶體,但實際上這些「記憶體」可能東一塊、西一塊地散落在實體晶片各處,甚至有一部分根本不在記憶體裡,而是暫存在硬碟上。要理解這套魔術,我們得先分清楚兩種位址。

作業系統概念示意圖

邏輯位址 vs 實體位址:一場善意的謊言

當你寫的程式存取某個變數時,CPU 送出的位址叫做邏輯位址(logical address),也稱虛擬位址(virtual address)。這是程式眼中的世界:每個行程(process)都以為自己擁有從 $0$ 開始、連續排列的整片位址空間。

但記憶體晶片上真正的格子,用的是實體位址(physical address)。兩者並不相同。中間有一個硬體元件叫做記憶體管理單元(MMU, Memory Management Unit),它在每一次記憶體存取時,即時把邏輯位址翻譯成實體位址。

這層翻譯帶來三個關鍵好處:

  • 隔離與保護:行程 A 看到的位址 $0x1000$ 和行程 B 看到的 $0x1000$,被 MMU 對應到不同的實體位置。A 無法存取 B 的記憶體,一個程式的崩潰不會波及另一個。
  • 彈性配置:程式不需要被載入到一塊連續的實體記憶體,作業系統可以把它拆散塞進任何空閒的角落。
  • 超額供應:實體記憶體不夠時,可以把暫時用不到的部分搬到硬碟,需要時再搬回來——這就是上面那 50 個分頁的祕密。

分頁(paging):把記憶體切成標準磚塊

要實作這層翻譯,最廣泛採用的機制是分頁(paging)。它的核心想法是:把邏輯位址空間切成固定大小的小塊,稱為頁(page);把實體記憶體也切成同樣大小的小塊,稱為頁框(frame)。常見的頁大小是 4KB。

一個頁恰好可以放進一個頁框。作業系統只要維護一張對照表,記錄「第幾頁放在第幾個頁框」,翻譯就完成了。這張表叫做分頁表(page table),每個行程各有一張。

邏輯位址在分頁機制下被拆成兩部分:

邏輯位址 = | 頁號 (page number) | 頁內偏移 (offset) |
  • 頁號:拿去查分頁表,得到對應的頁框號。
  • 頁內偏移:在頁框內的位置,直接照搬不變。

舉例來說,若頁大小為 4KB(即 $2^{12}$ 位元組),那麼邏輯位址的低 12 位元就是偏移,高位則是頁號。翻譯公式為:

$$\text{實體位址} = (\text{頁框號} \times \text{頁大小}) + \text{偏移}$$

因為偏移部分完全不動,分頁不會改變資料在頁框內的相對位置,只是把整塊頁「搬家」到不同的頁框而已。

動手看一個例子

假設頁大小為 4KB($2^{12} = 4096$ 位元組),某行程的分頁表如下:

頁號 頁框號
0 5
1 9
2 2

現在 CPU 要存取邏輯位址 $9000$,翻譯過程如下:

步驟 1:算出頁號與偏移
  頁號   = 9000 // 4096 = 2
  偏移   = 9000 %  4096 = 808

步驟 2:查分頁表
  頁號 2 → 頁框號 2

步驟 3:組合實體位址
  實體位址 = 2 × 4096 + 808 = 8192 + 808 = 9000

這個例子裡實體位址恰好也是 9000(因為頁框號剛好等於頁號),但若存取邏輯位址 $5000$:

頁號   = 5000 // 4096 = 1
偏移   = 5000 %  4096 = 904
頁號 1 → 頁框號 9
實體位址 = 9 × 4096 + 904 = 36864 + 904 = 37768

可以看到,邏輯上相鄰的位址(5000 和 9000),在實體記憶體中可能相隔甚遠。這正是分頁帶來的彈性。

虛擬記憶體(virtual memory):硬碟也來湊一腳

分頁真正威力強大之處,在於它讓虛擬記憶體(virtual memory)成為可能。

核心觀察是:程式在任一時刻其實只用到自己一小部分的頁(這稱為局部性原理(principle of locality))。既然如此,何必把整個程式都塞進實體記憶體?作業系統可以只把當前需要的頁載入頁框,把暫時用不到的頁留在硬碟上的置換空間(swap space)或原始檔案中。

於是分頁表的每個項目除了頁框號,還多了一個存在位元(valid/present bit)

  • 位元為 1:這一頁在實體記憶體中,可直接存取。
  • 位元為 0:這一頁不在記憶體裡,目前躺在硬碟上。

分頁錯誤(page fault):把資料從硬碟救回來

當 CPU 存取一個存在位元為 0 的頁時,MMU 無法完成翻譯,於是觸發一個例外,稱為分頁錯誤(page fault)。注意,「錯誤」這個詞容易誤導——它不是程式 bug,而是一個正常的、由作業系統處理的事件。處理流程如下:

1. CPU 觸發分頁錯誤,控制權交給作業系統
2. 作業系統確認這次存取是合法的(位址在程式空間內)
3. 找一個空閒頁框;若沒有空閒頁框,啟動置換演算法挑一頁犧牲
4. 從硬碟讀入所需的頁,放進該頁框
5. 更新分頁表(填入頁框號,存在位元設為 1)
6. 重新執行剛才那條中斷的指令——這次就能成功了

分頁錯誤的代價極高。從記憶體讀取資料約需數十至上百奈秒($ns$),但從硬碟讀取一頁可能需要數毫秒($ms$)——兩者相差約十萬倍。因此,減少分頁錯誤的次數,是記憶體管理效能的關鍵。

置換演算法(page replacement):該犧牲哪一頁?

當實體記憶體已滿、又必須載入新頁時,作業系統得從現有的頁中挑一個「踢出去」(寫回硬碟或直接丟棄)。挑哪一個?這就是置換演算法要回答的問題。目標是:盡量踢掉「最不可能馬上又被用到」的頁,以降低未來的分頁錯誤。

FIFO(先進先出)

最直覺的策略:最早載入的頁最先被踢出,像排隊一樣。實作簡單,只要維護一個佇列即可。但它有個違反直覺的缺陷——FIFO 可能發生貝雷迪異常(Bélády's anomaly):增加頁框數量,分頁錯誤反而變多。

LRU(最近最少使用,Least Recently Used)

更聰明的策略:踢掉最久沒被使用的頁。它的理論依據正是局部性原理——最近用過的頁,很可能馬上又會用到;很久沒碰的頁,很可能以後也用不到。LRU 不會有貝雷迪異常,效能通常優於 FIFO。

動手看一個例子

假設有 3 個頁框,頁的存取序列為:1, 2, 3, 4, 1, 2, 5。我們比較 FIFO 與 LRU 的分頁錯誤次數(F 表示發生分頁錯誤):

FIFO:

存取 頁框狀態(左為最早載入) 結果
1 [1] F
2 [1, 2] F
3 [1, 2, 3] F
4 [2, 3, 4] F(踢 1)
1 [3, 4, 1] F(踢 2)
2 [4, 1, 2] F(踢 3)
5 [1, 2, 5] F(踢 4)

FIFO 共 7 次分頁錯誤。

LRU:

存取 頁框狀態(右為最近使用) 結果
1 [1] F
2 [1, 2] F
3 [1, 2, 3] F
4 [2, 3, 4] F(踢 1,最久沒用)
1 [3, 4, 1] F(踢 2)
2 [4, 1, 2] F(踢 3)
5 [1, 2, 5] F(踢 4)

這個序列下兩者剛好都是 7 次,但對許多真實工作負載而言,LRU 通常能少踢中「即將被用到」的頁,分頁錯誤更少。讀者可以試著用序列 1,2,3,4,1,2,5,1,2,3,4,5 比較,會看到明顯差異。

輾轉現象(thrashing):系統忙著搬資料卻不做事

如果分配給行程的頁框太少,少到連它最基本需要的頁集合都裝不下,會發生什麼事?每次存取都觸發分頁錯誤,作業系統忙著在記憶體與硬碟間搬頁,CPU 大部分時間都在等硬碟、幾乎沒做任何有意義的計算。這種「搬資料的時間遠超過計算時間」的惡性狀態,稱為輾轉現象(thrashing)

輾轉的典型徵兆是:CPU 使用率低、硬碟燈狂閃、系統卡到動彈不得。解法包括:

  • 工作集模型(working set model)估計每個行程「當前真正需要」的頁數,確保分配的頁框足夠。
  • 必要時暫停或換出整個行程(中期排程),釋放頁框給其他行程,等記憶體寬鬆再恢復。

重點回顧

  • 邏輯位址是程式眼中的位址,實體位址是記憶體晶片上的真實位置,兩者由 MMU 即時翻譯,達成隔離、彈性與超額供應。
  • 分頁把記憶體切成固定大小的頁與頁框,透過分頁表對應;邏輯位址拆成「頁號 + 偏移」,偏移在翻譯中保持不變。
  • 虛擬記憶體讓暫時用不到的頁留在硬碟,存取時觸發分頁錯誤再搬回記憶體;分頁錯誤代價可達記憶體存取的十萬倍。
  • 置換演算法決定記憶體滿時踢哪一頁:FIFO 簡單但可能有貝雷迪異常;LRU 依局部性原理表現較佳。
  • 頁框過少導致輾轉現象,系統忙於搬頁而無法計算,需用工作集模型保障足夠頁框。

深入探討(研究所視角)

多層分頁表與 TLB:翻譯本身也要付出代價

前面把分頁表講得很單純,但真實系統有兩個棘手問題。

第一,分頁表本身很大。在 64 位元架構下,若用單層分頁表,光是表格就會大到無法放進記憶體。解法是多層分頁表(multi-level page table):把頁號再切成數段,逐層查表。例如 x86-64 採用四層(PML4 → PDPT → PD → PT),只有真正用到的子表才需配置,大幅節省空間。代價是查一次表得走訪多層,記憶體存取次數倍增。

第二,每次位址翻譯都要查表,而分頁表放在記憶體裡。這意味著「一次資料存取」竟然需要「多次記憶體存取」(先查表、再取資料),效能無法接受。

硬體的解法是轉譯後備緩衝區(TLB, Translation Lookaside Buffer)——一個位於 MMU 內、專門快取「頁號 → 頁框號」對應的小型高速關聯記憶體。每次翻譯先查 TLB:

  • TLB 命中(hit):直接拿到頁框號,幾乎零延遲。
  • TLB 失誤(miss):才去走訪(可能多層的)分頁表,並把結果填回 TLB。

得益於局部性,TLB 命中率通常高達 99% 以上。有效記憶體存取時間可近似為:

$$\text{EAT} = h \times (t_{\text{mem}}) + (1-h) \times (t_{\text{walk}} + t_{\text{mem}})$$

其中 $h$ 為 TLB 命中率,$t_{\text{mem}}$ 為一次記憶體存取時間,$t_{\text{walk}}$ 為分頁表走訪的額外成本。$h$ 越接近 1,翻譯的額外開銷越能被攤平到可忽略。值得一提的是,行程切換(context switch)時 TLB 往往需要清空(除非用 ASID 標記區分行程),這也是頻繁切換代價高昂的原因之一。

為什麼真實系統用「近似 LRU」而非真正的 LRU

理論上 LRU 表現優異,但精確的 LRU 在硬體上代價過高。要實作真正的 LRU,每次記憶體存取都得更新該頁的「最後使用時間戳」或調整一個排序結構——這對每秒數十億次的存取而言完全不可行。因此真實作業系統採用近似 LRU,最經典的是第二次機會(second-chance)演算法,又稱時鐘演算法(clock algorithm)

其機制如下:硬體為每個頁維護一個參考位元(reference bit),每當該頁被存取,硬體自動把它設為 1。作業系統把所有頁框排成一個環形佇列,並有一個像時鐘指針的指標:

當需要置換一頁時,指針從目前位置開始掃描:
  檢查指針所指頁的參考位元:
    若為 1 → 給它「第二次機會」:把參考位元清為 0,指針前進到下一頁
    若為 0 → 選中這一頁犧牲,置換之,指針停在下一頁

直覺是:參考位元為 1 表示「最近被用過」,先饒它一次並清掉位元;若指針繞一圈回來它仍是 0,代表這一整圈期間都沒被碰過,便可安心踢出。這只需要每頁一個位元、由硬體自動維護,成本極低,卻能近似 LRU 的效果。

進一步的精緻版本同時考慮參考位元修改位元(dirty bit)(即 NRU/增強型時鐘演算法):優先踢掉「沒被參考且沒被修改」的頁,因為未修改的頁不必寫回硬碟,置換成本更低。這體現了系統設計的一貫哲學——用便宜的硬體位元,逼近昂貴的理論最佳解。

與其他主題的連結

記憶體管理並非孤島。它與快取(cache)階層共享「局部性」與「置換」的核心思想,只是層級不同(cache 對記憶體、記憶體對硬碟)。它與檔案系統透過記憶體映射檔案(memory-mapped file)交織,讓檔案存取與記憶體存取統一在分頁機制下。它也是行程排程的考量之一——當記憶體吃緊,中期排程器會換出整個行程以緩解輾轉。理解了這一層,你便能看穿作業系統如何用一張小小的分頁表,編織出「人人都有無限記憶體」的善意謊言。

AI 共讀助教正在陪你讀:記憶體管理
嗨!我是這篇文章的共讀助教,只根據〈記憶體管理〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。