---
title: Page Cache 与 I/O
url: https://doc.liz6.com/linux-kernel/03-file-system/02-page-cache-and-i-o
locale: zh
area: linux-kernel
tags:
- linux-kernel
- 文件系统
date: 2026-06-30
modified: 2026-06-30
description: '覆盖: address_space → radix tree/xarray → readahead → writeback → direct I/O → folio (5.16+) 内核版本: 2.6 ~ 6.x，重点标注 folio 转换'
---

# 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

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

```c
// 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: 复合页管理

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

```mermaid
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

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

```mermaid
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

```c
// 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 参与
```

---

## 调试

```bash
# 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*
