このページの目次

ページ回収とスワップ

カバー範囲: 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)

// 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 内のページ → 回収候補

回収の優先度

// 回収順序 (ファイルページが匿名ページより優先される):
//   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: バックグラウンド回収

// 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: 同期回収

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


スワップ

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

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

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

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

// 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() へ
}

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

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

// 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)

// 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 (断片化解消)

// 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

デバッグと観測

# メモリプレッシャーの概要
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