仮想メモリ
仮想メモリは「物理メモリの限界を超え、各プロセスに独立した広大なアドレス空間を提供する」OSの最も重要な抽象化の一つ。
この章で学ぶこと
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
基本的なプログラミングの知識
関連する基礎概念の理解
1. なぜ仮想メモリが必要か
1.1 仮想メモリがない世界の問題
仮想メモリ以前のメモリ管理:
1950〜1960年代:
→ プログラムは物理アドレスを直接使用
→ 1つのプログラムだけがメモリ全体を使用
→ マルチプログラミング不可
1960年代 ベースレジスタ方式:
→ プロセスごとにベースアドレスを設定
→ アドレス = ベース + オフセット
→ 問題: メモリの断片化、保護が不十分
仮想メモリがない場合の5つの問題:
問題1: メモリ不足物理メモリ: 4GB プロセスA: 3GB必要 プロセスB: 2GB必要 → 同時に実行できない! 仮想メモリなら: 各プロセスに仮想的に8GBの空間を提供 実際に使う部分だけ物理メモリに配置 残りはディスク(スワップ)に退避
問題2: メモリの断片化↑ この隙間 ↑ ここも
→ 合計の空きは十分でも連続領域が不足
問題3: メモリ保護の欠如
→ プロセスAが誤ったアドレスに書き込み
→ プロセスBのメモリを破壊
→ システム全体がクラッシュ
問題4: プログラムの再配置
→ プログラムは特定のアドレスにロードされることを想定
→ 複数プログラムのアドレスが衝突
→ プログラマがアドレスを管理する必要
問題5: メモリ効率
→ 同じライブラリを使う100個のプロセス
→ 100コピーが物理メモリに存在(無駄)
→ 仮想メモリなら1コピーを共有可能
1.2 仮想メモリの解決策
仮想メモリの基本概念:
各プロセスに独立した仮想アドレス空間を提供
→ 物理メモリの配置を意識する必要なし
→ MMU(Memory Management Unit)がアドレス変換を担当
Process A Process B│ MMU │ MMU
↓ ↓物理メモリ(RAM) [A's data][B's data][空き]
↕ スワップ仮想メモリの4つの重要な機能:
1. アドレス変換(Address Translation):
仮想アドレス → 物理アドレスのマッピング
→ ページテーブルによる管理
→ MMUがハードウェアで高速変換
2. メモリ保護(Memory Protection):
プロセス間の完全な分離
→ プロセスAはプロセスBのメモリにアクセス不可
→ 各ページに読み/書き/実行の権限設定
→ カーネル空間とユーザー空間の分離
3. デマンドページング(Demand Paging):
必要な時だけ物理メモリに読み込む
→ プログラム起動時に全コードを読み込まない
→ アクセスされたページだけ物理メモリに配置
→ メモリの効率的な使用
4. メモリ共有(Memory Sharing):
複数プロセスが同じ物理ページを共有
→ 共有ライブラリ(libc等)の共有
→ Copy-on-Write(fork時の最適化)
→ 明示的な共有メモリ(IPC)
仮想アドレス空間のレイアウト(x86-64 Linux):0xFFFFFFFFFFFFFFFF ┌────────────────────────────────────────────┐ カーネル空間(上位半分) → ユーザーからアクセス不可 → 全プロセスで共通のマッピング ├────────────────────────────────────────────┤ 0xFFFF800000000000 ─── カーネル開始 非正規アドレス領域(使用不可) 0x00007FFFFFFFFFFF ─── ユーザー空間上限 ├────────────────────────────────────────────┤ スタック(↓ 下向きに成長) ↓ ↑ mmap領域(共有ライブラリ、大きなmalloc) ↑ ↑ ヒープ(↑ 上向きに成長) ─── brk BSS(未初期化データ) データセグメント(初期化済みグローバル変数) テキストセグメント(プログラムコード) 0x0000000000400000 ─── プログラム開始 NULL ポインタトラップ領域 └────────────────────────────────────────────┘ 0x0000000000000000
ユーザー空間: 128TB(47ビット)
カーネル空間: 128TB(47ビット)
→ 5レベルページテーブル有効時: 各64PB(56ビット)
1.3 ASLR(Address Space Layout Randomization)
ASLR: アドレス空間レイアウトのランダム化
目的:
→ セキュリティ攻撃(バッファオーバーフロー等)の防止
→ スタック、ヒープ、ライブラリのアドレスを起動ごとに変更
→ 攻撃者が特定のアドレスを予測できなくする
ランダム化される領域:スタック: 約8MBのランダムオフセット mmap領域: 約1TBの範囲でランダム ヒープ: 約2MBのランダムオフセット プログラム本体: PIE(Position Independent Executable)有効時のみ
確認方法:
$ cat /proc/sys/kernel/randomize_va_space
0 = 無効
1 = スタック、mmap のみ
2 = 全て(デフォルト)
$ cat /proc/self/maps # 実行するたびにアドレスが変わる
PIE(Position Independent Executable):
→ -fPIE -pie でコンパイル
→ 実行ファイル本体のアドレスもランダム化
→ GCC 6+/Ubuntu 17.10+ ではデフォルト有効
KASLR(Kernel ASLR):
→ カーネルのロードアドレスもランダム化
→ カーネルの脆弱性攻撃を困難に
→ Linux 4.12+ でデフォルト有効
2. ページングの仕組み
2.1 ページとフレーム
ページング:
仮想メモリを固定サイズの「ページ」に分割
物理メモリを同じサイズの「フレーム」に分割
ページ(仮想メモリ側) → フレーム(物理メモリ側)
一般的なページサイズ:ページサイズ アーキテクチャ 用途 4KB x86, ARM 標準(デフォルト) 16KB ARM64(選択可) Apple Silicon 64KB ARM64(選択可) 一部のARM Linux 2MB x86(hugepage) ラージページ(DB、JVM) 1GB x86(hugepage) 超大規模メモリ
なぜ4KBか?
- 小さすぎると: ページテーブルが巨大になる
- 大きすぎると: 内部断片化が増大
- 4KBは歴史的にバランスが良い(1980年代のVAXから)
- ディスクのセクタサイズ(512B→4KB)とも合致
Apple Silicon (M1以降) のページサイズ:
→ macOSは16KBページを採用
→ iOSも16KBページ
→ 4KBの4倍のTLBカバレッジ
→ x86からの移植時にアライメント注意
2.2 アドレス変換
仮想アドレス → 物理アドレスの変換:
仮想アドレス(例: 32bit):│ │
↓ │ページテーブル [0] → フレーム5 [1] → フレーム2 [3] → フレーム8
│ │
↓ ↓= 物理アドレス
計算例:
仮想アドレス = 0x00003A7F
ページサイズ = 4KB = 2^12 = 4096バイト
ページ番号 = 0x00003A7F >> 12 = 0x3 = 3
オフセット = 0x00003A7F & 0xFFF = 0xA7F
ページテーブル: ページ3 → フレーム8
物理アドレス = (8 << 12) | 0xA7F = 0x8A7F
ページテーブルエントリ(PTE):ビットフィールド: [63] NX (No Execute): 実行禁止 [62:52] 予約 / ソフトウェア用 [51:12] 物理フレーム番号(40ビット) [11:9] ソフトウェア用 [8] G (Global): TLBフラッシュ時に保持 [7] PS/PAT: ページサイズ / PAT [6] D (Dirty): 書き込み済みフラグ [5] A (Accessed): 参照済みフラグ [4] PCD: キャッシュ無効化 [3] PWT: ライトスルーキャッシュ [2] U/S (User/Supervisor): ユーザー/カーネル [1] R/W (Read/Write): 読み書き権限 [0] P (Present): ページが物理メモリに存在
重要なフラグの用途:
P=0: ページフォルトが発生
→ デマンドページング、スワップアウト
R/W=0: 書き込み試行でページフォルト
→ Copy-on-Write の実装
U/S=0: ユーザーモードからのアクセスで例外
→ カーネルメモリの保護
NX=1: 実行試行で例外
→ DEP(Data Execution Prevention)
→ スタック上のシェルコード実行を防止
D=1: ページが変更された
→ スワップアウト時にディスクに書き戻しが必要
A=1: ページが参照された
→ ページ置換アルゴリズムで使用
2.3 多階層ページテーブル
なぜ多階層が必要か:
1階層ページテーブル(32bit、4KBページ):
→ 2^20 = 約100万エントリ
→ 各エントリ4バイト → 4MBのテーブル
→ プロセスごとに4MB(多数プロセスで膨大に)
→ しかもほとんどのエントリは使われない
(通常のプロセスは数MB〜数百MB程度しか使わない)
2階層ページテーブル(32bit):│ │
↓ ↓
[ページディレクトリ] → [ページテーブル] → 物理フレーム
→ 使用していない領域のページテーブルは作らない
→ 1MBしか使わないプロセスは少数のページテーブルで済む
x86-64の4階層ページテーブル:
仮想アドレス(48bit):PML4 PDPT PD PT Offset (9) (9) (9) (9) (12)
│ │ │ │
↓ ↓ ↓ ↓
[PML4]→[PDPT]→[PD]→[PT]→ 物理フレーム
各レベル: 512エントリ(9ビット、8バイト/エントリ → 4KB/テーブル)
→ 512 × 512 × 512 × 512 × 4KB = 256TB の仮想空間
階層ごとの役割:PML4 最上位。CR3レジスタが指す。 512エントリ → 最大512個のPDPT 各エントリが512GBの範囲をカバー PDPT 512エントリ → 最大512個のPD 各エントリが1GBの範囲をカバー 1GBラージページも可能 PD 512エントリ → 最大512個のPT 各エントリが2MBの範囲をカバー 2MBラージページも可能 PT 512エントリ → 最大512個の物理フレーム 各エントリが4KBのページ
メモリ節約の例:
100MBを使用するプロセス:
→ 4階層: PML4(1) + PDPT(1) + PD(1) + PT(25) = 28テーブル = 112KB
→ 1階層: 256TB/4KB = 640億エントリ × 8B = 512GB(不可能)
5階層ページテーブル(Intel LA57, Linux 4.14+):
仮想アドレス(57bit):PML5 PML4 PDPT PD PT Offset (9) (9) (9) (9) (9) (12)
→ 128PB(ペタバイト)の仮想アドレス空間
→ サーバー用の巨大メモリ環境向け
→ 一般用途ではまだ不要
3. TLB(Translation Lookaside Buffer)
3.1 TLBの基本
問題: 毎回ページテーブルを辿ると遅い
→ 4階層 = 4回のメモリアクセス
→ 通常のメモリアクセス1回が5回に!(400%のオーバーヘッド)
TLB: ページテーブルのキャッシュ(CPU内蔵のハードウェア)
仮想アドレス
│
├──→ [TLB] ──→ ヒット! → 物理アドレス(1サイクル)
│ │
│ ミス
│ ↓
└──→ [ページテーブルウォーク] → 物理アドレス + TLBに登録
(4回のメモリアクセス、数百サイクル)
TLBの内部構造:TLBエントリ: ┌──────────┬──────────┬───────┬────────┐ 仮想PN 物理FN ASID フラグ (タグ) (データ) R/W/X └──────────┴──────────┴───────┴────────┘ 連想記憶(Content-Addressable Memory): → 仮想PN で全エントリを同時検索 → 1サイクルで結果を返す → ハードウェアコストが高い(エントリ数制限)
TLBの典型的なサイズ:Intel Core i7: L1 iTLB: 128エントリ(4KBページ)、8-way L1 dTLB: 64エントリ(4KBページ)、4-way L2 sTLB: 1536エントリ(4KBページ)、12-way + ラージページTLB(2MB/1GB): 数十エントリ Apple M1: L1 iTLB: 192エントリ(16KBページ) L1 dTLB: 160エントリ(16KBページ) L2 TLB: 3072エントリ → 非常に小さい(数百〜数千エントリ) → それでもヒット率99%以上(局所性の恩恵)
TLBカバレッジ:
L1 dTLB 64エントリ × 4KB = 256KB
L2 sTLB 1536エントリ × 4KB = 6MB
→ ワーキングセットが6MBを超えるとTLBミスが増加
ラージページ(2MB)の場合:
L1 dTLB 32エントリ × 2MB = 64MB
→ TLBカバレッジが大幅に拡大!
3.2 TLBの管理
TLBミスのコスト:
ヒット: 1サイクル(〜0.3ns @3GHz)
L2 TLBヒット: 〜5サイクル(〜1.5ns)
ミス(ページテーブルウォーク):
ページテーブルがL1キャッシュに: 〜20サイクル
ページテーブルがL2キャッシュに: 〜50サイクル
ページテーブルがメモリに: 〜200-400サイクル
→ TLBヒット率を保つことがパフォーマンスの鍵
コンテキストスイッチとTLB:
問題: プロセスAのTLBエントリはプロセスBでは無効
→ コンテキストスイッチ時にTLBを無効化する必要
方法1: TLBフラッシュ(全エントリ無効化)
→ 単純だが、スイッチ後にTLBミスが頻発
→ コンテキストスイッチのコストが増大
方法2: ASID(Address Space Identifier)
→ 各TLBエントリにプロセスのIDを付与
→ 異なるASIDのエントリは自動的に無視
→ TLBフラッシュ不要!
→ x86ではPCID(Process Context Identifier)と呼ぶ
→ Linux 4.14+でPCIDを活用TLBフラッシュなし(PCID有効): Process A(PCID=1): [VPN=3→PFN=5, PCID=1] Process B(PCID=2): [VPN=3→PFN=8, PCID=2] → 同じVPN=3でもPCIDで区別される → コンテキストスイッチ時にTLBが保持される → Spectre/Meltdown対策でPCIDの重要性が増大
KPTI(Kernel Page Table Isolation):
Meltdown脆弱性(2018年)への対策
→ ユーザーモードではカーネルのページマッピングを削除
→ コンテキストスイッチ時にページテーブルを切り替え
→ TLBフラッシュが必要 → PCIDで緩和
→ パフォーマンス影響: 0.1%〜5%(ワークロード依存)
3.3 ラージページ(Huge Pages)
ラージページの利点:
4KBページ:
4GBメモリ = 1,048,576 ページ = 100万TLBエントリが必要
→ TLBはせいぜい数千エントリ → ミスが頻発
2MBラージページ:
4GBメモリ = 2,048 ページ = 2千TLBエントリで済む
→ TLBミスが大幅に減少(約500分の1)
1GBラージページ:
4GBメモリ = 4 ページ = 4TLBエントリで済む
→ TLBミスがほぼゼロ
パフォーマンス向上:ワークロード 4KB 2MB(huge) PostgreSQL (大規模DB) 基準 5〜15%向上 Redis (メモリDB) 基準 3〜8%向上 JVM (大規模ヒープ) 基準 10〜20%向上 科学計算 基準 5〜30%向上 DPDK(ネットワーク) 基準 必須
Linuxでのラージページ設定:
1. Transparent Huge Pages (THP):
→ カーネルが自動的にラージページを使用
→ アプリケーションの変更不要
2. HugePages (静的):
→ 事前にラージページを予約
→ hugetlbfsを使用
→ データベースで推奨
# ラージページの設定と確認
# Transparent Huge Pagesの状態確認
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never
# THP を madvise モードに(推奨)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# → アプリが明示的に要求した場合のみラージページ使用
# 静的HugePagesの予約
echo 1024 > /proc/sys/vm/nr_hugepages # 2MB × 1024 = 2GB予約
cat /proc/meminfo | grep -i huge
# HugePages_Total: 1024
# HugePages_Free: 1024
# HugePages_Rsvd: 0
# Hugepagesize: 2048 kB
# 1GB HugePagesの予約(ブート時パラメータ)
# GRUB_CMDLINE_LINUX="hugepagesz=1G hugepages=4"
# → 1GB × 4 = 4GBの1GBラージページを予約
# hugetlbfsのマウント
mount -t hugetlbfs none /mnt/huge
# PostgreSQL でのHugePages使用
# postgresql.conf:
# huge_pages = try
# shared_buffers = 8GB
// madviseによるTransparent Huge Pages の要求
#include <sys/mman.h>
#include <string.h>
int main () {
size_t size = 256 * 1024 * 1024 ; // 256MB
// 2MB境界にアライメントされた領域を確保
void * ptr = mmap ( NULL , size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
- 1 , 0 );
// カーネルにラージページの使用を要求
madvise (ptr, size, MADV_HUGEPAGE);
// メモリ使用...
memset (ptr, 0 , size);
munmap (ptr, size);
return 0 ;
}
4. ページフォルトとデマンドページング
4.1 デマンドページングの仕組み
デマンドページング:
プログラム起動時に全ページを読み込まない
→ アクセスされた時に初めて物理メモリに配置
→ 「怠惰な」メモリ管理(Lazy Allocation)
プログラム起動時:
1. ELFヘッダを読み、仮想アドレスマッピングを設定
2. 実際のページは物理メモリに配置しない(PTEのP=0)
3. プログラムカウンタをエントリポイントに設定
4. 最初の命令フェッチでページフォルト発生
5. カーネルがそのページをディスクから読み込み
6. 以降、アクセスごとに必要なページだけ読み込み
利点:
- 起動が高速(全コードを読まなくてよい)
- メモリ効率が良い(使わないページは物理メモリを消費しない)
- 大きなプログラムでも少ないメモリで実行可能
100MBのプログラムで実際に使うのが20MBなら:
→ 物理メモリ20MBだけで実行可能
→ 残り80MBは物理メモリを消費しない
4.2 ページフォルトの種類と処理
ページフォルトの処理フロー:
1. CPUが仮想アドレスにアクセス
2. MMUがページテーブルを参照
3. PTEの Present ビットが 0(またはアクセス権限違反)
4. ページフォルト例外(#PF)が発生
5. CPUがフォルトアドレスをCR2レジスタに保存
6. カーネルのページフォルトハンドラが呼ばれる
ページフォルトの3種類:
1. マイナーページフォルト(Minor / Soft):
→ ディスクI/Oが不要
→ 新しい物理ページの割り当てとゼロクリアのみ
→ 例: mallocで確保した領域への初回アクセス
→ コスト: 〜1μs
2. メジャーページフォルト(Major / Hard):
→ ディスクI/Oが必要
→ スワップ領域やファイルからの読み込み
→ 例: スワップアウトされたページへのアクセス
→ コスト: 〜1-10ms(HDD)、〜0.1ms(SSD)
3. 不正アクセス(Invalid):
→ アクセス権限違反またはマッピングされていないアドレス
→ カーネルがSIGSEGV(Segmentation Fault)を送信
→ プロセスがクラッシュ
カーネルの判断フロー:ページフォルト発生 ├─ アドレスがVMA(仮想メモリ領域)内か? NO → SIGSEGV(不正アクセス) YES ↓ ├─ アクセス権限は正しいか? NO → SIGSEGV(権限違反) YES ↓ ├─ ページは初めてアクセスされた? YES → 新しいフレームを割り当て(Minor) NO ↓ ├─ ページはスワップに退避されている? YES → スワップから読み込み(Major) NO ↓ ├─ Copy-on-Write ページへの書き込み? YES → ページをコピー(Minor) NO ↓ └─ ファイルマッピングのページ? YES → ファイルから読み込み(Major)
4.3 Copy-on-Write(COW)
Copy-on-Write:
fork() の最適化。親子プロセスが同じ物理ページを共有し、
書き込みが発生した時だけコピーする。
fork() 直後:
Parent ChildVPN 0 ─── ──┐ ┌─── ── VPN 0 VPN 1 ─── ──┼──┼─── ── VPN 1 VPN 2 ─── ──┼──┼─── ── VPN 2
↓ ↓子プロセスがVPN 1に書き込み:
1. ページフォルト発生(read-only ページへの書き込み)
2. カーネルが Copy-on-Write と判断
3. 新しい物理フレームを確保
4. 元のページの内容をコピー
5. 子プロセスのPTEを新しいフレームに変更
6. 両方のPTEをread/writeに戻す
7. 書き込みを再実行
Parent Child ┌──────────┐ ┌──────────┐
│ VPN 0 ───│──┐ ┌───│── VPN 0 │ ← まだ共有
│ VPN 1 ───│──┼──│───│── VPN 1 │ ← コピーされた
│ VPN 2 ───│──┼──┼───│── VPN 2 │ ← まだ共有
└──────────┘ │ │ └──────────┘↓ ↓ │ ┌──────────┐ │
│ FN 5 │ │ ← 共有中
│ FN 8 │ │ ← 親のみ
│ FN 12 │ │ ← 共有中
└──────────┘ │↓ ┌──────────┐
│ FN 20 │ ← 子のVPN 1用コピー
└──────────┘利点:
- fork() がほぼ瞬時(ページテーブルのコピーだけ)
- fork() + exec() パターンで不必要なコピーを回避
- 読み取り専用データは永続的に共有
4.4 ページ置換アルゴリズム
物理メモリが満杯の時:
→ どのページを追い出す(evict)か?
→ ページ置換アルゴリズムの出番
1. FIFO(First-In, First-Out):
最も古いページを追い出す
→ 実装が簡単
→ 欠点: 頻繁に使うページも古ければ追い出す
→ Béládyの異常: メモリを増やすとフォルト増加
Béládyの異常の例:
参照列: 1 2 3 4 1 2 5 1 2 3 4 5
3フレーム: 9回のページフォルト
4フレーム: 10回のページフォルト ← メモリ増やしたのに悪化!
2. OPT(最適アルゴリズム / Bélády's Algorithm):
将来最も遅く使われるページを追い出す
→ 理論上最適だが未来予知が必要で実装不可能
→ 他のアルゴリズムの性能評価のベースラインとして使用
3. LRU(Least Recently Used):
最も長い間使われていないページを追い出す
→ 時間的局所性に基づく(最近使ったものは再び使われる)
→ OPTに近い性能
→ 実装コストが高い(完全なLRUにはタイムスタンプかスタックが必要)
完全LRUの実装コスト:
方法1: タイムスタンプ
→ 毎アクセスでタイムスタンプ更新 → オーバーヘッド大
方法2: スタック
→ アクセスのたびにスタックの先頭に移動 → O(n)
→ いずれもハードウェアで実装が困難
4. Clock(時計アルゴリズム / Second Chance):
LRUの近似。Accessedビットを使用。↑ clock hand
アルゴリズム:
1. clock hand が指すページの Accessed ビットを確認
2. A=1 → A=0 にして次へ(セカンドチャンス)
3. A=0 → そのページを追い出す
→ Linuxが採用(改良版:2つのリスト active/inactive)
5. LFU(Least Frequently Used):
最も使用頻度が低いページを追い出す
→ カウンタベース
→ 一時的に多く使われたページが残り続ける問題
→ 減衰カウンタで対策
6. Working Set アルゴリズム:
ワーキングセット(最近一定時間内にアクセスされたページ集合)
をメモリに保持
→ ワーキングセット外のページを追い出す
→ スラッシング防止に効果的
比較:アルゴリズム ページ 実装 Béládyの 実用 フォルト コスト 異常 OPT 最少 不可能 なし ベンチ LRU 少ない 高い なし 近似版 Clock やや少 低い なし Linux FIFO 多い 最低 あり 教育用 LFU 少ない 中 なし キャッシュ
4.5 Linuxのページ回収
Linuxのページ回収(Page Reclaim):
2つのLRUリスト:Active List: → 最近アクセスされたページ → ここにあるページは基本的に追い出さない Inactive List: → しばらくアクセスされていないページ → ここから追い出しの候補を選ぶ ページの流れ: 新規 → Inactive → アクセス → Active → 長期間未アクセス → 追い出し Active → 長期間未アクセス → Inactive
さらに4つのリスト(Linux 2.6.28+):
- Active Anonymous: スタック、ヒープ(スワップ対象)
- Inactive Anonymous: 同上(追い出し候補)
- Active File: ファイルキャッシュ(ファイルに書き戻し)
- Inactive File: 同上(追い出し候補)
kswapd:
→ バックグラウンドのページ回収デーモン
→ 空きページが low watermark を下回ると起動
→ Inactive リストからページを回収
→ high watermark まで回収したら休止
Direct Reclaim:
→ ページ割り当て時に空きがない場合
→ プロセス自身がページ回収を実行(ブロック)
→ パフォーマンスに大きな影響
ウォーターマーク:空きメモリ量 ┃ ┃ ┃ ─── high watermark ─── kswapd停止 ┃ ┃ ─── low watermark ─── kswapd起動 ┃ ┃ ─── min watermark ─── direct reclaim開始 ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
vm.swappiness パラメータ:
→ Anonymous ページ vs File ページの回収比率を制御
→ 0: できるだけスワップしない(File優先回収)
→ 60: デフォルト(バランス)
→ 100: Anonymous と File を同等に扱う
→ 200: Anonymous を積極的にスワップ(zswap使用時)
5. スラッシングとOOM
5.1 スラッシング
スラッシング(Thrashing):
物理メモリが極端に不足し、ページフォルトが頻発する状態
→ 多くの時間がページI/Oに費やされる
→ CPU利用率が極端に低下
→ システムが事実上停止する
スラッシングの悪循環:
1. 物理メモリ不足
2. ページフォルトが頻発
3. I/O待ち時間が増大
4. CPU利用率が低下
5. OSが「CPUが暇→もっとプロセスを投入しよう」と判断
6. さらにメモリが不足
7. さらにページフォルトが増加
8. → 悪化の一途CPU利用率 ┃ ╱╲ ┃ ╱ ╲ ┃ ╱ ╲ ┃ ╱ ╲ ← スラッシング開始 ┃ ╱ ╲ ┃ ╱ ╲ ┃ ╱ ╲ ┃ ╱ ╲ ┃──╱────────────────╲────── ┗━━━━━━━━━━━━━━━━━━━━━━━ 少ない 多い 非常に多い プロセス数
対策:
1. メモリの追加(根本対策)
2. スワップの最適化(SSD使用、zswap/zram)
3. プロセス数の制限
4. OOM Killer によるメモリ解放
5. ワーキングセットの監視と制御
6. cgroups でのメモリ制限
5.2 OOM Killer
OOM Killer(Out-Of-Memory Killer):
メモリが完全に枯渇した時に、プロセスを強制終了して
空きメモリを確保するLinuxの仕組み
発動条件:
→ 物理メモリ + スワップが全て使い切られた
→ ページ回収が十分なメモリを確保できない
→ カーネルがメモリ確保に失敗
OOM Scoreの計算:/proc/<pid>/oom_score: 0〜1000の値。高いほど kill されやすい 考慮される要因: - メモリ使用量(最も重要) - スワップ使用量 - プロセスの存在時間 - root権限(-30のボーナス) - oom_score_adj による調整 /proc/<pid>/oom_score_adj: -1000〜+1000 でスコアを調整 -1000: OOM Killer対象外(DBサーバー等に設定) +1000: 最優先でkill 0: デフォルト
重要プロセスの保護:
# PostgreSQL を OOM Killer から保護
echo -1000 > /proc/$(pidof postgres)/oom_score_adj
# systemd でのOOM設定
# /etc/systemd/system/myservice.service
[Service]
OOMScoreAdjust=-900
MemoryMax=4G
MemoryHigh=3G
OOM発生の確認:
# dmesg でOOM Killerのログを確認
dmesg | grep -i "out of memory"
dmesg | grep -i "oom"
# "Out of memory: Kill process 1234 (java) score 850"
# メモリ管理の監視コマンド
# メモリ使用状況の概要
free -h
# total used free shared buff/cache available
# Mem: 32Gi 8.0Gi 2.0Gi 256Mi 22Gi 23Gi
# Swap: 8.0Gi 0B 8.0Gi
# available: 新しいプロセスが使えるメモリ量
# → free + buff/cache(回収可能分)
# 詳細なメモリ情報
cat /proc/meminfo
# 重要な項目:
# MemTotal: メモリ総量
# MemFree: 完全に空いているメモリ
# MemAvailable: 利用可能なメモリ(推定値)
# Buffers: ブロックデバイスのバッファ
# Cached: ページキャッシュ
# SwapTotal: スワップ総量
# SwapFree: スワップ空き
# Active: Activeリスト
# Inactive: Inactiveリスト
# AnonPages: Anonymous ページ
# Mapped: mmapされたページ
# Shmem: 共有メモリ
# HugePages_Total: ラージページ数
# ページフォルトの確認
/usr/bin/time -v ls 2>&1 | grep "page faults"
# Minor (reclaiming a frame): 234
# Major (requiring I/O): 0
# プロセスのメモリ情報
cat /proc/self/status | grep -E "^(Vm|Rss|Threads)"
# VmPeak: 最大仮想メモリサイズ
# VmSize: 現在の仮想メモリサイズ
# VmRSS: 常駐メモリ(実際に使用中の物理メモリ)
# VmSwap: スワップ使用量
# プロセスのメモリマップ
cat /proc/self/maps
pmap -x $$ # 拡張情報付き
6. 仮想メモリの実践
6.1 メモリ使用量の確認
# プロセスのメモリマップ詳細
cat /proc/self/maps
# アドレス範囲 権限 オフセット デバイス inode パス
# 55a0b1200000-55a0b1201000 r--p 00000000 08:01 123 /usr/bin/bash
# 55a0b1201000-55a0b12e8000 r-xp 00001000 08:01 123 /usr/bin/bash
# 55a0b12e8000-55a0b1320000 r--p 000e8000 08:01 123 /usr/bin/bash
# 55a0b1320000-55a0b1324000 r--p 0011f000 08:01 123 /usr/bin/bash
# 55a0b1324000-55a0b132d000 rw-p 00123000 08:01 123 /usr/bin/bash
# 55a0b27d0000-55a0b2910000 rw-p 00000000 00:00 0 [heap]
# 7f5c2a000000-7f5c2c000000 rw-p 00000000 00:00 0 (anonymous)
# ...
# 7fff45600000-7fff45621000 rw-p 00000000 00:00 0 [stack]
# 7fff45764000-7fff45768000 r--p 00000000 00:00 0 [vvar]
# 7fff45768000-7fff4576a000 r-xp 00000000 00:00 0 [vdso]
# 権限の意味:
# r = read, w = write, x = execute
# p = private (COW), s = shared
# macOS の場合
vmmap $$ # プロセスのメモリマップ
vm_stat # システム全体のメモリ統計
# smaps: 詳細なメモリ使用情報(Linux)
cat /proc/self/smaps_rollup
# Rss: 1234 kB # 物理メモリ使用量
# Pss: 890 kB # 共有を按分した使用量
# Pss_Anon: 456 kB # Anonymous
# Pss_File: 434 kB # ファイルマッピング
# Referenced: 1200 kB # 参照されたページ
# Swap: 0 kB # スワップ使用量
6.2 アドレス変換の手計算
演習: ページサイズ4KB、32bit仮想アドレス空間で:
仮想アドレス 0x00003A7F のアクセス:
Step 1: ページ番号とオフセットに分割
4KB = 2^12 → オフセット12ビット
仮想アドレス = 0x00003A7F = 0000 0000 0000 0000 0011 1010 0111 1111
ページ番号 = 上位20ビット = 0x00003 = 3
オフセット = 下位12ビット = 0xA7F = 2687
Step 2: ページテーブル参照
ページ3 → フレーム7
物理アドレス = (7 << 12) | 0xA7F = 0x7A7F
Step 3: TLBにこのエントリがない場合の処理
1. TLBミス発生
2. MMUがページテーブルウォークを開始
3. CR3レジスタからPML4テーブルのアドレスを取得(x86-64の場合)
4. 各レベルのテーブルを辿って物理フレーム番号を取得
5. TLBに新しいエントリを登録
6. 必要に応じて古いTLBエントリを追い出す
7. 物理アドレスでメモリアクセスを再実行
演習2: 64bitアドレス変換(x86-64)
仮想アドレス: 0x00007F5C2A001234
48ビット仮想アドレス: 0x7F5C2A001234
ビット分割:
PML4 = ビット47-39 = 0x0FE = 254
PDPT = ビット38-30 = 0x170 = 368
PD = ビット29-21 = 0x150 = 336
PT = ビット20-12 = 0x001 = 1
Offset = ビット11-0 = 0x234 = 564
→ PML4[254] → PDPT[368] → PD[336] → PT[1] → 物理フレーム → + 0x234
6.3 パフォーマンスチューニング
# 仮想メモリのチューニングパラメータ
# スワップの積極性(0=最小、100=最大、200=zswap時最大)
cat /proc/sys/vm/swappiness # デフォルト: 60
echo 10 > /proc/sys/vm/swappiness # DB サーバー推奨値
# ダーティページの書き込みタイミング
cat /proc/sys/vm/dirty_ratio # デフォルト: 20(%)
cat /proc/sys/vm/dirty_background_ratio # デフォルト: 10(%)
# dirty_background_ratio: バックグラウンド書き込み開始
# dirty_ratio: 同期書き込み強制(プロセスがブロック)
# 推奨設定(SSD、大メモリの場合)
echo 5 > /proc/sys/vm/dirty_background_ratio
echo 10 > /proc/sys/vm/dirty_ratio
# オーバーコミット制御
cat /proc/sys/vm/overcommit_memory
# 0: ヒューリスティック(デフォルト)
# 1: 常にオーバーコミット許可
# 2: オーバーコミット禁止(swap + ratio × physical)
cat /proc/sys/vm/overcommit_ratio # デフォルト: 50(%)
# ウォーターマークの調整
cat /proc/sys/vm/min_free_kbytes # 最小空きメモリ
echo 262144 > /proc/sys/vm/min_free_kbytes # 256MBに設定
# THP の設定
cat /sys/kernel/mm/transparent_hugepage/enabled
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
# NUMA のメモリポリシー
numactl --hardware # NUMAトポロジの確認
numactl --interleave=all ./my_program # メモリを均等配置
numactl --membind=0 --cpunodebind=0 ./my_program # ノード0に固定
# zswap(圧縮スワップキャッシュ)
echo 1 > /sys/module/zswap/parameters/enabled
echo lz4 > /sys/module/zswap/parameters/compressor
echo 20 > /sys/module/zswap/parameters/max_pool_percent
# zram(圧縮RAMディスクのスワップ)
modprobe zram
echo 4G > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon -p 100 /dev/zram0 # 高優先度でスワップに追加
7. 仮想メモリの高度なトピック
7.1 メモリオーバーコミット
Linuxのメモリオーバーコミット:
malloc() 成功 ≠ 物理メモリ確保Process A: malloc(1GB) → 成功(仮想メモリのみ) Process B: malloc(1GB) → 成功(仮想メモリのみ) Process C: malloc(1GB) → 成功(仮想メモリのみ) 物理メモリ: 2GBしかない → 3GB分のmalloc が成功している! 実際にアクセスするまで物理メモリは割り当てない → 全プロセスが1GBずつ使おうとすると... → OOM Killer 発動
なぜオーバーコミットするのか?
- 多くのプログラムはmalloc した分を全て使わない
- fork() 時に親プロセスのメモリを全コピーしなくて済む(COW)
- 実際の使用パターンではうまく機能する
オーバーコミットが問題になるケース:
- Java: 起動時に大きなヒープを確保(-Xmx8g等)
- Redis: fork + COW(バックグラウンド保存時)
- 科学計算: 大きな配列の一括確保
overcommit_memory の設定:
0(デフォルト): ヒューリスティック判定
→ 明らかに不合理な要求は拒否
→ 「空きメモリ + スワップ」より大きい要求は拒否の可能性
1: 常に許可
→ JVM等で使用(起動時の大きなmalloc用)
2: 厳密な制限
→ CommitLimit = swap + physical × ratio
→ Committed_AS がこれを超えるとmallocが失敗
→ OOM Killerが発動しない保証
7.2 KSM(Kernel Same-page Merging)
KSM: 同一内容のページを自動的に共有
→ 仮想化環境で特に効果的(複数VMが同じOSを実行)
→ メモリの重複排除KSM前: VM1: [Page A: "Hello"] [Page B: "World"] VM2: [Page C: "Hello"] [Page D: "Linux"] → 4ページ分の物理メモリ使用 KSM後: VM1: [Page A: "Hello" ─┐ [Page B: "World"] VM2: [Page C: "Hello" ─┘ [Page D: "Linux"] → 3ページ分("Hello"は共有) 書き込み時: COWでコピーが発生
設定:
echo 1 > /sys/kernel/mm/ksm/run
cat /sys/kernel/mm/ksm/pages_shared # 共有されたページ数
cat /sys/kernel/mm/ksm/pages_sharing # 共有に参加しているページ数
cat /sys/kernel/mm/ksm/pages_unshared # ユニークなページ数
注意: KSMはCPU負荷がある(定期的にページを比較するため)
→ サイドチャネル攻撃のリスクもある(タイミング攻撃)
→ クラウド環境ではセキュリティ上無効化されることも
7.3 MGLRU(Multi-generational LRU, Linux 6.1+)
MGLRU: 新しいページ回収フレームワーク
従来のActive/Inactiveリスト問題:
→ 2つのリストだけでは精度が不足
→ ページの「温度」(アクセス頻度)を2段階でしか区別できない
→ スキャンのオーバーヘッドが大きい
MGLRUの改善:
→ 4世代のリスト(gen0〜gen3)で管理
→ ページの「温度」をより細かく追跡
→ 効率的なスキャン(ページテーブルウォークによる参照確認)Gen 3 (最新): 最近アクセスされたページ Gen 2: 少し前にアクセスされたページ Gen 1: かなり前にアクセスされたページ Gen 0 (最古): 長期間アクセスされていないページ → 回収候補 アクセスごとにページが上の世代に昇格 時間経過で全ページが下の世代に降格
有効化:
echo Y > /sys/kernel/mm/lru_gen/enabled
パフォーマンス向上:
→ Chrome OS で開発・テスト
→ メモリ逼迫時のパフォーマンスが大幅改善
→ 特にメモリが少ない環境(4GB以下)で効果的
→ Android でも採用
実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
入力データの検証を行うこと
エラーハンドリングを適切に実装すること
テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1 :
"""基本的な実装パターンの演習"""
def __init__ ( self ):
self .data = []
def validate_input ( self , value ):
"""入力値の検証"""
if value is None :
raise ValueError( "入力値がNoneです" )
return True
def process ( self , value ):
"""データ処理のメインロジック"""
self . validate_input (value)
self .data. append (value)
return self .data
def get_results ( self ):
"""処理結果の取得"""
return {
'count' : len ( self .data),
'data' : self .data
}
# テスト
def test_exercise1 ():
ex = Exercise1 ()
assert ex. process ( 1 ) == [ 1 ]
assert ex. process ( 2 ) == [ 1 , 2 ]
assert ex. get_results ()[ 'count' ] == 2
try :
ex. process ( None )
assert False , "例外が発生するべき"
except ValueError:
pass
print ( "全テスト合格!" )
test_exercise1 ()
演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise :
"""応用パターンの演習"""
def __init__ ( self , max_size : int = 100 ):
self ._items: List[Dict] = []
self ._max_size = max_size
self ._created_at = datetime. now ()
def add ( self , key : str , value : any ) -> bool :
"""アイテムの追加(サイズ制限付き)"""
if len ( self ._items) >= self ._max_size:
return False
self ._items. append ({
'key' : key,
'value' : value,
'timestamp' : datetime. now (). isoformat ()
})
return True
def find ( self , key : str ) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed ( self ._items):
if item[ 'key' ] == key:
return item
return None
def remove ( self , key : str ) -> bool :
"""キーによる削除"""
for i, item in enumerate ( self ._items):
if item[ 'key' ] == key:
self ._items. pop (i)
return True
return False
def stats ( self ) -> Dict:
"""統計情報"""
return {
'total_items' : len ( self ._items),
'max_size' : self ._max_size,
'usage_percent' : len ( self ._items) / self ._max_size * 100 ,
'uptime' : str (datetime. now () - self ._created_at)
}
# テスト
def test_advanced ():
ex = AdvancedExercise ( max_size = 3 )
assert ex. add ( "a" , 1 ) == True
assert ex. add ( "b" , 2 ) == True
assert ex. add ( "c" , 3 ) == True
assert ex. add ( "d" , 4 ) == False # サイズ制限
assert ex. find ( "b" )[ 'value' ] == 2
assert ex. remove ( "b" ) == True
assert ex. find ( "b" ) is None
stats = ex. stats ()
assert stats[ 'total_items' ] == 2
print ( "応用テスト全合格!" )
test_advanced ()
演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search ( data : list , target : int ) -> int :
"""非効率な検索"""
for i in range ( len (data)):
for j in range (i + 1 , len (data)):
if data[i] + data[j] == target:
return (i, j)
return ( - 1 , - 1 )
# 最適化後(O(n))
def fast_search ( data : list , target : int ) -> tuple :
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate (data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return ( - 1 , - 1 )
# ベンチマーク
def benchmark ():
import random
data = list ( range ( 5000 ))
random. shuffle (data)
target = data[ 100 ] + data[ 4000 ]
start = time. time ()
result1 = slow_search (data, target)
slow_time = time. time () - start
start = time. time ()
result2 = fast_search (data, target)
fast_time = time. time () - start
print ( f "非効率版: { slow_time :.4f } 秒" )
print ( f "効率版: { fast_time :.6f } 秒" )
print ( f "高速化率: { slow_time / fast_time :.0f } 倍" )
benchmark ()
ポイント:
アルゴリズムの計算量を意識する
適切なデータ構造を選択する
ベンチマークで効果を測定する
トラブルシューティング
よくあるエラーと解決策
エラー
原因
解決策
初期化エラー
設定ファイルの不備
設定ファイルのパスと形式を確認
タイムアウト
ネットワーク遅延/リソース不足
タイムアウト値の調整、リトライ処理の追加
メモリ不足
データ量の増大
バッチ処理の導入、ページネーションの実装
権限エラー
アクセス権限の不足
実行ユーザーの権限確認、設定の見直し
データ不整合
並行処理の競合
ロック機構の導入、トランザクション管理
デバッグの手順
エラーメッセージの確認 : スタックトレースを読み、発生箇所を特定する
再現手順の確立 : 最小限のコードでエラーを再現する
仮説の立案 : 考えられる原因をリストアップする
段階的な検証 : ログ出力やデバッガを使って仮説を検証する
修正と回帰テスト : 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
import logging
import traceback
from functools import wraps
# ロガーの設定
logging. basicConfig (
level = logging. DEBUG ,
format = ' %(asctime)s [ %(levelname)s ] %(name)s : %(message)s '
)
logger = logging. getLogger ( __name__ )
def debug_decorator ( func ):
"""関数の入出力をログ出力するデコレータ"""
@wraps ( func )
def wrapper (* args , ** kwargs ):
logger. debug ( f "呼び出し: { func. __name__ } (args= { args } , kwargs= { kwargs } )" )
try :
result = func (*args, **kwargs)
logger. debug ( f "戻り値: { func. __name__ } -> { result } " )
return result
except Exception as e:
logger. error ( f "例外発生: { func. __name__ } : { e } " )
logger. error (traceback. format_exc ())
raise
return wrapper
@debug_decorator
def process_data ( items ):
"""データ処理(デバッグ対象)"""
if not items:
raise ValueError( "空のデータ" )
return [item * 2 for item in items]
パフォーマンス問題の診断
パフォーマンス問題が発生した場合の診断手順:
ボトルネックの特定 : プロファイリングツールで計測
メモリ使用量の確認 : メモリリークの有無をチェック
I/O待ちの確認 : ディスクやネットワークI/Oの状況を確認
同時接続数の確認 : コネクションプールの状態を確認
問題の種類
診断ツール
対策
CPU負荷
cProfile, py-spy
アルゴリズム改善、並列化
メモリリーク
tracemalloc, objgraph
参照の適切な解放
I/Oボトルネック
strace, iostat
非同期I/O、キャッシュ
DB遅延
EXPLAIN, slow query log
インデックス、クエリ最適化
8. FAQ
Q1: スワップとは何か?
物理メモリが不足した時に、使用頻度の低いページをディスク(スワップ領域)に退避すること。これによりプロセスは物理メモリ以上のメモリを使用できるが、ディスクI/Oは非常に遅い(RAMの10万倍遅い:RAM 100ns vs HDD 10ms)。SSDで10〜100倍改善されるが、根本的にはRAMを増やすべき。最近はzswap(圧縮キャッシュ)やzram(圧縮RAMディスク)でスワップの影響を軽減する手法が普及している。
Q2: OOM Killerとは?
Linux Out-Of-Memory Killer。メモリが完全に枯渇した時に、OSがプロセスを強制終了して空きメモリを確保する仕組み。各プロセスにoom_score(0〜1000)が付与され、スコアが高いプロセスが優先的にkillされる。/proc/<pid>/oom_score_adjで-1000〜+1000の範囲で調整可能。-1000に設定するとOOM Killer対象外になる(データベースサーバー等に推奨)。systemdではOOMScoreAdjustディレクティブで設定。
Q3: なぜ64bitでも48bitしかアドレスを使わないのか?
現在のx86-64は48bit仮想アドレス空間(256TB)を使用。64bit全て(16EB = 16エクサバイト)は現時点で必要なく、ページテーブルの階層が深くなりすぎる(6〜7階層)。Intel 5-Level Paging(LA57)で57bit(128PB)に拡張可能で、Linux 4.14+でサポートされている。需要に応じて段階的に拡張される設計。AMD もSVME(Secure Virtual Machine Extensions)で5レベルページングをサポート。
VSS(Virtual Set Size): プロセスの仮想メモリ全体。mallocしたが使っていない領域も含む。
RSS(Resident Set Size): 物理メモリに存在するページの合計。共有ライブラリを二重計上。
PSS(Proportional Set Size): 共有ページをプロセス数で按分。最も正確な「実使用量」。
USS(Unique Set Size): そのプロセス固有のページのみ。プロセス終了で解放される量。
例: libc.soを10プロセスが共有、libc = 2MB
RSS: 各プロセスに2MBを計上(合計20MB)
PSS: 各プロセスに200KB(2MB/10)を計上(合計2MB)
Q5: なぜDockerコンテナにもメモリ制限を設定すべきなのか?
コンテナはデフォルトでホストの全メモリを使用可能。メモリ制限なしだと、1つのコンテナがメモリを使い切ってOOM Killerが他のコンテナを強制終了する可能性がある。cgroupsのmemory.maxで制限を設定し、memory.highで早期の警告と回収を促すのが推奨。Kubernetesではresources.limitsで設定する。制限を超えたコンテナは即座にOOM killされるため、適切な値の設定が重要。
Q6: メモリリークと仮想メモリの関係は?
メモリリークが発生すると、使われないメモリが解放されずに蓄積する。仮想メモリの観点では:
VSSが増加し続ける(mallocが成功し続ける)
実際にアクセスされるとRSSも増加
物理メモリを圧迫→スワップ増加→性能低下
最終的にOOM Killerが発動
仮想メモリのオーバーコミットにより、リークの初期段階ではmallocが成功し続けるため問題の発見が遅れることがある。
FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
9. まとめ
概念
ポイント
仮想メモリ
プロセスごとに独立したアドレス空間を提供
ページング
4KB単位で仮想→物理をマッピング
多階層ページテーブル
x86-64は4階層(48bit)、5階層(57bit)
TLB
ページテーブルのキャッシュ。ヒット率99%+
ラージページ
2MB/1GBでTLBカバレッジ拡大
デマンドページング
必要時にのみ物理メモリに配置
Copy-on-Write
fork時の最適化。書き込み時にコピー
ページ置換
Clock(近似LRU)。Linux はActive/Inactive リスト
スラッシング
メモリ不足でページフォルト頻発
OOM Killer
メモリ枯渇時にプロセスを強制終了
オーバーコミット
malloc成功≠物理メモリ確保
次に読むべきガイド
参考文献
Silberschatz, A. et al. "Operating System Concepts." 10th Ed, Ch.9-10, 2018.
Gorman, M. "Understanding the Linux Virtual Memory Manager." Prentice Hall, 2004.
Arpaci-Dusseau, R. H. & Arpaci-Dusseau, A. C. "Operating Systems: Three Easy Pieces." Ch.13-24, 2018.
Bovet, D. & Cesati, M. "Understanding the Linux Kernel." 3rd Ed, O'Reilly, 2005.
Love, R. "Linux Kernel Development." 3rd Ed, Ch.15-16, 2010.
Corbet, J. "Multi-generational LRU: the next generation." LWN.net, 2022.
Intel. "Intel 64 and IA-32 Architectures Software Developer's Manual." Volume 3A, Ch.4, 2024.
Tanenbaum, A. S. & Bos, H. "Modern Operating Systems." 4th Ed, Ch.3, 2014.