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

記憶體是電腦最珍貴的資源之一。如果同時開啟 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 建立一個「視覺化記憶體管理模擬器」:

  1. 顯示實體記憶體與虛擬記憶體的佈局
  2. 模擬分頁表轉換過程
  3. 實作 FIFO、LRU、Clock 三種替換演算法
  4. 比較不同演算法的 page fault 次數
  5. 用圖表呈現效能差異

會員專屬免費教學

本章節為註冊會員專屬的免費開放內容!請先登入或註冊會員,即可立即解鎖閱讀。

立即登入 / 註冊