2. Page Cache 数据结构:address_space、inode 与 struct page
Page Cache 的核心数据结构围绕三个关键对象构建:address_space(地址空间)、inode(索引节点)和 struct page(页描述符)。这三者通过精巧的指针和链表组织在一起,构成了文件数据在内存中的缓存骨架。理解它们之间的关系,是掌握回写机制的前提。
核心要点: address_space 是 inode 与 page 之间的桥梁,负责管理一个文件在内存中的所有缓存页;inode 通过
i_mapping 指针指向 address_space;每个缓存页通过 mapping 和 index 字段定位到所属的 address_space 及文件内的偏移位置。
2.1 address_space 结构体详解
address_space 是 Page Cache 的“调度中心”,定义在 linux/fs.h 中。每个打开的文件(inode)都对应一个 address_space 实例,负责管理该文件所有已缓存的物理页。
struct address_space {
struct inode *host; /* 指向宿主 inode */
struct radix_tree_root i_pages; /* 基数树根节点,存储所有缓存页 */
atomic_t i_mmap_writable;/* 可写映射计数 */
struct rb_root_cached i_mmap; /* 红黑树,管理所有内存映射区域 */
struct rw_semaphore i_mmap_rwsem; /* 保护 i_mmap 树的读写信号量 */
unsigned long nrpages; /* 当前缓存页总数 */
unsigned long nrexceptional; /* 影子条目数(如 DAX 异常页) */
pgoff_t writeback_index;/* 回写起始页索引 */
const struct address_space_operations *a_ops; /* 操作函数表 */
unsigned long flags; /* 标志位,如 AS_EIO/AS_ENOSPC */
spinlock_t private_lock; /* 私有锁 */
struct list_head private_list; /* 私有链表 */
void *private_data; /* 私有数据指针 */
} __attribute__((aligned(sizeof(long))));
| 字段 | 作用 | 回写相关说明 |
|---|---|---|
host |
指向所属的 inode | 回写时通过 host 获取文件操作函数 |
i_pages |
基数树根,存储所有缓存页 | 回写遍历脏页时需锁定树结构 |
nrpages |
缓存页总数 | 用于统计和内存回收判断 |
writeback_index |
回写起始页索引 | 轮询回写时记录断点,避免饥饿 |
a_ops |
操作函数表 | 包含 writepage、readpage 等关键回调 |
flags |
地址空间标志 | AS_EIO 表示回写发生 I/O 错误 |
关键设计:
i_pages 字段在 4.20 之前是 radix_tree_root,之后迁移为 xarray(扩展数组)。xarray 提供了更简洁的 API 和更好的 RCU 支持,但逻辑上仍然是“页偏移 → struct page *”的映射。
2.2 inode 与 address_space 的关联
每个 inode 结构体中包含两个关键指针,用于连接 address_space:
struct inode {
...
struct address_space *i_mapping; /* 指向 address_space */
struct address_space i_data; /* 内嵌的 address_space 实例 */
...
};
关联规则非常直接:
- 常规文件:
i_mapping指向&i_data,即 inode 自身内嵌的 address_space。 - 块设备文件:
i_mapping指向bdev->bd_inode->i_mapping,共享块设备的全局缓存。 - 特殊文件系统(如 tmpfs): 可能自定义
i_mapping指向独立的 address_space 实例。
2.3 struct page 与 PG_xxx 标志位
struct page 是物理内存页的描述符,每个物理页帧(通常 4KB)对应一个 page 实例。在 Page Cache 上下文中,page 承载着文件数据,并通过标志位记录页面的状态。
struct page {
unsigned long flags; /* 原子标志位,PG_xxx 系列 */
atomic_t _refcount; /* 引用计数 */
atomic_t _mapcount; /* 映射计数 */
struct address_space *mapping; /* 所属 address_space(低比特位编码) */
pgoff_t index; /* 在文件内的页偏移 */
struct list_head lru; /* LRU 链表节点 */
void *freelist; /* 空闲链表(用于伙伴系统) */
struct list_head buddy_list; /* 伙伴链表 */
...
unsigned long private; /* 私有数据,如 buffer_head 指针 */
};
注意:
mapping 字段的低 2 位被用于编码特殊信息:bit0 为 1 表示匿名页(匿名映射),bit1 为 1 表示该页属于交换缓存。Page Cache 页的 mapping 字段指向 address_space,且低 2 位为 0。
2.3.1 核心 PG_xxx 标志位
| 标志位 | 宏定义 | 含义 | 回写相关 |
|---|---|---|---|
| PG_locked | 0 | 页被锁定,禁止并发访问 | 回写时先锁定页,防止竞态 |
| PG_dirty | 4 | 页内容已被修改,需要写回磁盘 | 回写的核心标志,脏页列表依据 |
| PG_writeback | 8 | 页正在被写回磁盘 | 回写进行中,避免重复提交 I/O |
| PG_uptodate | 2 | 页内容与磁盘一致(数据有效) | 读页时检查,回写完成后设置 |
| PG_referenced | 3 | 页最近被访问过 | 影响 LRU 回收策略 |
| PG_swapbacked | 14 | 页由交换层支持(匿名页或 shmem) | 区分文件页与匿名页 |
| PG_error | 6 | 页发生 I/O 错误 | 回写失败时设置,触发错误处理 |
2.3.2 页状态转换与回写流程
一个 Page Cache 页在生命周期中经历以下关键状态转换:
- 分配 & 读入: 页被分配并填充数据后,设置
PG_uptodate,清除PG_dirty。 - 修改(写操作): 应用程序写入页后,内核设置
PG_dirty,标记页为脏。 - 回写开始: 回写线程锁定页(
PG_locked),设置PG_writeback,提交 I/O 请求。 - 回写完成: I/O 完成后清除
PG_writeback和PG_dirty,设置PG_uptodate,解锁页。 - 回收: 当内存压力大时,干净页被回收,page 结构归还伙伴系统。
2.4 从 inode 到 page 的查找路径
当内核需要访问某个文件偏移处的缓存页时,遵循以下查找路径:
// 给定 inode 和页偏移 index,查找缓存页
struct page *find_get_page(struct inode *inode, pgoff_t index)
{
struct address_space *mapping = inode->i_mapping;
// 在 xarray/基数树中查找
return xa_load(&mapping->i_pages, index);
}
// 设置脏页标记
void set_page_dirty(struct page *page)
{
struct address_space *mapping = page_mapping(page);
if (!mapping)
return;
// 调用 a_ops->set_page_dirty 或默认实现
mapping->a_ops->set_page_dirty(page);
}
性能关键路径: 从 inode 到 page 的查找完全在内存中完成,通过 xarray(O(log N) 复杂度)或基数树直接定位。这是 Page Cache 能够提供高性能缓存访问的基础。回写线程在遍历脏页时,同样依赖这个路径来获取每个需要回写的页。
2.5 关键数据结构关系总结
| 数据结构 | 核心字段 | 在回写中的角色 |
|---|---|---|
| inode | i_mapping, i_data |
文件元数据入口,提供 address_space |
| address_space | i_pages, a_ops, writeback_index |
管理所有缓存页,定义回写回调函数 |
| struct page | flags, mapping, index |
承载数据,通过 PG_dirty/PG_writeback 标记状态 |
| xarray/基数树 | 内部节点和叶子节点 | 提供页偏移到 page 指针的高效映射 |
理解要点: 这三个数据结构构成了 Page Cache 的“铁三角”。inode 代表文件本身,address_space 是缓存管理器,page 是实际的数据容器。回写机制的本质就是:遍历 address_space 中的脏页,调用 a_ops 中的 writepage 回调,将 page 中的数据刷入磁盘。