---
title: ページ回収とスワップ
url: https://doc.liz6.com/ja/linux-kernel/02-memory-management/05-page-reclaim-and-swap
locale: ja
area: linux-kernel
tags:
- linux-kernel
- memory-management
date: 2026-06-30
modified: 2026-07-16
description: 'カバー範囲: LRU (匿名/ファイル) → kswapd + direct reclaim → スワップ機構 → 逆マップ (rmap) → ページキャッシュ回収 → 断片化解消。カーネルバージョン: 2.6 ~ 6.x。multi-gen LRU (6.1+) を重点的に注釈付きで解説'
---

# ページ回収とスワップ

> カバー範囲: LRU (匿名/ファイル) → kswapd + direct reclaim → スワップ機構 → 逆マップ (rmap) → ページキャッシュ回収 → 断片化解消
> カーネルバージョン: 2.6 ~ 6.x。multi-gen LRU (6.1+) を重点的に注釈付きで解説

## 概要

物理メモリが不足すると、カーネルは一部のページを回収（解放）しなければなりません。回収対象は以下の2種類に分類されます。
- **ファイルページ (file-backed)**: クリーンな場合はそのまま破棄（ディスクにコピーがあるため）、ダーティな場合は書き戻し
- **匿名ページ (anonymous)**: 回収するにはスワップ領域へ書き込む必要がある

回収戦略の中核は **LRU** (Least Recently Used: 最古使用優先) です。これは「冷たい」ページを外部へ追い出し、「熱い」ページを保持するという考え方です。しかし、数百GB規模のワークセットにおいて「熱さ」と「冷たさ」を判断することは極めて困難です。Linux は従来の双方向リストによる LRU から進化し、6.1 で導入された **Multi-Gen LRU (MGLRU)** により、大規模なワークセットやスキャン嵐（scanning storm）への対応が大幅に改善されました。

---

## LRU リスト

### 従来の2つの LRU (2.6 ~ 5.x)

```c
// mm/vmscan.c
// 各 memcg および各ノードに、一組の LRU リストが存在する:

enum lru_list {
    LRU_INACTIVE_ANON,  // 冷たい匿名ページ（長時間未アクセス）
    LRU_ACTIVE_ANON,    // 熱い匿名ページ
    LRU_INACTIVE_FILE,  // 冷たいファイルページ
    LRU_ACTIVE_FILE,    // 熱いファイルページ
    LRU_UNEVICTABLE,    // 回収不可 (mlock, ramfs など)
    NR_LRU_LISTS
};

// ページは active と inactive の間で移動する:
//   初回マップ → inactive
//   再アクセス → active へ昇格
//   長時間未アクセス → inactive へ降格
//   inactive 内のページ → 回収候補
```

### 回収の優先度

```c
// 回収順序 (ファイルページが匿名ページより優先される):
//   1. クリーンなファイルページ (drop, 最速)
//   2. ダーティなファイルページ (書き戻し)
//   3. 匿名ページ (スワップアウト)
//
// なぜファイルページが優先されるのか?
//   クリーンなファイルページ: 直接破棄可能、I/O 0
//   匿名ページ:             スワップへ書き込む必要あり → I/O オーバーヘッド
//   ダーティなファイルページ: 書き戻す必要あり → I/O オーバーヘッド
//
// /proc/sys/vm/swappiness はこの優先度を制御する:
//   0:   可能な限りファイルページを回収し、やむを得ない場合のみスワップ
//   100: ファイルと匿名を同等に扱う
//   デフォルト: 60
```

### Multi-Gen LRU (6.1+)

```
従来の LRU の問題点:
  - active/inactive の2世代のみ → グランularity（粒度）が粗い
  - 熱さの判断は referenced bit と定期的なスキャンに依存
  - 大規模ワークセットではスキャンコストが極めて高い（「スキャン嵐」）

MGLRU のアプローチ:
  ページは世代（generation）ごとにグループ化され、各世代に独立した LRU が存在する
    新世代 = アクティブ（直近でアクセスあり）
    旧世代 = 冷たい（回収候補）
    
  回収時は最も古い世代から開始 → 自然に熱いページが除外される
  全ページをスキャンして熱さを判断する必要がない → スキャン量が大幅に削減
  
  実装: mm/vmscan.c (MGLRUビルド時) + /sys/kernel/mm/lru_gen
  
  Google および Meta ともに、CPU使用量の大幅な削減を報告
  （スキャンオーバーヘッドの 50%〜90% 削減）
```

---

## kswapd と Direct Reclaim

### kswapd: バックグラウンド回収

```c
// mm/vmscan.c
// 各 NUMA ノードに独自の kswapd カーネルスレッドが存在する

// 起動条件:
//   空きページ数が high watermark を下回ると kswapd が起動
//   kswapd は空きページ数が high watermark を超えるまで継続的に回収を行う

static int kswapd(void *p) {
    while (!kthread_should_stop()) {
        // 1. watermark を確認: high を超えていればスリープ
        // 2. balance_pgdat() を呼び出す
        //    ├─ LRU リストをスキャン
        //    ├─ shrink_node() → shrink_lruvec()
        //    │   ├─ shrink_inactive_list()  → 冷たいページを回収
        //    │   └─ shrink_active_list()    → 熱いページを降格
        //    └─ 断片化が深刻な場合は compaction をトリガーする可能性あり

        // 3. 十分なページを回収できればスリープ
        //    回収が不十分で watermark が低いままなら → スキャン強度を上げる
    }
}
```

### Direct Reclaim: 同期回収

```c
// kswapd が追いつかない場合（メモリ割り当てが速すぎる場合）、
// 割り当て側が自らページを回収する:
__alloc_pages_slowpath()
  → __alloc_pages_direct_reclaim()
    → __perform_reclaim() → try_to_free_pages()
      → do_try_to_free_pages() → shrink_zones()
        → shrink_node() → shrink_lruvec()
          // kswapd と同じ回収ロジックだが、同期的に実行
          // 割り当て側は利用可能なページが得られるまでここでブロックされる
```

direct reclaim は割り当てレイテンシの急増を引き起こすため、「メモリプレッシャー」指標が注目される理由となっています。

---

## スワップ

### スワップパーティション/ファイル

```bash
# 設定
swapon /dev/sda2           # スワップパーティション
swapon /swapfile            # スワップファイル (5.5+ で Btrfs スワップ対応)

# プライオリティ (複数のスワップデバイスがある場合)
swapon -p 10 /dev/nvme0n1p2  # NVMe スワップ (高速)
swapon -p 1  /dev/sda2        # SATA スワップ (低速)
```

### スワップアウト (匿名ページ → スワップ)

```c
// mm/vmscan.c
// shrink_page_list() で匿名ページを回収する際:

if (PageAnon(page) && !PageSwapCache(page)) {
    // 1. スワップスロットの割り当て
    add_to_swap(page)
      → get_swap_page()       // スワップビットマップから空きスロットを検索
      → add_to_swap_cache()   // ページをスワップキャッシュ (address_space) に追加
    
    // 2. スワップへの書き込み
    //   CONFIG_THP_SWAP=y の場合:
    //     THP は 4KB ページに分割され、それぞれ個別にスワップされる
    //     (2MB の連続 I/O を回避するため)
    //
    // 3. PTE をスワップエントリとしてマーク
    //    PTE = swp_entry_t (type + offset)
    //   ハードウェアは Present=0 を検知 → ページフォルト発生 → do_swap_page() へ
}

// スワップキャッシュ:
//   ページがスワップとメモリ間の遷移状態にある際のキャッシュ
//   スワップアウトとスワップインの間に競合が発生した場合、
//   スワップキャッシュが一貫性を保証する
```

### スワップイン (ページフォルト → スワップ)

```c
// mm/memory.c
// do_swap_page(): プロセスがスワップアウトされたアドレスにアクセスした

do_swap_page()
  ├─ lookup_swap_cache()     // スワップキャッシュをチェック（メモリ内にある場合あり）
  │   └─ 存在する場合: 直接マッピング（I/O 不要!）
  │
  └─ swapin_readahead()      // スワップデバイスから先読み
      └─ read_swap_cache_async()
          → swap_readpage()  // I/O をサブミット
          → ページをスワップキャッシュに追加
          → PTE をマッピング
          → ユーザープロセスの再開
```

---

## 逆マップ (rmap)

```c
// mm/rmap.c
// 逆マップが答える質問: 「特定の物理ページを、どのプロセスのどの仮想アドレスがマップしているか?」

// 匿名ページの場合:
//   anon_vma + VMA のインターバル木を用いて、すべてのマッピングを走査
//   page->mapping → anon_vma → anon_vma_chain → VMA

// ファイルページの場合:
//   address_space + page->index を用いて検索
//   page->mapping → address_space → i_mmap (インターバル木)

// rmap_walk(page, func):
//   page のすべての PTE マッピングを走査
//   各マッピングに対して func を呼び出す → 例: try_to_unmap
//     → PTE をスワップエントリとしてマークするか、クリア
//     → TLB をフラッシュ

// try_to_unmap() はスワップの前提条件:
//   すべての PTE 参照を見つけ、解除してからでなければページを回収できない
```

---

## Compaction (断片化解消)

```c
// mm/compaction.c
// 問題: バディアロケータは連続したページを必要とする（例: order 4 = 16ページ = 64KB）
//       しかしメモリが断片化しているため、連続ブロックを割り当てられない

// compaction: 使用中のページを「移動」させ、連続した空き領域を確保する
//   1. スキャン: 空きページ（ターゲット）と使用中ページ（ソース）を検索
//   2. 移行: 使用中ページの内容を空きページへコピー
//   3. ページテーブルの更新: すべての PTE を新しい位置へ向ける
//   4. 繰り返し: 十分な連続空間が確保されるまで

// トリガー方法:
//   1. kcompactd: バックグラウンドスレッド（kswapd に類似）
//   2. direct compaction: 割り当て側が slowpath で自発的に断片化解消を実行

// compaction の状態確認:
cat /proc/vmstat | grep compact
```

---

## デバッグと観測

```bash
# メモリプレッシャーの概要
cat /proc/meminfo
# SwapTotal/SwapFree: スワップの使用状況
# Active/Inactive(anon/file): LRU 内のページ数
# Dirty/Writeback: 書き戻し待ちのページ

# 回収統計
cat /proc/vmstat | grep -E 'pgsteal|pgscan|pswpin|pswpout'
# pgsteal_*: 回収されたページ数
# pgscan_*:  スキャンされたページ数
#   scan >> steal → LRU スキャンの効率が低い（MGLRU で改善）

# MGLRU が有効かどうかの確認
ls /sys/kernel/mm/lru_gen 2>/dev/null && echo "MGLRU enabled"

# kswapd の動作確認
ps aux | grep kswapd
cat /proc/<kswapd_pid>/status

# compaction
cat /proc/vmstat | grep compact
cat /sys/kernel/debug/extfrag/extfrag_index

# 手動での回収トリガー（テスト用、本番環境では注意して使用）
echo 3 > /proc/sys/vm/drop_caches  # ページキャッシュをクリア
```

---

## 参考資料と拡張読み物

- **カーネルドキュメント**: `Documentation/admin-guide/sysctl/vm.rst`, `Documentation/mm/multigen_lru.rst`
- **LWN**:
  - "Multi-generational LRU" (lwn.net/Articles/856932/)
  - "The case for MGLRU" (lwn.net/Articles/916777/)
  - "Swap and the page cache" (lwn.net/Articles/358953/)
- **ソースファイル**:
  - `mm/vmscan.c` — kswapd + LRU スキャン + direct reclaim
  - `mm/swap_state.c` — スワップキャッシュ
  - `mm/swapfile.c` — スワップパーティション/ファイル管理
  - `mm/rmap.c` — 逆マップ
  - `mm/compaction.c` — 断片化解消
  - `mm/page_io.c` — スワップ I/O

---

*キーワード: LRU, MGLRU, kswapd, direct reclaim, swap, swappiness, rmap, try_to_unmap, compaction, page cache reclaim*
