主記憶體管理的背景 (Main Memory Background)
本系列文章內容參考自經典教材 Operating System Concepts, 10th Edition (Silberschatz, Galvin, Gagne)。本文對應章節:Section 9.1 Background。
為什麼需要主記憶體管理?
在前面的 CPU 排程章節中,核心目標是讓 CPU 不要閒置:當一個 Process 等待 I/O 時,OS 可以切換到另一個 Process 執行,提高 CPU 使用率與系統回應速度。這個想法要成立,有一個前提:系統必須同時把多個 Process 留在記憶體中。如果記憶體一次只能容納一個 Process,CPU 排程再聰明也沒有太多發揮空間。
問題也因此出現:記憶體不再是單一程式的私人空間,而是由 OS 與多個 Process 共同使用的共享資源。OS 必須回答幾個基本問題:
| 問題 | 說明 |
|---|---|
| 程式如何被放進記憶體? | 一個 executable file 原本在 disk 上,執行前必須被載入主記憶體 |
| Process 能存取哪些位址? | Process 只能存取自己的記憶體,不應碰到 OS 或其他 Process |
| 程式中的位址如何對應到實體記憶體? | 程式看到的位址不一定等於 RAM 上真正的位址 |
| 如何節省記憶體空間? | 不常用的 routine 或共用 library 不應在每個程式中重複載入 |
本章的主軸就是這些問題。Section 9.1 先建立基礎:硬體能直接存取哪些儲存體、如何保護每個 Process 的記憶體範圍、程式中的位址如何逐步綁定到實體位址,以及動態載入與動態連結如何改善記憶體使用效率。
9.1.1 基本硬體 (Basic Hardware)
CPU 執行指令時,能直接存取的通用儲存體只有兩類:CPU 內部暫存器 (Registers) 與主記憶體 (Main Memory)。機器指令可以把記憶體位址當作 operand,例如從某個 memory address 載入資料,但一般指令不能直接把 disk address 當作 operand。因此,正在執行的指令與正在使用的資料都必須位於 registers 或 main memory 中;如果資料還在 disk,就必須先被搬進記憶體,CPU 才能對它運算。
這裡有一個速度落差。CPU register 通常可以在一個 CPU clock cycle 內存 取,部分處理核心甚至能在每個 clock tick 解碼並執行一個以上的簡單 register operation。主記憶體則必須透過 memory bus 交易才能存取,可能需要許多 CPU cycles。若每次讀寫記憶體都讓 CPU 原地等待,整個系統會被頻繁的 memory access 拖慢。因此硬體通常會在 CPU 與主記憶體之間加入高速 cache,用硬體自動加速常用資料與指令的存取。
當 CPU 執行某條指令時發現所需資料還沒有從主記憶體回來,CPU 可能必須停住,這稱為 memory stall。Cache 的目的不是改變程式語意,而是讓常用資料更可能已經在靠近 CPU 的高速儲存體中,降低 stall 發生的頻率。
在多執行緒核心中,若某個 hardware thread 因記憶體存取而 stall,核心可能切換到另一個 hardware thread 繼續執行,藉此隱藏部分記憶體延遲。
記憶體保護為什麼必須靠硬體?
多個 Process 同時在記憶體中時,OS 必須防止兩種危險:
- User process 修改 OS kernel 的 code 或 data structure。
- User process 讀寫其他 Process 的記憶體。
這件事不能只靠 OS 軟體檢查,原因是 CPU 執行每一條 load/store 指令時,OS 通常不會介入。若每次記憶體存取都進入 kernel 檢查,系統效能會無法接受。因此,記憶體保護必須由硬體在每一次 memory access 發生時自動檢查。
最基本的一種設計是使用兩個暫存器:Base Register 與 Limit Register。Base register 存放 Process 可以存取的最小合法實體位址;limit register 存放這段合法範圍的大小。若 base 是 300040,limit 是 120900,合法範圍就是 300040 到 420939,其中 420940 是第一個不合法位址。
下圖呈現 base 與 limit 如何定義一個 Process 的合法記憶體範圍:
圖中的標記含義如下:
- base:這個 Process 可存取範圍的起始實體位址。
- base + limit:合法範圍結束後的第一個位址,不包含在合法範圍內。
- process 區段:OS 為不同 Process 分配的實體記憶體區域。
- operating system 區段:OS 自己使用的記憶體,user mode 程式不應直接存取。
這張圖的核心是:Process 的合法記憶體範圍不是靠程式自律,而是靠硬體界線定義出來。只要 CPU 每次存取位址時都檢查 base 與 limit,user program 就無法任意越界。
Base/Limit 的硬體檢查流程
當 CPU 在 user mode 執行程式並產生一個 memory address 時,硬體會做兩個比較:
- 檢查 address 是否大於或等於 base。
- 檢查 address 是否小於 base + limit。
只有兩個條件都成立,這次存取才會送到 memory。只要任一條件失敗,硬體就會觸發 trap,讓 OS 接手處理。這種越界存取通常被視為 fatal error,因為它代表程式試圖存取不屬於自己的記憶體。
下圖呈現這個硬體檢查流程:
流程中的每個判斷點如下:
- CPU 先產生一個 address。
- 第一個比較器確認
address >= base。 - 第二個比較器確認
address < base + limit。 - 兩個比較都通過,address 才能進入 memory。
- 任一比較失敗,硬體觸發 trap,OS 將其視為 illegal addressing error。
這張圖最重要的觀念是:記憶體保護發生在 CPU 與 memory 之間,而不是發生在應用程式內部。User program 沒有權限繞過這個檢查。
Base register 與 limit register 只能由 OS 使用 privileged instruction 載入。Privileged instruction 只能在 kernel mode 執行,而一般 user program 只能在 user mode 執行,因此 user program 無法把 base 或 limit 改成對自己有利的值。
這個限制是記憶體保護成立的關鍵。若 user program 可以自行修改 base/limit,它就能把合法範圍擴大到 OS 或其他 Process 的記憶體,整個保護機制會立刻失效。
Kernel mode 的 OS 則擁有不受限制的記憶體存取能力,因為它需要替 user process 載入程式、在程式錯誤時 dump 記憶體內容、讀寫 system call 參數、替 I/O 裝置把資料搬進或搬出 user memory。換句話說,user process 必須被限制在自己的範圍內,但 OS 必須能管理所有範圍。
9.1.2 位址綁定 (Address Binding)
程式一開始通常是一個放在 disk 上的 binary executable file。要執行它,OS 必須把它載入記憶體,建立對應的 Process,然後讓 CPU 有機會執行它。在 Process 執行期間,指令與資料都會從記憶體中讀取;Process 結束後,OS 再回收它佔用的記憶體。
直覺上,程式似乎應該從記憶體位址 0 開始執行。但在多 Process 系統中,這通常不可能也不必要。實體記憶體中可能已經有 OS、其他 Process、buffer、library,因此一個 user process 可以被放在實體記憶體的任何可用位置。這就帶來核心問題:程式原本寫的位址,何時被決定成真正的記憶體位址?
這個把「某個位址表示」映射到「另一個位址表示」的過程,就是 address binding。在程式從 source code 走到 memory 中可執行 image 的過程中,位址通常會經過多種形式:
| 階段 | 位址形式 | 例子 |
|---|---|---|
| Source program | Symbolic address | 變數名稱 count |
| Object file | Relocatable address | 距離 module 開頭 14 bytes |
| Loaded program | Absolute address | 實體位址 74014 |
下圖把使用者程式從 source program 到 memory 中執行的過程串起來:
圖中的流程可以分成三個時間點:
- Compile time:compiler 把 source program 轉成 object file,可能產生 absolute code 或 relocatable code。
- Load time:linker 與 loader 把 object files、其他 object files 合併成 executable file,並把程式載入記憶體。
- Execution time:程式已在 memory 中執行,dynamic libraries 可能在此時被連結進來。
這張圖的核心洞察是:位址不是只有一種,也不是只能在一個時間點決定。OS 與硬體設計可以選擇在 compile time、load time 或 execution time 才把程式中的位址綁定到實際記憶體位置。
三種 Address Binding 時機
Compile-time binding 的前提是:編譯時已經知道 Process 將被放在記憶體的哪個位置。Compiler 可以直接產生 absolute code。這種方式簡單,但很僵硬;只要起始位置改變,就必須重新編譯程式。
Load-time binding 適用於編譯時還不知道程式會被載入哪裡的情況。Compiler 產生 relocatable code,最後的位址綁定延後到 loader 載入程式時才完成。如果起始位置改變,不需要重編譯,只要重新載入即可。
Execution-time binding 則把位址綁定延後到程式執行期間。這代表 Process 在執行中仍可能從一段記憶體移動到另一段記憶體。這種彈性最高,但需要特殊硬體支援,因為每一次 memory reference 都必須在執行時被轉換。現代 OS 多數採用這種方法,後面的 paging 與 virtual memory 都建立在這個方向上。
| 綁定時機 | 優點 | 代價 |
|---|---|---|
| Compile time | 執行時最簡單 | 起始位置改變就要重新編譯 |
| Load time | 起始位置改變只需重新載入 | Process 載入後不容易任意移動 |
| Execution time | Process 執行中仍可搬移,支援更彈性的記憶體管理 | 需要 MMU 等硬體在執行時轉譯位址 |
9.1.3 邏輯位址與實體位址空間 (Logical Versus Physical Address Space)
CPU 產生的位址稱為邏輯位址 (Logical Address)。Memory unit 實際看到並放入 memory-address register 的位址稱為實體位址 (Physical Address)。在本書中,虛擬位址 (Virtual Address) 與 logical address 可視為同義詞。
如果位址在 compile time 或 load time 就完成綁定,logical address 與 physical address 會相同。若使用 execution-time binding,兩者就會不同:程式以為自己在使用某個 logical address,但真正送到 memory 的是轉換後的 physical address。
這裡需要區分兩個集合:
| 名稱 | 定義 |
|---|---|
| Logical Address Space | 程式執行時可能產生的所有 logical addresses |
| Physical Address Space | 這些 logical addresses 實際對應到的 physical addresses |
真正的關鍵不是單一位址,而是「一整個 logical address space」如何被綁定到「另一個 physical address space」。
MMU 的角色
執行時的 logical-to-physical mapping 由硬體裝置 Memory-Management Unit (MMU) 負責。MMU 位在 CPU 與 physical memory 之間:CPU 送出 logical address,MMU 將它轉成 physical address,memory 最後只看到 physical address。
下圖呈現最基本的 MMU 位置:
圖中的資料流如下:
- CPU 產生 logical address。
- Logical address 進入 MMU。
- MMU 依照目前的記憶體管理機制轉換成 physical address。
- Physical memory 只接收轉換後的 physical address。
這張圖的重點是:程式不需要也不應知道真正的 physical address。程式只處理 logical address,轉換責任交給硬體與 OS 管理的映射資訊。
用 Relocation Register 理解動態重定位
最簡單的 MMU 例子可以從 base-register scheme 推廣而來。這裡 base register 被稱為 relocation register。MMU 會把 relocation register 的值加到 user process 產生的每個 logical address 上,形成 physical address。
若 relocation register 的值是 14000:
| Logical Address | Relocation Register | Physical Address |
|---|---|---|
0 | 14000 | 14000 |
346 | 14000 | 14346 |
max | 14000 | 14000 + max |
下圖呈現 logical address 346 如何在 MMU 中被轉換成 physical address 14346:
圖中的標記含義如下:
- relocation register = 14000:這個 Process 的 logical address space 在 physical memory 中的起始位置。
- logical address = 346:CPU 執行 user program 時產生的位址。
- physical address = 14346:MMU 將
14000 + 346相加後送往 memory 的位址。 - MMU:執行位址轉換的硬體。
這張圖的核心洞察是:user program 永遠以為自己在操作 0 到 max 的位址範圍,但 memory 實際收到的是 R 到 R + max 的位址範圍。程式可以建立指向 346 的 pointer、把它存進記憶體、比較它與其他 pointer,這些操作都仍然是在 logical address 的世界中進行。只有當這個值真的被當作 memory address 使用時,MMU 才把它轉換成 physical address。
因為 referenced memory address 的最後實體位置不是在編譯時或載入時決定,而是在每一次 memory reference 發生時才由 MMU 決定。這讓 OS 有能力在執行期間維持「程式看到的位址」與「實體 RAM 的位置」分離。