このページの目次

Page Cache と I/O

カバー範囲: address_space → radix tree/xarray → readahead → writeback → direct I/O → folio (5.16+) カーネルバージョン: 2.6 ~ 6.x、folio 変換を重点的に注釈

概要

Page cache はカーネルにおいて最も中核的なキャッシュ層です。VFS とファイルシステムの間に位置し、ディスクから読み出されたページをキャッシュします。多くの read() システムコールは、page cache にヒットするため、ディスクを一切参照しません。同様に、多くの write() は page cache 内のページのみを変更し、バックグラウンドの flusher スレッドが非同期で書き戻しを行います。

Page cache の基盤となるデータ構造は 5.x シリーズで大幅なリファクタリングを経験しました。radix tree から xarray (5.0) へ、さらに folio (5.16+) へと進化し、メモリ管理の基本単位が「単一の 4KB ページ」から「複数のページを含む可能性のある複合ページ」へと引き上げられました。


address_space

// include/linux/fs.h
struct address_space {
    struct inode        *host;              // 所属する inode
    struct xarray       i_pages;            // page cache (page->index → page)
    
    unsigned long       nrpages;            // キャッシュされた総ページ数
    unsigned long       nrexceptional;      // shadow/swap エントリ

    const struct address_space_operations *a_ops;
    // → read_folio, writepages, readahead, write_begin, write_end, ...

    struct list_head    i_private_list;
    struct address_space *private_data;
};

各ファイル (inode->i_mapping) は独自の address_space を持ちます。Page cache はファイル単位 (per-file) です。address_space->i_pages は、そのファイルのすべてのキャッシュされたページの xarray です。


Xarray: Radix Tree の置換

// include/linux/xarray.h
// 5.0 で導入: 古い radix tree を置換
// よりシンプルな API、組み込み RCU 保護、範囲ロックのサポート

// 主要な操作:
void *xa_store(struct xarray *xa, unsigned long index, void *entry, gfp_t gfp);
void *xa_load(struct xarray *xa, unsigned long index);
void *xa_erase(struct xarray *xa, unsigned long index);

// page cache の場合:
//   index = page->index (ファイル内でのオフセット、PAGE_SIZE 単位)
//   entry = folio (5.16+) または page

Folio: 複合ページの管理

// include/linux/page-flags.h
// 5.16+: page cache の基本単位を page から folio に引き上げ

struct folio {
    struct page page;           // 「先頭ページ」
    // folio は複数の連続した page を含むことができる
    //   folio_nr_pages(folio)  総ページ数を返す
    //   folio_page(folio, n)   n 番目のサブページを返す
};

// folio が必要な理由:
//   旧: page cache は PAGE_SIZE (4KB) を単位としていた
//       THP (2MB) には特別な compound page の処理が必要
//       至る所に if (PageTransHuge(page)) の分岐が存在
//
//   新: folio は 4KB ページ、2MB THP、将来の mTHP を統一して処理
//       VFS とファイルシステムは folio のみを操作し、内部に何ページあるかを知らなくてよい
//       コードが簡素化され、バグが減少

読取パス: Page Cache から bio へ

flowchart TD
    START["read() → filemap_read()<br/>各 folio を順に処理"]

    START --> LOOKUP["filemap_get_entry()<br/>xa_load(&mapping->i_pages, index)"]

    LOOKUP --> HIT{"page cache<br/>ヒット?"}

    HIT -->|"✅ ヒット (folio uptodate)"| COPY["copy_folio_to_iter()<br/>→ copy_to_user()<br/>→ ユーザー空間へ返す"]

    HIT -->|"❌ ミス"| MISS["filemap_read_folio()<br/>→ readahead / read_folio"]

    MISS --> ALLOC["新しい folio を割り当て<br/>(order 0 または THP order)"]
    ALLOC --> BIO["ブロック層に bio を送信<br/>(submit_bio)"]
    BIO --> WAIT["bio の完了を待機<br/>(lock_folio)"]
    WAIT --> UPTODATE["folio を uptodate にマーク<br/>→ xarray に挿入"]
    UPTODATE --> COPY

    COPY --> ACCESS["file_accessed(folio)<br/>LRU を更新 (accessed マーク)"]
    ACCESS --> DONE["ユーザー空間へ返す ✅"]

    classDef start fill:#e3f2fd,stroke:#1565c0
    classDef decision fill:#fff3e0,stroke:#ef6c00
    classDef io fill:#ffebee,stroke:#c62828
    classDef done fill:#e8f5e9,stroke:#2e7d32
    class START start
    class HIT decision
    class MISS,ALLOC,BIO,WAIT,UPTODATE io
    class COPY,ACCESS,DONE done

Readahead

// mm/readahead.c
// カーネルは要求された 4KB だけを読むのではなく、より多くのデータを「先読み」する

// 2 種類の先読み:
//   1. 同期先読み (synchronous):
//      現在の読み取りで page cache ミスが発生
//      → 現在のページに加え、その後の連続するページも読み込む (例: 128KB)
//      → 後続の page fault を削減
//
//   2. 非同期先読み (asynchronous):
//      シーケンシャルな読み取りパターンを検出 → バックグラウンド IO を事前にトリガー
//      → ユーザー空間がまだ要求していないが、カーネルは既に読み込んでいる
//      ondemand_readahead() によって実装され、読み取りパターンを追跡

// page_cache_sync_readahead() → 同期
// page_cache_async_readahead()  → 非同期

書込パス: Page Cache + Writeback

flowchart TD
    START["write() → generic_perform_write()<br/>各 folio を順に処理"]

    START --> WB["a_ops->write_begin()<br/>folio の取得または割り当て<br/>(grab_cache_folio_write_begin)"]

    WB --> RMW{"書き込みが<br/>ページ全体を覆うか?"}
    RMW -->|"否 → 部分的な書き込み"| READ_IN["カバーされていない部分をディスクから先に読み込む<br/>(read-modify-write)"]
    RMW -->|"是"| LOCK["lock_folio"]
    READ_IN --> LOCK

    LOCK --> COPY["copy_page_from_iter()<br/>→ copy_from_user()<br/>ユーザーデータ → folio"]

    COPY --> WE["a_ops->write_end()"]
    WE --> DIRTY["folio を dirty にマーク<br/>(folio_mark_dirty)"]
    WE --> ISIZE["i_size を更新<br/>(ファイルが拡張された場合)"]
    WE --> UNLOCK["unlock_folio"]
    DIRTY --> BALANCE
    ISIZE --> BALANCE
    UNLOCK --> BALANCE

    BALANCE["balance_dirty_pages()<br/>流量制御: dirty ページが多すぎるか?"]
    BALANCE -->|"閾値超過"| WAIT["バックグラウンドの書き戻しを待機"]
    WAIT --> NEXT["次の folio"]
    BALANCE -->|"正常"| NEXT

    classDef start fill:#e3f2fd,stroke:#1565c0
    classDef decision fill:#fff3e0,stroke:#ef6c00
    classDef step fill:#f3e5f5,stroke:#7b1fa2
    classDef risky fill:#ffebee,stroke:#c62828
    class START start
    class RMW decision
    class WB,READ_IN,LOCK,COPY,WE,DIRTY,ISIZE,UNLOCK,NEXT step
    class BALANCE,WAIT risky

### Writeback メカニズム

```c
// fs/fs-writeback.c + mm/page-writeback.c
// 書き戻しはバックグラウンドの flusher スレッドによって完了する (bdi ごとに 1 つ)
// bdi = Block Device I/O context

// トリガー条件:
//   1. 定期的: flusher スレッドは 5 秒ごとに起床 (dirty_writeback_centisecs)
//   2. dirty ページのタイムアウト: dirty_expire_centisecs (30 秒)
//   3. dirty ページの過多: dirty_ratio (総メモリの 20%)
//   4. fsync / sync システムコール

// 閾値による制御 (/proc/sys/vm/ を介して):
//   dirty_background_ratio: これに達するとバックグラウンド書き戻しが開始 (デフォルト 10%)
//   dirty_ratio:            これに達すると書き込み側がブロック (デフォルト 20%)

Direct I/O

// fs/direct-io.c (旧) / fs/iomap/direct-io.c (新、iomap ベース)
// O_DIRECT: page cache をバイパスし、ディスクに直接読み書き

// 主要な制約:
//   1. セクタアライメントが必要 (512B / 4KB)
//   2. 書き込み操作は同期 (dirty page cache による「偽の完了」は発生しない)
//   3. ユーザーバッファとディスクの間にカーネルキャッシュがない (page cache は関与しない)

// 実装 (iomap バージョン):
__iomap_dio_rw()
  ├─ データは bio を介して直接ブロックデバイスに送信
  ├─ ユーザーバッファのページはピン留めされる (get_user_pages_fast)
  ├─ bio 完了後に直接アンピン
  └─ page cache の関与は不要

デバッグ

# Page cache の使用状況
cat /proc/meminfo | grep -E 'Cached|Dirty|Writeback|WritebackTmp'
# Cached:     page cache (ファイルページ)
# Dirty:      dirty ページ (書き戻し待ち)
# Writeback:  書き戻し中のページ

# 手動での書き戻しトリガー
sync
echo 3 > /proc/sys/vm/drop_caches  # page cache をクリア (テスト用)

# ファイルごとの page cache 表示
vmtouch /path/to/file  # vmtouch のインストールが必要

# 書き戻し統計
cat /proc/vmstat | grep -E 'nr_dirty|nr_writeback|pgwrite'

参考と拡張

  • カーネルドキュメント: Documentation/filesystems/vfs.rst, Documentation/core-api/xarray.rst
  • LWN:
    • "The folio conversion" シリーズ (lwn.net/Articles/849538/)
    • "Better writeback with iomap" (lwn.net/Articles/869187/)
  • ソースファイル:
    • mm/filemap.c — page cache の中核 (読取パス)
    • mm/page-writeback.c — writeback の閾値と流量制御
    • fs/fs-writeback.c — flusher スレッド
    • mm/readahead.c — 先読み
    • lib/xarray.c — Xarray 実装
    • include/linux/page-flags.h — folio の定義

キーワード: page cache, address_space, xarray, folio, readahead, writeback, flusher, O_DIRECT, iomap, dirty_ratio