Skilore

仮想メモリ

仮想メモリは「物理メモリの限界を超え、各プロセスに独立した広大なアドレス空間を提供する」OSの最も重要な抽象化の一つ。

84 分で読めます41,594 文字

仮想メモリ

仮想メモリは「物理メモリの限界を超え、各プロセスに独立した広大なアドレス空間を提供する」OSの最も重要な抽象化の一つ。

この章で学ぶこと

  • 仮想メモリの必要性と仕組みを理解する
  • アドレス変換(MMU、ページテーブル)を説明できる
  • TLBの役割と最適化手法を知る
  • ページフォルトとデマンドページングの動作を追える
  • ページ置換アルゴリズムを比較・評価できる
  • Linuxの仮想メモリ管理の全体像を把握する
  • 仮想メモリのパフォーマンスチューニングを実施できる

前提知識

このガイドを読む前に、以下の知識があると理解が深まります:

  • 基本的なプログラミングの知識
  • 関連する基礎概念の理解

1. なぜ仮想メモリが必要か

1.1 仮想メモリがない世界の問題

仮想メモリ以前のメモリ管理:

  1950〜1960年代:
  → プログラムは物理アドレスを直接使用
  → 1つのプログラムだけがメモリ全体を使用
  → マルチプログラミング不可

  1960年代 ベースレジスタ方式:
  → プロセスごとにベースアドレスを設定
  → アドレス = ベース + オフセット
  → 問題: メモリの断片化、保護が不十分

  仮想メモリがない場合の5つの問題:

  問題1: メモリ不足
物理メモリ: 4GB
プロセスA: 3GB必要
プロセスB: 2GB必要
→ 同時に実行できない!
仮想メモリなら:
各プロセスに仮想的に8GBの空間を提供
実際に使う部分だけ物理メモリに配置
残りはディスク(スワップ)に退避
問題2: メモリの断片化
ABC
↑ この隙間                ↑ ここも
  → 合計の空きは十分でも連続領域が不足

  問題3: メモリ保護の欠如
  → プロセスAが誤ったアドレスに書き込み
  → プロセスBのメモリを破壊
  → システム全体がクラッシュ

  問題4: プログラムの再配置
  → プログラムは特定のアドレスにロードされることを想定
  → 複数プログラムのアドレスが衝突
  → プログラマがアドレスを管理する必要

  問題5: メモリ効率
  → 同じライブラリを使う100個のプロセス
  → 100コピーが物理メモリに存在(無駄)
  → 仮想メモリなら1コピーを共有可能

1.2 仮想メモリの解決策

仮想メモリの基本概念:
  各プロセスに独立した仮想アドレス空間を提供
  → 物理メモリの配置を意識する必要なし
  → MMU(Memory Management Unit)がアドレス変換を担当

  Process A           Process B
仮想アドレス仮想アドレス
0xFFFF0xFFFF
│ 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 ページとフレーム

ページング:
  仮想メモリを固定サイズの「ページ」に分割
  物理メモリを同じサイズの「フレーム」に分割

  ページ(仮想メモリ側) → フレーム(物理メモリ側)

  一般的なページサイズ:
ページサイズアーキテクチャ用途
4KBx86, ARM標準(デフォルト)
16KBARM64(選択可)Apple Silicon
64KBARM64(選択可)一部のARM Linux
2MBx86(hugepage)ラージページ(DB、JVM)
1GBx86(hugepage)超大規模メモリ
なぜ4KBか?
  - 小さすぎると: ページテーブルが巨大になる
  - 大きすぎると: 内部断片化が増大
  - 4KBは歴史的にバランスが良い(1980年代のVAXから)
  - ディスクのセクタサイズ(512B→4KB)とも合致

  Apple Silicon (M1以降) のページサイズ:
  → macOSは16KBページを採用
  → iOSも16KBページ
  → 4KBの4倍のTLBカバレッジ
  → x86からの移植時にアライメント注意

2.2 アドレス変換

仮想アドレス → 物理アドレスの変換:

  仮想アドレス(例: 32bit):
ページ番号オフセット
(20bit)(12bit)
│              │
         ↓              │
ページテーブル
[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):
PD(10)PT(10)Offset(12)
│        │
      ↓        ↓
  [ページディレクトリ] → [ページテーブル] → 物理フレーム

  → 使用していない領域のページテーブルは作らない
  → 1MBしか使わないプロセスは少数のページテーブルで済む

x86-64の4階層ページテーブル:

  仮想アドレス(48bit):
PML4PDPTPDPTOffset
(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の範囲をカバー
PDPT512エントリ → 最大512個のPD
各エントリが1GBの範囲をカバー
1GBラージページも可能
PD512エントリ → 最大512個のPT
各エントリが2MBの範囲をカバー
2MBラージページも可能
PT512エントリ → 最大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):
PML5PML4PDPTPDPTOffset
(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物理FNASIDフラグ
(タグ)(データ)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ミスがほぼゼロ

  パフォーマンス向上:
ワークロード4KB2MB(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                 Child
VPN 0 ─────┐ ┌───── VPN 0
VPN 1 ─────┼──┼───── VPN 1
VPN 2 ─────┼──┼───── VPN 2
↓  ↓
FN 5
FN 8
FN 12
子プロセスが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ビットを使用。
A=1A=0A=1A=0
↑ 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()

ポイント:

  • アルゴリズムの計算量を意識する
  • 適切なデータ構造を選択する
  • ベンチマークで効果を測定する

トラブルシューティング

よくあるエラーと解決策

エラー 原因 解決策
初期化エラー 設定ファイルの不備 設定ファイルのパスと形式を確認
タイムアウト ネットワーク遅延/リソース不足 タイムアウト値の調整、リトライ処理の追加
メモリ不足 データ量の増大 バッチ処理の導入、ページネーションの実装
権限エラー アクセス権限の不足 実行ユーザーの権限確認、設定の見直し
データ不整合 並行処理の競合 ロック機構の導入、トランザクション管理

デバッグの手順

  1. エラーメッセージの確認: スタックトレースを読み、発生箇所を特定する
  2. 再現手順の確立: 最小限のコードでエラーを再現する
  3. 仮説の立案: 考えられる原因をリストアップする
  4. 段階的な検証: ログ出力やデバッガを使って仮説を検証する
  5. 修正と回帰テスト: 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
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]

パフォーマンス問題の診断

パフォーマンス問題が発生した場合の診断手順:

  1. ボトルネックの特定: プロファイリングツールで計測
  2. メモリ使用量の確認: メモリリークの有無をチェック
  3. I/O待ちの確認: ディスクやネットワークI/Oの状況を確認
  4. 同時接続数の確認: コネクションプールの状態を確認
問題の種類 診断ツール 対策
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レベルページングをサポート。

Q4: RSS、VSS、PSS の違いは?

  • 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: メモリリークと仮想メモリの関係は?

メモリリークが発生すると、使われないメモリが解放されずに蓄積する。仮想メモリの観点では:

  1. VSSが増加し続ける(mallocが成功し続ける)
  2. 実際にアクセスされるとRSSも増加
  3. 物理メモリを圧迫→スワップ増加→性能低下
  4. 最終的に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成功≠物理メモリ確保

次に読むべきガイド


参考文献

  1. Silberschatz, A. et al. "Operating System Concepts." 10th Ed, Ch.9-10, 2018.
  2. Gorman, M. "Understanding the Linux Virtual Memory Manager." Prentice Hall, 2004.
  3. Arpaci-Dusseau, R. H. & Arpaci-Dusseau, A. C. "Operating Systems: Three Easy Pieces." Ch.13-24, 2018.
  4. Bovet, D. & Cesati, M. "Understanding the Linux Kernel." 3rd Ed, O'Reilly, 2005.
  5. Love, R. "Linux Kernel Development." 3rd Ed, Ch.15-16, 2010.
  6. Corbet, J. "Multi-generational LRU: the next generation." LWN.net, 2022.
  7. Intel. "Intel 64 and IA-32 Architectures Software Developer's Manual." Volume 3A, Ch.4, 2024.
  8. Tanenbaum, A. S. & Bos, H. "Modern Operating Systems." 4th Ed, Ch.3, 2014.