4. 页缓存的生命周期:页的分配、读取、缓存、回写与回收的完整流程
页缓存(Page Cache)是 Linux 内核文件 I/O 性能的核心基石。理解一个页帧从诞生到消亡的完整生命周期,是掌握内存回收与调优的前提。本章将沿着 分配 → 读取 → 缓存 → 回写 → 回收 这条主线,逐阶段剖析内核的行为与关键数据结构。
4.1 生命周期全景图
下图展示了页缓存中一个物理页帧的完整流转路径。每个阶段都对应特定的内核机制与函数调用。
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 回写触发条件
- 脏页比例超限:当脏页占内存比例超过
dirty_ratio(默认 20%)时,进程同步等待回写。 - 周期性回写:flusher 线程每
dirty_writeback_centisecs(默认 5 秒)唤醒一次。 - 过期回写:脏页驻留时间超过
dirty_expire_centisecs(默认 30 秒)时强制回写。 - 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)。页在两条链表间流动,只有非活跃链表尾部的页才被回收。
4.6.2 回收决策树
shrink_page_list() 函数对每个候选页执行以下判断:
- 页是否被锁定? → 跳过,等待下次扫描。
- 页是否为脏页? → 加入回写队列,不清除。
- 页是否被映射? → 解除所有页表映射。
- 页是否为交换缓存? → 释放交换缓存条目。
- 释放页帧 → 调用
__free_pages()归还伙伴系统。
4.6.3 回收性能指标
| 指标 | 含义 | 健康值 |
|---|---|---|
pgscan_kswapd | kswapd 扫描页数 | 持续增长说明内存压力大 |
pgsteal_kswapd | kswapd 成功回收页数 | 应接近 pgscan |
pgscan_direct | 直接回收扫描页数 | 应接近 0,否则触发 OOM |
allocstall | 分配 stall 次数 | 应接近 0 |
核心原则:页缓存的生命周期是一个动态平衡过程。分配与回收的速度决定了系统的内存压力水平。通过监控 /proc/vmstat 中的回收相关计数器,可以准确判断当前内存压力状态。
4.7 生命周期总结
下表汇总了页缓存各阶段的关键特征与调优关注点:
| 阶段 | 核心数据结构 | 关键函数 | 调优参数 |
|---|---|---|---|
| 分配 | struct page | alloc_pages() | min_free_kbytes |
| 读取 | bio | mpage_readpage() | read_ahead_kb |
| 缓存 | xarray | find_get_page() | vfs_cache_pressure |
| 回写 | bdi_writeback | wb_workfn() | dirty_ratio, dirty_background_ratio |
| 回收 | LRU 链表 | shrink_page_list() | swappiness, watermark_scale_factor |