このページの目次

仮想メモリ

カバー範囲: 頁表トラバーサル (4レベル/5レベル) → PTE形式 → TLBメカニズム → HugeTLB/THP → KPTI分離 カーネルバージョン: 2.6〜6.x。5レベルページングとmTHPの進化を重点的に注釈付きで解説

概要

仮想メモリはLinuxメモリ管理の基盤です。各プロセスを独立したアドレス空間に隔離すると同時に、頁表を用いてディマンドページング (demand paging)、コピー・オン・ライト (COW)、メモリ共有 (mmap MAP_SHARED)、スワップを実現します。

仮想メモリを理解する鍵は、⁠頁表トラバーサル⁠、つまりCPUが仮想アドレスを物理アドレスに変換する方法を理解することにあります。この処理はすべてのメモリアクセスで発生し(MMUハードウェアによって実行)、速度が極めて重要です。


頁表構造

x86-64 4レベルページング (48ビット)

flowchart LR
    VA["仮想アドレス (48-bit)"]

    VA --> PGD["PGD/PML4 インデックス<br/>[47:39] 9 bit<br/>512 entries"]
    PGD -->|"CR3 →"| PUD["PUD インデックス<br/>[38:30] 9 bit<br/>512 entries"]
    PUD --> PMD["PMD インデックス<br/>[29:21] 9 bit<br/>512 entries"]
    PMD --> PTE["PTE インデックス<br/>[20:12] 9 bit<br/>512 entries"]
    PTE --> PAGE["4KB 物理ページ<br/>オフセット [11:0] 12 bit"]

    classDef va fill:#e3f2fd,stroke:#1565c0
    classDef level fill:#f3e5f5,stroke:#7b1fa2
    classDef phys fill:#e8f5e9,stroke:#2e7d32
    class VA va
    class PGD,PUD,PMD,PTE level
    class PAGE phys

なぜ各レベルがちょうど9ビット (512エントリ) なのか? PTEは8バイトであり、512 × 8 = 4KBとなり、ちょうど1つの物理ページに収まるからです。この自己相似な設計により、頁表自体をページアロケータで管理することができます。

5レベルページング (57ビット、カーネル 4.14以降)

flowchart LR
    VA5["仮想アドレス (57-bit)"]
    VA5 --> P5E["PML5 インデックス<br/>[56:48] 9 bit<br/>512 entries"]
    P5E --> P4D["P4D インデックス<br/>[47:39] 9 bit<br/>512 entries"]
    P4D --> PUD5["PUD インデックス<br/>[38:30] 9 bit"]
    PUD5 --> PMD5["PMD インデックス<br/>[29:21] 9 bit"]
    PMD5 --> PTE5["PTE インデックス<br/>[20:12] 9 bit"]
    PTE5 --> PAGE5["4KB 物理ページ<br/>オフセット [11:0]"]

    classDef va fill:#e3f2fd,stroke:#1565c0
    classDef level fill:#f3e5f5,stroke:#7b1fa2
    classDef phys fill:#e8f5e9,stroke:#2e7d32
    class VA5 va
    class P5E,P4D,PUD5,PMD5,PTE5 level
    class PAGE5 phys

CPUがLA57機能 (Ice Lake以降) をサポートしている必要があります。 確認方法: grep la57 /proc/cpuinfo

5レベルページングにより、仮想アドレス空間は256TB (ユーザー128T + カーネル128T) から128PBに拡張されます。これは今日のアプリケーション向けではなく、将来のCXL共有メモリプールやPB級のNVDIMM向けです。

大ページがレベルを減らす仕組み

flowchart LR
    subgraph HUGE["大ページトラバーサル階層"]
        direction LR
        A["PGD"] --> B["PUD"]
        B -->|"2MB: PS=1"| C["PMD → 2MB 大ページ ✅"]
        B -->|"1GB: PS=1"| D["PUD → 1GB 大ページ ✅"]
    end

    subgraph NORMAL["4KBページトラバーサル"]
        direction LR
        E["PGD"] --> F["PUD"] --> G["PMD"] --> H["PTE → 4KB"]
    end

    classDef huge fill:#e8f5e9,stroke:#2e7d32
    classDef normal fill:#e3f2fd,stroke:#1565c0
    class HUGE huge
    class NORMAL normal

PMD/PUDエントリにおいて: bit 7 (PSビット) = 1 → さらに下層に進まず、このレベルが最終的な物理アドレスとなる。

頁表エントリ (PTE) 形式

x86-64 PTE (簡略化版, 64ビット):
  bit 0:   Present       → ページが物理メモリ上にあるか (1) / スワップ済みまたは未マップか (0)
  bit 1:   R/W           → 書き込み可能 (1) / 読み取り専用 (0) — COWの鍵!
  bit 2:   U/S           → Userアクセス可能 (1) / カーネルのみ (0)
  bit 3:   PWT           → ページライトスルーキャッシュ
  bit 4:   PCD           → ページキャッシュ無効化
  bit 5:   Accessed      → ハードウェアが自動的に1に設定 (ページがアクセスされた)
                          → 回収の鍵: アクセスされていないページがスワップの候補
  bit 6:   Dirty         → ハードウェアが自動的に1に設定 (ページが書き込まれた)
                          → ダーティページはスワップ時に書き戻しが必要、クリーンページは破棄可能
  bit 7:   PAT / PS      → PTEレベル: PAT (Page Attribute Table)
                          → PMD/PUDレベル: PS (Page Size, 大ページを示す)
  bit 8:   Global        → CR3切替時にフラッシュしない (カーネル頁表用)
  bit 11:  NX bit 63     → No-Execute (XD), 実行不可
  bit 63:  NX            → bit 11のエイリアス

物理アドレス: bits [51:12] → 4KBアライメントされた物理ページを指す
  残りの bits [11:0] はオフセットであり、アドレス変換には関与しない

AccessedビットとDirtyビットはメモリ回収のハードウェアアクセラレータです。これらがなければ、カーネルは「使用されたか」をシミュレートするためにページフォールトを使用する必要があり、コストは数百倍になります。


TLB (Translation Lookaside Buffer)

基本概念

TLB = MMU内部の頁表キャッシュ (ハードウェア)
  L1 TLB: エントリ約64個、レイテンシ1サイクル (命令用とデータ用は分離)
  L2 TLB: エントリ約1500個、レイテンシ約5サイクル

TLBミス:
  1. ハードウェアが頁表をトラバーサル (x86: ハードウェアページウォーカー、4回のメモリアクセス)
  2. トラバーサル中の任意のレベルで Present=0 → ページフォールト → カーネル処理
  3. PTEが見つかったら、TLBにロード

TLBカバレッジ:
  4KBページ:  L2 TLB 1500エントリ × 4KB = 6MB カバレッジ
  2MBページ:  L2 TLB 1500エントリ × 2MB = 3GB カバレッジ
  1GBページ:  32エントリ (L1のみ) × 1GB = 32GB カバレッジ

PCID / ASID

問題: コンテキストスイッチ時にCR3へ書き込み → TLB内の全 non-global エントリが無効化
       → 新プロセスのすべてのメモリアクセスがTLBミス → 深刻なパフォーマンス低下

解決策:
  x86 PCID (Process-Context Identifier, 12ビット):
    CR3書き込み時にASIDを付与 → TLB内の異なるASIDのエントリ:
      - フラッシュされない (TLB内に保持)
      - 誤ヒットしない (ASID不一致)
    → ホットプロセスが切り替わって戻ってきた際、TLBは依然として大部分がヒット

  ARM64 ASID (8ビットまたは16ビット):
    同様の原理。TTBR0_EL1書き込み時にASIDを指定
    ハードウェアが異なるASIDのTLBエントリを区別

  カーネル態頁表 (Globalビット):
    カーネル頁表のPTEにGlobalビットを設定 → CR3切替時にフラッシュしない
    → カーネル態TLBエントリはプロセス間で共有される

TLB Shootdown

TLB Shootdown: マルチコアTLB一貫性調整フロー CPU 0 · 発信側コア 他CPU · 対象コア 1. 頁表の修正 例: munmap() / mprotect() 2. ローカルTLBの即時フラッシュ INVLPG命令 または CR3書き換え 元のタスクを継続実行 頁表の変更をまだ認識していない (TLB内に古いPTEがキャッシュされている) 3. IPIブロードキャスト送信 Inter-Processor Interrupt IPI割り込みを受信 現在の実行フローを一時停止 IPI ブロック待機 flush_tlb_mm_range() が返らない間 割り込みコンテキストでTLBフラッシュ 該当mmの無効なPTEキャッシュをクリア 完了応答(ACK) 発信側に処理完了を通知 全対象CPUの完了応答 flush_tlb_mm_range() が返り、各コアのTLB状態が一致 Lazy TLB 対象がカーネルスレッド(ユーザー態なし) → フラッシュを遅延 バッチ処理 複数回のフラッシュを結合 → 1回のIPI ハードウェアブロードキャスト AMD INVLPGB / Intel TLB broadcast なぜIPIを送信しなければならないか: ローカルTLBのみをフラッシュするのは不十分である。他のコアは依然として古いPTEをキャッシュしており、 通知をブロードキャストしないと古い変換を使い続け、コア間でメモリビューの不整合やセキュリティ上の問題を引き起こす可能性がある。

HugeTLB

大きな連続した物理メモリを予約し、確実な大ページマッピングを提供します:

// 設定
echo 128 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
// またはブートパラメータ: hugepagesz=1G hugepages=4 hugepagesz=2M hugepages=1024

// 使用方法 1: hugetlbfs
mount -t hugetlbfs -o pagesize=2M none /dev/hugepages
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_HUGETLB, fd, 0);

// 使用方法 2: mmapフラグ
mmap(NULL, size, PROT_READ|PROT_WRITE,
     MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0);

// QEMU/libvirt は HugeTLB を使用して VM の guest RAM をマッピング
// → ネストされた頁表 (stage-2 page table) の TLB ミスを削減

HugeTLBの利点: 利用可能度が確定している(予約時に割り当て)、TLB効率が高い。欠点: 連続した物理メモリが必要、スワップ不可(5.x以前)、小規模データではページ全体が無駄になる。


THP (Transparent Huge Pages)

メカニズム

カーネルは自動的に連続した4KBページを2MBの大ページにマージし、ユーザーからは透明です:

割り当て:
  プロセス malloc(128KB) → ページフォールトリガー → do_anonymous_page()
    → VMがTHPをサポートし、アライメントが合致 → 2MBのゼロページの割り当てを試みる
    → 成功: 全体を2MBの大ページでマッピング (以降の512個のページフォールトを削減)
    → 失敗: 4KBページにフォールバック

khugepaged:
  バックグラウンドのカーネルスレッド → 定期的にプロセスのアドレス空間をスキャン
    → マージ可能な連続した4KBページを発見 → 移行/マージ → 2MB THPにアップグレード
    → THP内の大半のページが解放されたことを発見 → 4KBページに分割
    → スキャンは /sys/kernel/mm/transparent_hugepage/khugepaged/ で制御

戦略制御

cat /sys/kernel/mm/transparent_hugepage/enabled
# always:  THPの割り当てを常に試みる (デフォルト)
# madvise: MADV_HUGEPAGEフラグが設定されたVMAのみ
# never:   無効化

# アプリケーションレベル制御:
madvise(ptr, size, MADV_HUGEPAGE);   // THPの使用を推奨

THPのコスト

利点:
  + TLBミス率が大幅に低下 (512倍のカバレッジ)
  + ページフォールトの削減 (1回の割り当てで512ページ)
  + カーネル頁表のトラバーサルが1レベル減少

欠点:
  - 割り当てレイテンシ (2MBの連続物理メモリを探す必要 → compact/reclaimが必要になる場合あり)
  - 内部フラグメンテーション (有効データ3KBに対して2MBの物理メモリを占有)
  - 特定のワークロードでパフォーマンスが低下 (一部のデータベースは自前で大ページを管理するため)

これが、DBAが頻繁に「echo never > transparent_hugepage/enabled」を推奨する理由です。

mTHP (multi-size THP, 6.12以降)

進行中のソリューション: THPのサイズを固定の2MBにしない
  → 4KB / 16KB / 32KB / 64KB / 128KB / 256KB / 512KB / 1MB / 2MB
  → 必要に応じて適切なサイズを選択
  → 内部フラグメンテーションを削減 (2MB全体を必要としない)
  → TLB効率を維持 (4KBよりはるかに優れている)

これはカーネルコミュニティで活発に開発されている分野です。

KPTI: 頁表分離

Meltdown と修正

Meltdown (2018):
  CPUは予測実行中に U/S ビットをチェックしない → ユーザー態からカーネルメモリを予測読み取り可能
  → キャッシュ内の残留データをサイドチャネル攻撃で読み取れる

KPTI (Kernel Page Table Isolation):
  x86: PTI (Page Table Isolation)
  ARM: KPTI (Kernel Page Table Isolation)

  各プロセスには**2セット**の頁表がある:
    カーネル態頁表: 完全マッピング (user + kernel アドレス)
    ユーザー態頁表: 最小マッピング (user + トランポリンのみ)

  トランポリン (エントリポイント):
    entry_SYSCALL_64 / iret / 割り込み記述子テーブル (IDT) のみを含む
    → カーネル態に入るのに十分 → 直ちにカーネル態頁表に切り替え
    → syscall 返却前にユーザー態頁表に戻り → その後 iret

パフォーマンスへの影響

syscall 集中型ワークロード: ~5%〜30% のパフォーマンス低下
  毎回の syscall: CR3書き込み2回 + TLBミス2回 (トランポリンが小さすぎるため)
  緩和策:
    - PCID: TLBの完全フラッシュを回避 (ただしCR3書き込み自体のオーバーヘッドは残る)
    - Meltdownの影響を受けないCPU (AMD / 新型Intel): KPTIが自動的に無効化

デバッグインターフェース

# 完全な頁表トラバーサル (root権限必要)
cat /proc/<pid>/pagemap  # バイナリ形式、ページごとに8バイト
# bit 63: ページ存在フラグ
# bits [54:0]: PFN (物理ページ番号)

# THP使用状況の確認
grep -i thp /proc/meminfo
# AnonHugePages: 匿名メモリに対するTHPの使用
# ShmemHugePages: shmemに対するTHPの使用

# 5レベルページングが有効かどうかの確認
grep la57 /proc/cpuinfo

# 大ページ情報
cat /proc/meminfo | grep -i huge
cat /sys/kernel/mm/hugepages/hugepages-*/nr_hugepages

# 頁表サイズ (プロセス単位)
cat /proc/<pid>/status | grep VmPTE

参考と拡張

  • カーネルドキュメント⁠: Documentation/x86/x86_64/mm.rst, Documentation/vm/transhuge.rst, Documentation/x86/pti.rst
  • LWN:
    • "5-level paging" (lwn.net/Articles/717293/)
    • "Transparent huge pages in 6.x" (lwn.net/Articles/971579/)
    • "The multi-size THP patch set" (lwn.net/Articles/931701/)
  • ソースファイル⁠:
    • arch/x86/include/asm/pgtable.h — PTE形式の定義
    • arch/x86/mm/tlb.c — TLBフラッシュの実装
    • mm/huge_memory.c — THPのコア
    • mm/hugetlb.c — HugeTLB
    • arch/x86/mm/pti.c — KPTI

キーワード: 頁表, PGD/PUD/PMD/PTE, PTEフラグ (A/D/RW/NX), TLB, PCID, ASID, HugeTLB, THP, khugepaged, mTHP, KPTI, Meltdown