記憶體管理:虛擬記憶體與分頁
記憶體是電腦最珍貴的資源之一。如果同時開啟 10 個程式,每個程式都需要記憶體,但你的電腦只有 16GB RAM——它是怎麼做到的?
答案是:虛擬記憶體 (Virtual Memory)。
🔥 Vibe Prompt
「用 Python 實作一個分頁式記憶體模擬器:支援邏輯位址到實體位址的轉換、分頁表 (Page Table)、TLB 快取、以及 LRU 分頁替換演算法。」
為什麼需要虛擬記憶體?
在早期系統中,程式直接存取實體記憶體。這導致了三個問題:
| 問題 | 說明 | |------|------| | 安全性 | 任何程式都能讀寫其他程式的記憶體 | | 碎片化 | 程式載入/卸載後產生大量記憶體碎片 | | 限制 | 程式大小不能超過實體記憶體 |
虛擬記憶體解決了這些問題:每個程式有自己的「虛擬位址空間」,由作業系統映射到實體記憶體。
分頁 (Paging) 機制
分頁是實現虛擬記憶體的核心技術:
邏輯位址 → [MMU + 分頁表] → 實體位址
- 頁 (Page): 虛擬記憶體被分割成固定大小的區塊(通常 4KB)
- 頁框 (Frame): 實體記憶體也被分割成同樣大小的區塊
- 分頁表 (Page Table): 記錄每個虛擬頁對應到哪個實體頁框
class PageTable:
"""簡易分頁表模擬"""
def __init__(self, page_size=4096):
self.page_size = page_size
# 虛擬頁號 → 實體頁框號
self.mapping = {}
# 實體頁框使用狀態
self.frames = set()
def map_page(self, virtual_page, physical_frame):
self.mapping[virtual_page] = physical_frame
self.frames.add(physical_frame)
def translate(self, virtual_address):
"""將邏輯位址轉換為實體位址"""
page_number = virtual_address // self.page_size
offset = virtual_address % self.page_size
if page_number not in self.mapping:
raise Exception("Page fault! 頁面不存在於記憶體中")
physical_frame = self.mapping[page_number]
physical_address = physical_frame * self.page_size + offset
return physical_address
# 測試
pt = PageTable()
pt.map_page(0, 5) # 虛擬頁 0 → 實體頁框 5
pt.map_page(1, 10) # 虛擬頁 1 → 實體頁框 10
pt.map_page(2, 3) # 虛擬頁 2 → 實體頁框 3
print(f"虛擬位址 0 → 實體位址 {pt.translate(0)}")
print(f"虛擬位址 4096 → 實體位址 {pt.translate(4096)}")
print(f"虛擬位址 8192 → 實體位址 {pt.translate(8192)}")
TLB (Translation Lookaside Buffer)
每次位址轉換都需要查分頁表——如果分頁表在記憶體中,這會很慢。
TLB 是一個硬體快取,儲存最近使用過的位址轉換。有了 TLB,大部分位址轉換可以在 1 個 CPU 週期內完成。
class TLB:
"""TLB 快取模擬(使用 LRU 策略)"""
def __init__(self, size=16):
self.size = size
self.cache = {} # virtual_page → physical_frame
self.access_order = []
def lookup(self, virtual_page):
"""查詢 TLB,回傳實體頁框或 None"""
if virtual_page in self.cache:
# 更新存取順序(LRU)
self.access_order.remove(virtual_page)
self.access_order.append(virtual_page)
return self.cache[virtual_page]
return None
def update(self, virtual_page, physical_frame):
"""更新 TLB"""
if virtual_page in self.cache:
self.access_order.remove(virtual_page)
elif len(self.cache) >= self.size:
# LRU:移除最久未使用的
oldest = self.access_order.pop(0)
del self.cache[oldest]
self.cache[virtual_page] = physical_frame
self.access_order.append(virtual_page)
# 模擬 TLB 效果
tlb = TLB(size=4)
tlb.update(0, 5)
tlb.update(1, 10)
tlb.update(2, 3)
tlb.update(3, 7)
print(f"TLB 查詢頁 0: {tlb.lookup(0)}") # 命中
print(f"TLB 查詢頁 4: {tlb.lookup(4)}") # 未命中
print(f"TLB 查詢頁 1: {tlb.lookup(1)}") # 命中
分頁替換演算法
當實體記憶體已滿,需要載入新頁面時,必須替換掉某個現有頁面。常見演算法:
| 演算法 | 策略 | 優點 | 缺點 | |--------|------|------|------| | FIFO | 替換最早載入的頁面 | 實作簡單 | 可能替換掉常用頁面 | | LRU | 替換最久未使用的頁面 | 效能好 | 需要硬體支援 | | Clock | 近似 LRU,使用參考位元 | 效率與效能的平衡 | 比 LRU 差一點 |
from collections import deque
def lru_page_replacement(page_sequence, num_frames):
"""
LRU 分頁替換演算法
page_sequence: 頁面存取序列
num_frames: 實體頁框數量
回傳: (page_faults, 最終頁框狀態)
"""
frames = []
page_faults = 0
for page in page_sequence:
if page not in frames:
if len(frames) < num_frames:
frames.append(page)
else:
frames.pop(0) # 移除最久未使用的
frames.append(page)
page_faults += 1
else:
# 頁面已存在,移動到最後(表示最近使用)
frames.remove(page)
frames.append(page)
return page_faults, frames
# 測試
sequence = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1]
for frames in [3, 4]:
faults, final = lru_page_replacement(sequence, frames)
print(f"{frames} 個頁框: {faults} 次 page fault")
記憶體碎片化
| 類型 | 說明 | 解決方案 | |------|------|----------| | 外部碎片 | 記憶體中有足夠總空間,但不連續 | 分頁機制(記憶體被分為固定頁框) | | 內部碎片 | 分配給程式的空間未完全使用 | 調整頁面大小 |
實際應用
1. 容器化 (Docker)
Docker 容器共享宿主機的 OS 核心,但每個容器有自己的虛擬位址空間。這正是虛擬記憶體隔離性的應用。
2. 資料庫快取
PostgreSQL 使用 shared buffers 來快取資料頁,本質上就是一種智慧型的分頁管理。
3. JVM (Java Virtual Machine)
JVM 的垃圾回收機制(GC)就是在管理 Java 堆積中的記憶體——類似於 OS 的記憶體管理。
總結
| 概念 | 重點 | |------|------| | 虛擬記憶體 | 讓每個程式擁有獨立的位址空間,不受實體記憶體限制 | | 分頁 (Paging) | 固定大小分割,消除外部碎片 | | 分頁表 | 記錄虛擬頁到實體頁框的映射 | | TLB | 硬體快取加速位址轉換 | | LRU | 最有效的分頁替換策略之一 |
實戰練習
💡 Vibe Coding 練習:請 AI 建立一個「視覺化記憶體管理模擬器」:
- 顯示實體記憶體與虛擬記憶體的佈局
- 模擬分頁表轉換過程
- 實作 FIFO、LRU、Clock 三種替換演算法
- 比較不同演算法的 page fault 次數
- 用圖表呈現效能差異