3、页缓存分配与回收:page fault与缓存填充、内存回收机制(kswapd)、LRU链表与页面换出

页缓存(Page Cache)的生命周期由“分配填充”与“回收换出”两个核心阶段构成。当进程访问一个尚未缓存在内存中的文件页时,内核通过缺页异常(Page Fault)触发缓存分配与磁盘I/O填充;当系统内存压力上升时,kswapd内核线程与直接回收(Direct Reclaim)机制协同工作,依据LRU(Least Recently Used)链表的冷热状态选择页面进行换出或丢弃。本章深入剖析这一完整闭环。

核心要点: Page Cache的分配是“按需分配、延迟填充”的;回收则是“压力驱动、LRU分级”的。理解缺页路径与LRU链表结构是优化缓存命中率与内存利用率的前提。

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 缓存填充流程(以文件映射缺页为例)

  1. 地址查找: VMA(虚拟内存区域)定位,确认访问合法。
  2. Page Cache查找: 调用 find_get_page() 在radix tree(现为xarray)中搜索目标页。
  3. 分配页框: 若未命中,调用 page_cache_alloc() 从伙伴系统分配一个干净页框。
  4. 加入缓存: 通过 add_to_page_cache_lru() 将新页加入Page Cache的xarray,并同时加入LRU链表。
  5. 启动I/O: 调用 mpage_readpage() 或 aops->readpage() 发起磁盘读取。
  6. 映射建立: I/O完成后,更新进程页表,建立虚拟地址到物理页框的映射。
性能提示: Major Fault是Page Cache填充的主要性能瓶颈。预读(read-ahead)机制通过提前发起I/O,将Major Fault转化为Minor Fault,显著降低缺页延迟。
缺页异常与Page Cache填充流程 缺页异常与Page Cache填充流程 进程访问文件页 查找Page Cache (xarray) 是否命中? Minor Fault 更新页表,直接访问 Major Fault 分配新页框 加入LRU链表 启动磁盘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() 进行直接回收。直接回收是阻塞式的,会导致分配者进程休眠等待,是系统性能抖动的常见来源。

性能风险: 直接回收频繁发生通常意味着内存压力过大或kswapd回收速度跟不上分配速度。此时系统可能出现明显的I/O等待和进程D状态(Uninterruptible Sleep)。监控 /proc/vmstat 中的 pgscan_direct 和 pgsteal_direct 指标可以量化直接回收的严重程度。
kswapd与直接回收协同工作 kswapd与直接回收协同工作 内存水位线区域 MIN LOW HIGH 内存分配请求 空闲 > LOW? 直接分配成功 kswapd被唤醒 kswapd扫描LRU回收 空闲 > HIGH? 继续分配

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链表,具体步骤如下:

  1. 扫描不活跃链表: 从 inactive_list 尾部取出页面进行隔离(isolation)。
  2. 页面分类: 判断页面类型——脏页(Dirty)、干净页(Clean)、匿名页(Anonymous)、swap cache页。
  3. 处理脏页: 脏页需要先回写(writeback)才能释放。回写完成后,页面变为干净页。
  4. 释放页框: 干净页直接释放回伙伴系统;匿名页若配置了swap,则写入swap分区后释放。
  5. 活跃链表降级: 若扫描active_list,将长时间未被访问的页面移动到inactive_list尾部。
关键指标: /proc/meminfo 中的 Active(file) 和 Inactive(file) 分别表示文件页在活跃和不活跃LRU链表中的数量。当 Inactive(file) 持续下降而 Active(file) 保持高位时,说明内存压力大且页面访问模式不利于回收。
双链LRU结构与页面换出 双链LRU结构与页面换出 活跃链表 (Active List) 热页 (频繁访问) 热页 (频繁访问) 温页 (偶尔访问) 降级 (aging) 不活跃链表 (Inactive List) 冷页 (未访问) 冷页 (未访问) 待回收页 页面换出/回收 释放页框 / 写入swap 新分配页

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
实践建议: 对于高并发Web服务器或数据库,建议监控 /proc/zoneinfo 中的水位线状态和 /proc/vmstat 中的 pgscan_kswapd 与 pgscan_direct 比例。若 pgscan_direct 占比超过5%,说明内存压力较大,应考虑增加内存或调整 vm.min_free_kbytes。