本页目录
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 保护, 支持 range locking
// 关键操作:
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 page, 2MB THP, 未来的 mTHP
// VFS 和文件系统只操作 folio, 不需要知道内部有多少页
// 代码简化, bug 减少
读路径: 从 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, 而是"预读"更多
// 两种预读:
// 1. 同步预读 (synchronous):
// 当前读取触发 page cache miss
// → 除当前页外, 还读后面的连续页 (如 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 一个)
// bdi = Block Device I/O context
// 触发条件:
// 1. 周期性: flusher 线程每 5 秒醒来 (dirty_writeback_centisecs)
// 2. 脏页超时: dirty_expire_centisecs (30 秒)
// 3. 脏页太多: dirty_ratio (20% of total memory)
// 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 based)
// O_DIRECT: 绕过 page cache, 直接读/写磁盘
// 关键约束:
// 1. 必须 sector-aligned (512B / 4KB)
// 2. 写操作是同步的 (不会因为 dirty page cache 而"假完成")
// 3. 用户缓冲区和磁盘之间无内核缓存 (page cache 不参与)
// 实现 (iomap 版本):
├─ 数据直接通过 bio 提交 → 块设备
├─ 用户缓冲区的页被
├─ bio 完成后直接 unpin
└─ 不需要 page cache 参与
调试
# Page cache 使用
|
# Cached: page cache (文件页)
# 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