本页目录

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 entries

    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 保护, 支持 range locking

// 关键操作:
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 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 版本):
__iomap_dio_rw()
  ├─ 数据直接通过 bio 提交 → 块设备
  ├─ 用户缓冲区的页被 pin (get_user_pages_fast)
  ├─ bio 完成后直接 unpin
  └─ 不需要 page cache 参与

调试

# Page cache 使用
cat /proc/meminfo | grep -E 'Cached|Dirty|Writeback|WritebackTmp'
# Cached:     page cache (文件页)
# 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