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 实例。
inode 与 address_space 关联关系 struct inode i_mapping → &i_data i_data (address_space) i_sb → super_block struct address_space host → inode i_pages (xarray/基数树) a_ops → 操作函数表 nrpages / writeback_index i_mapping host page[0] index=0 page[1] index=1 page[N] index=N 关联规则 常规文件:i_mapping = &i_data 块设备:指向 bdev 的 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 页在生命周期中经历以下关键状态转换:

  1. 分配 & 读入: 页被分配并填充数据后,设置 PG_uptodate,清除 PG_dirty。
  2. 修改(写操作): 应用程序写入页后,内核设置 PG_dirty,标记页为脏。
  3. 回写开始: 回写线程锁定页(PG_locked),设置 PG_writeback,提交 I/O 请求。
  4. 回写完成: I/O 完成后清除 PG_writeback 和 PG_dirty,设置 PG_uptodate,解锁页。
  5. 回收: 当内存压力大时,干净页被回收,page 结构归还伙伴系统。
Page Cache 页生命周期与标志位转换 分配 / 读入 PG_uptodate=1 脏页 PG_dirty=1 回写中 PG_writeback=1 干净页 PG_dirty=0 回收 释放 page 写操作 回写开始 I/O 完成 内存回收 再次读入

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 中的数据刷入磁盘。