記憶體管理:虛擬記憶體與分頁

記憶體是電腦最珍貴的資源之一。如果同時開啟 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")

多層分頁表 (Multi-Level Page Table)

現代 64 位元作業系統的虛擬位址空間極大(2^64 = 16 EB),如果使用單層分頁表,分頁表本身就會消耗大量記憶體。

解決方案:多層分頁表 — 將分頁表本身也分頁,只載入需要的部分。

虛擬位址分解(4 層分頁,x86-64):
┌─────────┬──────────┬──────────┬──────────┬──────────┐
│ PML4    │ 目錄指標器 │ 目錄     │ 頁表     │ 偏移量   │
│ (9 bit) │ (9 bit)   │ (9 bit)  │ (9 bit)  │ (12 bit) │
└─────────┴──────────┴──────────┴──────────┴──────────┘
                         = 48 位元虛擬位址
class MultiLevelPageTable:
    """簡化版多層分頁表(2 層)"""
    def __init__(self):
        # 第一層:目錄(1024 個項目)
        self.page_directory = {}
        self.page_size = 4096  # 4KB
    
    def map_page(self, virtual_addr, physical_frame):
        page_number = virtual_addr // self.page_size
        # 分割頁號為兩部分
        dir_index = page_number >> 10     # 高 22 bit(第一層)
        table_index = page_number & 0x3FF  # 低 10 bit(第二層)
        
        if dir_index not in self.page_directory:
            self.page_directory[dir_index] = {}
        
        self.page_directory[dir_index][table_index] = physical_frame
    
    def translate(self, virtual_addr):
        page_number = virtual_addr // self.page_size
        dir_index = page_number >> 10
        table_index = page_number & 0x3FF
        
        if dir_index not in self.page_directory:
            raise Exception("Page fault!")
        if table_index not in self.page_directory[dir_index]:
            raise Exception("Page fault!")
        
        physical_frame = self.page_directory[dir_index][table_index]
        offset = virtual_addr % self.page_size
        return physical_frame * self.page_size + offset

多層分頁表的優點

| 優點 | 說明 | |------|------| | 節省記憶體 | 只有使用的分頁表層級才會被分配 | | 稀疏位址空間支援 | 程式通常只使用極小部分的虛擬位址空間 | | 較小的連續記憶體需求 | 不需要一大塊連續的實體記憶體來存放分頁表 |

頁面錯誤 (Page Fault) 處理

當程式存取一個不在實體記憶體中的頁面時,MMU 會引發 頁面錯誤 (Page Fault),交由 OS 處理:

程式存取位址 → MMU 查分頁表 → 頁面不存在 →
    ↓
CPU 觸發 Page Fault 中斷 → OS 中斷處理程式接手
    ↓
1. 檢查位址是否合法(若非法 → Segmentation Fault)
2. 尋找空閒的頁框(若無 → 執行頁面置換演算法)
3. 從磁碟(swap 分區)讀取頁面內容到頁框
4. 更新分頁表
5. 重新執行觸發錯誤的指令
class PageFaultHandler:
    """簡化版頁面錯誤處理模擬"""
    def __init__(self, disk, ram_size=4):
        self.disk = disk  # 模擬 swap 磁碟
        self.ram = {}     # 模擬實體記憶體
        self.ram_size = ram_size
        self.page_table = {}
    
    def access(self, virtual_page):
        if virtual_page in self.ram:
            return self.ram[virtual_page]  # 正常存取
        
        print(f"⚠️ Page Fault! 頁面 {virtual_page} 不在實體記憶體中")
        
        # 從磁碟讀取
        if virtual_page not in self.disk:
            raise Exception("Invalid address!")
        
        # 實體記憶體是否已滿
        if len(self.ram) >= self.ram_size:
            # LRU 置換
            evicted = next(iter(self.ram))
            print(f"  置換頁面 {evicted} 到磁碟")
            self.disk[evicted] = self.ram.pop(evicted)
        
        # 載入頁面
        self.ram[virtual_page] = self.disk[virtual_page]
        print(f"  載入頁面 {virtual_page} 到實體記憶體")
        return self.ram[virtual_page]

Copy-on-Write (COW)

當行程用 fork() 建立子行程時,如果直接複製整個記憶體空間會非常昂貴。Copy-on-Write 技術解決了這個問題:

fork() 時:父子行程共享相同的實體記憶體頁面(全部設為唯讀)
                ↓
任一行程嘗試寫入 → 觸發 Page Fault →
    OS 複製該頁面 → 分配新頁框 → 各自擁有獨立副本
class COWPageTable:
    """寫入時複製模擬"""
    def __init__(self):
        self.pages = {}      # 頁面資料
        self.writable = {}   # 是否可寫
        self.ref_count = {}  # 引用計數
    
    def fork(self):
        """父行程 fork 子行程:共享頁面,全部設為唯讀"""
        child = COWPageTable()
        for page_id, data in self.pages.items():
            child.pages[page_id] = data
            child.writable[page_id] = False
            self.writable[page_id] = False
            self.ref_count[page_id] = self.ref_count.get(page_id, 1) + 1
            child.ref_count[page_id] = self.ref_count[page_id]
        return child
    
    def write(self, page_id, data):
        if page_id not in self.pages:
            self.pages[page_id] = data
            self.writable[page_id] = True
            return
        
        if not self.writable[page_id]:
            # COW:複製頁面
            print(f"📋 COW! 複製頁面 {page_id}")
            new_page_id = f"{page_id}_copy"
            self.pages[new_page_id] = self.pages[page_id].copy()
            self.writable[new_page_id] = True
            self.pages[new_page_id] = data
            return
        
        self.pages[page_id] = data

分段 (Segmentation)

除了分頁之外,另一種記憶體管理方式是 分段 (Segmentation)

| 面向 | 分頁 (Paging) | 分段 (Segmentation) | |------|--------------|-------------------| | 單位 | 固定大小(4KB) | 可變大小(邏輯區段) | | 程式觀點 | 透明(程式看不到分頁) | 可見(程式知道區段) | | 碎片化 | 內部碎片(頁內未用空間) | 外部碎片(區段間空隙) | | 共享 | 難以共享頁面 | 容易共享區段(如 shared library) | | 保護 | 頁層級權限 | 區段層級權限(讀/寫/執行) |

現代作業系統(Linux、Windows)使用 分頁 + 分段混合 或純分頁方式。

ASLR (Address Space Layout Randomization)

ASLR 是一種安全機制,每次行程啟動時隨機化位址空間的配置:

正常配置:
stack: 0x7fff00000000
heap:  0x00600000
code:  0x00400000

ASLR 啟用:
stack: 0x7fff3a4b2000(每次不同)
heap:  0x01a3c000(每次不同)
code:  0x0055e000(每次不同)

這使得攻擊者難以預測記憶體位址,大幅增加了緩衝區溢位等攻擊的難度。

本章總結

| 概念 | 核心重點 | |------|---------| | 虛擬記憶體 | 每個程式有獨立位址空間,不受實體記憶體限制 | | 分頁 | 固定大小分割,消除外部碎片 | | 多層分頁表 | 節省分頁表記憶體,支援稀疏位址空間 | | TLB | 硬體快取加速位址轉換 | | Page Fault | 缺頁時 OS 從磁碟載入頁面 | | 頁面置換 | LRU、FIFO、Clock 演算法 | | COW | fork 時共享頁面,寫入時才複製 | | 分段 | 可變大小區段,程式可見 | | ASLR | 隨機化位址空間以提升安全性 |

實戰練習

💡 Vibe Coding 練習:請 AI 建立一個「記憶體管理互動式學習工具」:

  1. 位址轉換模擬器:輸入虛擬位址,顯示分頁表查詢過程與結果
  2. TLB 效能分析:模擬不同 TLB 大小(16/64/256 項目)的命中率
  3. 頁面置換可視化:動態展示 FIFO、LRU、Clock 的置換過程
  4. 多層分頁表瀏覽器:顯示 x86-64 四層分頁表的位址分解
  5. COW fork 模擬:展示 fork 前後的記憶體共享與複製
  6. Page Fault 統計:記錄並圖表化缺頁率

会員限定無料チュートリアル

このチャプターは登録会員限定の無料コンテンツです!ログインまたは登録してすぐにロックを解除してください。

今すぐログイン / 登録