このページの目次
仮想メモリ
カバー範囲: 頁表トラバーサル (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
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
;
// 使用方法 2: mmapフラグ
;
// 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/ で制御
戦略制御
# always: THPの割り当てを常に試みる (デフォルト)
# madvise: MADV_HUGEPAGEフラグが設定されたVMAのみ
# never: 無効化
# アプリケーションレベル制御:
;
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権限必要)
# bit 63: ページ存在フラグ
# bits [54:0]: PFN (物理ページ番号)
# THP使用状況の確認
# AnonHugePages: 匿名メモリに対するTHPの使用
# ShmemHugePages: shmemに対するTHPの使用
# 5レベルページングが有効かどうかの確認
# 大ページ情報
|
# 頁表サイズ (プロセス単位)
|
参考と拡張
- カーネルドキュメント:
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— HugeTLBarch/x86/mm/pti.c— KPTI
キーワード: 頁表, PGD/PUD/PMD/PTE, PTEフラグ (A/D/RW/NX), TLB, PCID, ASID, HugeTLB, THP, khugepaged, mTHP, KPTI, Meltdown