4. 页缓存的生命周期:页的分配、读取、缓存、回写与回收的完整流程

页缓存(Page Cache)是 Linux 内核文件 I/O 性能的核心基石。理解一个页帧从诞生到消亡的完整生命周期,是掌握内存回收与调优的前提。本章将沿着 分配 → 读取 → 缓存 → 回写 → 回收 这条主线,逐阶段剖析内核的行为与关键数据结构。

4.1 生命周期全景图

下图展示了页缓存中一个物理页帧的完整流转路径。每个阶段都对应特定的内核机制与函数调用。

页缓存生命周期全景图 ① 分配 alloc_page() ② 读取 mpage_readpage() ③ 缓存 address_space ④ 回写 wb_workfn() ⑤ 回收 shrink_page_list() 脏页标记 内存压力触发 页帧归还伙伴系统 关键路径:分配 → 读取 → 缓存 → 回写/回收 → 释放,形成完整闭环

4.2 阶段一:页的分配

页缓存的分配发生在进程发起 read() 或 mmap() 系统调用,且目标数据不在缓存中时。内核通过 按需分配 策略,从伙伴系统获取空闲页帧。

4.2.1 分配入口与路径

调用路径触发条件分配标志
do_generic_file_read() → page_cache_alloc()普通文件读,缓存未命中GFP_KERNEL
filemap_fault() → page_cache_alloc()内存映射缺页GFP_HIGHUSER_MOVABLE
__do_fault() → alloc_page_vma()写时复制缺页GFP_HIGHUSER

关键数据结构:每个页缓存页对应一个 struct page,其 mapping 字段指向所属的 address_space,index 字段记录文件内的页偏移。

4.2.2 分配时的 GFP 标志选择

  • GFP_KERNEL:普通进程上下文,允许直接回收和 I/O 等待。
  • GFP_HIGHUSER_MOVABLE:用户态可移动页,有利于内存规整(compaction)。
  • __GFP_FS:允许执行文件系统操作,回写脏页以释放内存。
  • __GFP_IO:允许发起块设备 I/O,用于从磁盘读取数据。

4.3 阶段二:页的读取

新分配的页帧内容为空,内核必须从磁盘或块设备将数据填充进来。这一过程称为 页读取(page read)。

4.3.1 同步读取流程

// 简化后的同步读页路径
int filemap_read_page(struct file *file, struct address_space *mapping,
                      struct page *page)
{
    // 1. 提交 bio 请求到块层
    struct bio *bio = submit_bio(REQ_OP_READ, page);
    
    // 2. 等待 I/O 完成
    wait_on_page_locked(page);
    
    // 3. 检查 I/O 错误
    if (PageError(page))
        return -EIO;
    
    // 4. 标记页为最新
    SetPageUptodate(page);
    return 0;
}

4.3.2 预读(readahead)优化

内核通过 预读算法 提前加载相邻页,减少同步等待。预读行为由 /sys/block/<dev>/queue/read_ahead_kb 控制。

预读模式触发条件预读窗口
同步预读首次访问文件4 页(默认)
异步预读顺序访问模式指数增长至 32 页
上下文预读随机访问但局部密集2~8 页

性能提示:对于大文件顺序读取,增大 read_ahead_kb 可显著提升吞吐量。对于数据库等随机 I/O 密集型应用,建议减小预读窗口以避免缓存污染。

4.4 阶段三:页的缓存

数据读取完成后,页帧进入 缓存状态,驻留在 address_space 的基数树(radix tree / xarray)中。此时页帧可能处于以下两种状态之一:

  • 干净页(Clean Page):内容与磁盘一致,可被直接回收。
  • 脏页(Dirty Page):内容已被修改,尚未写回磁盘,回收前必须回写。

4.4.1 缓存组织:xarray

内核使用 xarray(旧称 radix tree)管理页缓存,以文件偏移为键,页指针为值。查找时间复杂度为 O(log n)。

// 在 address_space 中查找页缓存
struct page *find_get_page(struct address_space *mapping, pgoff_t index)
{
    // 1. 锁定 xarray
    xa_lock(&mapping->pages);
    
    // 2. 在 xarray 中查找
    struct page *page = xa_load(&mapping->pages, index);
    
    // 3. 增加引用计数
    if (page)
        get_page(page);
    
    // 4. 解锁
    xa_unlock(&mapping->pages);
    
    return page;
}

4.4.2 页引用计数与 pin

每个缓存页维护一个 _refcount 引用计数。当引用计数降为 0 时,页帧可被回收。以下操作会增加引用:

操作函数引用变化
进程映射get_page()+1
内核临时使用kmap()+1
块 I/O 进行中lock_page()+1
LRU 链表管理lru_cache_add()+1

4.5 阶段四:页的回写

脏页必须写回磁盘才能保证数据一致性。内核通过 pdflush/flusher 线程 和 周期性回写 机制完成这一任务。

4.5.1 回写触发条件

  1. 脏页比例超限:当脏页占内存比例超过 dirty_ratio(默认 20%)时,进程同步等待回写。
  2. 周期性回写:flusher 线程每 dirty_writeback_centisecs(默认 5 秒)唤醒一次。
  3. 过期回写:脏页驻留时间超过 dirty_expire_centisecs(默认 30 秒)时强制回写。
  4. sync/fdatasync 系统调用:用户显式要求刷盘。

4.5.2 回写流程

// flusher 线程主循环(简化)
void wb_workfn(struct bdi_writeback *wb)
{
    while (1) {
        // 1. 收集需要回写的脏页
        struct list_head *dirty_list = collect_dirty_pages(wb);
        
        // 2. 对每个 inode 执行回写
        list_for_each_entry(inode, dirty_list, i_io_list) {
            // 3. 调用文件系统回写方法
            mapping->a_ops->writepages(mapping, &wbc);
        }
        
        // 4. 等待 I/O 完成
        wait_for_completion(&wb->iowait);
        
        // 5. 清除脏页标记
        clear_page_dirty_for_io(page);
        
        // 6. 休眠直到下次触发
        schedule_timeout(dirty_writeback_interval);
    }
}

调优警告:过度降低 dirty_expire_centisecs 会导致频繁回写,增加 I/O 压力并降低写入吞吐量。建议在 SSD 上保持默认值,在 HDD 上可适当增大。

4.6 阶段五:页的回收

当系统内存不足时,内核启动 直接回收(direct reclaim) 或 kswapd 后台回收,将页缓存页从 LRU 链表中摘除并释放。

4.6.1 回收策略:双链 LRU

内核维护两条 LRU 链表:活跃链表(active list) 和 非活跃链表(inactive list)。页在两条链表间流动,只有非活跃链表尾部的页才被回收。

双链 LRU 回收机制 活跃链表(Active List) 非活跃链表(Inactive List) 页A 页B 页C 页X 页Y 页Z refault 第二次访问 回收释放 回收目标:非活跃链表尾部页(页Z),优先回收干净页,脏页需先回写

4.6.2 回收决策树

shrink_page_list() 函数对每个候选页执行以下判断:

  1. 页是否被锁定? → 跳过,等待下次扫描。
  2. 页是否为脏页? → 加入回写队列,不清除。
  3. 页是否被映射? → 解除所有页表映射。
  4. 页是否为交换缓存? → 释放交换缓存条目。
  5. 释放页帧 → 调用 __free_pages() 归还伙伴系统。

4.6.3 回收性能指标

指标含义健康值
pgscan_kswapdkswapd 扫描页数持续增长说明内存压力大
pgsteal_kswapdkswapd 成功回收页数应接近 pgscan
pgscan_direct直接回收扫描页数应接近 0,否则触发 OOM
allocstall分配 stall 次数应接近 0

核心原则:页缓存的生命周期是一个动态平衡过程。分配与回收的速度决定了系统的内存压力水平。通过监控 /proc/vmstat 中的回收相关计数器,可以准确判断当前内存压力状态。

4.7 生命周期总结

下表汇总了页缓存各阶段的关键特征与调优关注点:

阶段核心数据结构关键函数调优参数
分配struct pagealloc_pages()min_free_kbytes
读取biompage_readpage()read_ahead_kb
缓存xarrayfind_get_page()vfs_cache_pressure
回写bdi_writebackwb_workfn()dirty_ratio, dirty_background_ratio
回收LRU 链表shrink_page_list()swappiness, watermark_scale_factor