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 的文件范围。
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 时,内核执行以下步骤:
- 计算页索引:
index = pos >> PAGE_SHIFT(通常右移 12 位) - 获取 address_space: 通过文件 inode 的
i_mapping指针 - xarray 查找: 调用
xa_load(&mapping->i_pages, index)获取struct page * - 检查页状态: 测试
PG_uptodate标志,判断数据是否已从磁盘读取 - 命中或缺失: 若页存在且 uptodate,直接返回;否则触发预读或同步读页
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 标志触发异步预读 |