3、页缓存分配与回收:page fault与缓存填充、内存回收机制(kswapd)、LRU链表与页面换出
页缓存(Page Cache)的生命周期由“分配填充”与“回收换出”两个核心阶段构成。当进程访问一个尚未缓存在内存中的文件页时,内核通过缺页异常(Page Fault)触发缓存分配与磁盘I/O填充;当系统内存压力上升时,kswapd内核线程与直接回收(Direct Reclaim)机制协同工作,依据LRU(Least Recently Used)链表的冷热状态选择页面进行换出或丢弃。本章深入剖析这一完整闭环。
3.1 缺页异常与缓存填充(Page Fault & Cache Fill)
进程通过 read()、mmap() 等系统调用访问文件时,若目标数据页不在Page Cache中,CPU触发Major Page Fault(主缺页)。内核缺页异常处理函数 do_fault() 或 filemap_fault() 负责分配新页框并启动磁盘I/O。
3.1.1 缺页类型与处理路径
| 缺页类型 | 触发条件 | 处理开销 | 对Page Cache的影响 |
|---|---|---|---|
| Minor Fault(轻微缺页) | 页表项无效,但页面已在内存中(如共享页、swap cache) | 低(仅更新页表) | 不分配新页,仅建立映射 |
| Major Fault(主缺页) | 页面不在内存中,需从磁盘读取 | 高(磁盘I/O + 页分配) | 分配新页框,填充数据后加入Page Cache |
| Invalid Fault(无效访问) | 访问非法地址或权限错误 | 触发SIGSEGV | 无影响 |
3.1.2 缓存填充流程(以文件映射缺页为例)
- 地址查找: VMA(虚拟内存区域)定位,确认访问合法。
- Page Cache查找: 调用
find_get_page()在radix tree(现为xarray)中搜索目标页。 - 分配页框: 若未命中,调用
page_cache_alloc()从伙伴系统分配一个干净页框。 - 加入缓存: 通过
add_to_page_cache_lru()将新页加入Page Cache的xarray,并同时加入LRU链表。 - 启动I/O: 调用
mpage_readpage()或aops->readpage()发起磁盘读取。 - 映射建立: I/O完成后,更新进程页表,建立虚拟地址到物理页框的映射。
3.2 内存回收机制:kswapd与直接回收
当系统内存不足时,内核必须回收部分页面以释放内存。Linux内核采用双路径回收策略:kswapd内核线程在后台异步回收,直接回收(Direct Reclaim)作为同步兜底机制。
3.2.1 kswapd工作原理
kswapd是每个内存节点(NUMA node)上的一个内核线程,其核心逻辑在 balance_pgdat() 函数中实现。它通过监控水位线(Watermark)决定是否启动回收:
| 水位线 | 含义 | kswapd行为 |
|---|---|---|
| WMARK_MIN | 最低警戒线,低于此线直接回收将被强制触发 | kswapd紧急回收,目标恢复到WMARK_LOW以上 |
| WMARK_LOW | 低水位线,kswapd开始后台回收的阈值 | kswapd被唤醒,扫描LRU链表回收页面 |
| WMARK_HIGH | 高水位线,内存充足 | kswapd休眠,停止回收 |
3.2.2 直接回收(Direct Reclaim)
当内存分配请求在kswapd回收完成前到达,且空闲内存低于WMARK_MIN时,分配路径会同步调用 try_to_free_pages() 进行直接回收。直接回收是阻塞式的,会导致分配者进程休眠等待,是系统性能抖动的常见来源。
/proc/vmstat 中的 pgscan_direct 和 pgsteal_direct 指标可以量化直接回收的严重程度。
3.3 LRU链表与页面换出
LRU链表是内核管理页面冷热状态的核心数据结构。Linux内核使用双链LRU(活跃链表与不活跃链表)来近似实现LRU语义,避免传统单链表的扫描开销。
3.3.1 双链LRU结构
每个内存节点(pglist_data)维护两组LRU链表:active_list 和 inactive_list。页面在链表之间移动遵循以下规则:
| 链表 | 页面特征 | 回收优先级 | 移动条件 |
|---|---|---|---|
| 活跃链表(Active) | 近期被访问过的页面,热数据 | 低(尽量保留) | 页面在inactive中被访问时,提升至active |
| 不活跃链表(Inactive) | 近期未被访问或仅被访问一次的页面,冷数据 | 高(优先回收) | 页面从active降级,或新分配的页面直接加入 |
3.3.2 页面换出流程
回收路径(kswapd或直接回收)调用 shrink_lruvec() 扫描LRU链表,具体步骤如下:
- 扫描不活跃链表: 从
inactive_list尾部取出页面进行隔离(isolation)。 - 页面分类: 判断页面类型——脏页(Dirty)、干净页(Clean)、匿名页(Anonymous)、swap cache页。
- 处理脏页: 脏页需要先回写(writeback)才能释放。回写完成后,页面变为干净页。
- 释放页框: 干净页直接释放回伙伴系统;匿名页若配置了swap,则写入swap分区后释放。
- 活跃链表降级: 若扫描active_list,将长时间未被访问的页面移动到inactive_list尾部。
/proc/meminfo 中的 Active(file) 和 Inactive(file) 分别表示文件页在活跃和不活跃LRU链表中的数量。当 Inactive(file) 持续下降而 Active(file) 保持高位时,说明内存压力大且页面访问模式不利于回收。
3.4 关键参数与调优方向
理解Page Cache的分配与回收机制后,可以通过调整内核参数优化缓存行为:
| 参数 | 位置 | 作用 | 推荐调整 |
|---|---|---|---|
vm.vfs_cache_pressure |
/proc/sys/vm/ | 控制dentry/inode缓存回收倾向(值越大越积极) | 文件服务器可降低至50 |
vm.swappiness |
/proc/sys/vm/ | 控制匿名页与文件页回收的偏好(0-100) | 数据库服务器建议设为10或更低 |
vm.min_free_kbytes |
/proc/sys/vm/ | 设置WMARK_MIN水位线,影响直接回收触发频率 | 大内存系统可适当提高 |
vm.page-cluster |
/proc/sys/vm/ | 控制swap换入/换出的批量页数 | SSD环境可设为0或1 |
/proc/zoneinfo 中的水位线状态和 /proc/vmstat 中的 pgscan_kswapd 与 pgscan_direct 比例。若 pgscan_direct 占比超过5%,说明内存压力较大,应考虑增加内存或调整 vm.min_free_kbytes。