2. Page Cache 数据结构:address_space、xarray 与 struct page

Page Cache 是 Linux 内核文件 I/O 性能的核心。要理解预读机制如何工作,必须先掌握其底层数据结构。本章深入剖析三个关键组件:address_space(地址空间)、xarray(索引树)和 struct page(页描述符)。它们共同构成了 Page Cache 的骨架,决定了缓存页的查找、组织与生命周期管理。

2.1 address_space:文件与页缓存之间的桥梁

每个打开的文件(inode)在内核中都有一个 struct address_space 实例,它负责管理该文件在内存中的所有缓存页。它是文件系统与页分配器之间的中间层。

字段 类型 作用
host struct inode * 指向所属的 inode,建立文件与缓存的双向关联
i_pages struct xarray 页索引树根节点,用于快速查找文件偏移对应的缓存页
nrpages unsigned long 当前缓存页总数,反映 Page Cache 占用大小
a_ops const struct address_space_operations * 操作函数表,包含 readpage、writepage、dirty_folio 等回调
i_mmap struct rb_root_cached 红黑树根,管理该文件的所有内存映射区域(mmap)
private_list struct list_head 用于关联缓冲区头(buffer_head)或其它私有数据
关键设计思想: address_space 将文件逻辑偏移(页索引)与物理内存页解耦。预读算法通过 i_pages 快速判断某页是否已缓存,从而决定是否发起磁盘 I/O。

2.2 xarray:取代 radix tree 的高效索引

从 Linux 4.20 开始,内核使用 xarray 替代了传统的 radix tree。xarray 是一种可扩展的稀疏数组,专为存储 struct page * 指针而优化。它提供了更简洁的 API 和更好的缓存局部性。

迁移背景: radix tree 的 API 复杂且容易出错,xarray 将树操作封装为数组风格的 xa_store / xa_load,大幅降低了内核内存管理的维护成本。
// xarray 核心操作示例(内核源码 mm/filemap.c)
// 查找文件偏移 index 对应的缓存页
struct page *page = xa_load(&mapping->i_pages, index);

// 插入一个缓存页到指定索引
xa_store(&mapping->i_pages, index, page, GFP_KERNEL);

// 从索引树中删除页
xa_erase(&mapping->i_pages, index);

xarray 的内部结构是一个多级树,每个节点可以存储 64 个条目(基于 6 位索引)。对于 4KB 页大小的文件系统,xarray 可以高效索引最大 16TB 的文件范围。

xarray 多级索引结构(文件偏移 → 缓存页) 根节点 (xa_head) 中间节点 0 中间节点 1 中间节点 N page[0] page[1] page[63] page[0] page[63] 每个中间节点最多存储 64 个条目(slot),通过文件偏移的位段逐级定位 查找时间复杂度 O(log₆₄ N),空间效率远高于哈希表

2.3 struct page:物理内存页的描述符

每个物理内存页(通常 4KB)对应一个 struct page 实例。它是内核内存管理的最小单元,在 Page Cache 上下文中承载文件数据。

字段 类型 Page Cache 相关含义
flags unsigned long 页状态标志:PG_locked(锁)、PG_uptodate(数据有效)、PG_dirty(脏页)、PG_referenced(被访问)等
mapping struct address_space * 指向所属的 address_space,标识该页属于哪个文件
index pgoff_t 文件内的页偏移(页号),与 xarray 的 key 对应
lru struct list_head 用于 LRU 链表链接,参与页面回收
refcount atomic_t 引用计数,决定页何时可以被释放
private unsigned long 文件系统私有数据,如 buffer_head 或块设备偏移
常见误区: struct page 并不存储文件数据本身,数据存放在页对应的物理内存中。struct page 只是一个 64 字节左右的描述符,通过 page_address() 或 kmap() 才能访问实际数据。

2.4 三者协作:一次缓存页查找的完整路径

当应用程序读取文件偏移 pos 时,内核执行以下步骤:

  1. 计算页索引: index = pos >> PAGE_SHIFT(通常右移 12 位)
  2. 获取 address_space: 通过文件 inode 的 i_mapping 指针
  3. xarray 查找: 调用 xa_load(&mapping->i_pages, index) 获取 struct page *
  4. 检查页状态: 测试 PG_uptodate 标志,判断数据是否已从磁盘读取
  5. 命中或缺失: 若页存在且 uptodate,直接返回;否则触发预读或同步读页
Page Cache 缓存页查找完整流程 ① 计算页索引 ② 获取 address_space ③ xarray 查找 ④ 检查 PG_uptodate ✅ 命中:直接返回页 ❌ 缺失:触发预读/读页 预读算法在步骤③发现页缺失时,会提前批量插入多个页到 xarray 并启动磁盘读取 address_space 的 nrpages 字段在此过程中动态增长,反映缓存热度

2.5 关键标志位与预读决策

struct page 的 flags 字段中,以下标志位直接参与预读和缓存管理决策:

  • PG_locked:页正在被 I/O 操作占用,预读算法会跳过锁定页,避免阻塞。
  • PG_uptodate:页数据已从磁盘读取完毕。预读页在 I/O 完成后设置此标志。
  • PG_referenced:页最近被访问过。内核使用此标志结合 LRU 链表判断页是否应被回收。
  • PG_dirty:页内容已被修改但尚未写回磁盘。脏页不会被预读算法驱逐。
  • PG_readahead:标记该页是预读页。当应用访问此页时,内核会触发下一轮预读。
预读触发信号: 当 page->flags 包含 PG_readahead 且应用发起读请求时,内核的 page_cache_async_readahead() 函数会被调用,启动异步预读窗口扩展。

2.6 数据结构关系总览

将上述三个数据结构串联起来,形成完整的 Page Cache 数据视图:

层次 数据结构 职责 预读相关操作
文件层 struct inode 文件元数据,包含 i_mapping 指针 预读参数(ra_pages)存储于此
地址空间层 struct address_space 管理所有缓存页,提供操作函数表 a_ops->readahead() 回调入口
索引层 struct xarray 快速查找文件偏移对应的页 预读批量插入页时使用 xa_store
页描述符层 struct page 描述物理页状态、归属、引用计数 PG_readahead 标志触发异步预读