このページの目次
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
;
各ファイル (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 *;
void *;
void *;
// page cache の場合:
// index = page->index (ファイル内でのオフセット、PAGE_SIZE 単位)
// entry = folio (5.16+) または page
Folio: 複合ページの管理
// include/linux/page-flags.h
// 5.16+: page cache の基本単位を page から folio に引き上げ
;
// 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 バージョン):
├─ データは bio を介して直接ブロックデバイスに送信
├─
├─ bio 完了後に直接アンピン
└─ page cache の関与は不要
デバッグ
# Page cache の使用状況
|
# Cached: page cache (ファイルページ)
# Dirty: dirty ページ (書き戻し待ち)
# Writeback: 書き戻し中のページ
# 手動での書き戻しトリガー
# ファイルごとの page cache 表示
# 書き戻し統計
|
参考と拡張
- カーネルドキュメント:
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