3. Page Cache 生命周期:Page Fault 机制、缺页中断处理流程、匿名页与文件页的区别

Page Cache 的生命周期始于一次缺页中断(Page Fault)。当 CPU 访问一个虚拟地址,而对应的物理页面尚未建立映射时,MMU 会触发缺页异常,内核随后根据缺页类型执行不同的处理路径。理解缺页中断的完整流程,以及匿名页与文件页的本质差异,是掌握 Page Cache 预读与性能调优的基础。

3.1 缺页中断分类与触发条件

缺页中断并非单一类型,Linux 内核根据缺页原因将其分为三类:

缺页类型 触发条件 典型场景
轻微缺页(Minor Fault) PTE 存在但权限不足或未激活访问位;物理页已在内存中 共享库映射、COW 写时复制、Page Cache 命中但未建立映射
严重缺页(Major Fault) PTE 无效,物理页不在内存中,需要从磁盘读取 首次读取文件、swap 换入、mmap 文件未缓存
无效缺页(Invalid Fault) 访问非法地址或权限错误 空指针解引用、栈溢出、内核态访问用户态非法地址
性能关键指标:轻微缺页仅需建立页表映射,耗时约 1-10 µs;严重缺页涉及磁盘 I/O,耗时可达 1-10 ms,两者相差三个数量级。预读机制的核心目标之一就是将严重缺页转化为轻微缺页。

3.2 缺页中断处理流程

缺页中断处理是内核内存管理中最核心的路径之一。以下 SVG 展示了从 CPU 触发缺页到完成映射的完整流程:

缺页中断处理完整流程 CPU 访问虚拟地址 MMU 查找 TLB / 页表 PTE 是否有效? 有效 正常访问(无缺页) 无效 触发缺页异常 do_page_fault() 判断缺页类型 轻微缺页 建立页表映射 严重缺页 文件页:do_file_page() 匿名页:do_anonymous_page() 从磁盘读取数据到 Page Cache 分配零页或 swap 换入 返回用户态继续执行 关键性能差异 轻微缺页:仅建立映射,无 I/O 严重缺页:磁盘 I/O,延迟高 预读目标:严重缺页 → 轻微缺页
处理流程要点:
  • do_page_fault() 是缺页中断的统一入口,根据地址空间和 PTE 状态分发到不同处理函数。
  • 轻微缺页 仅需填充页表项,不涉及磁盘 I/O,是性能最优的路径。
  • 严重缺页 需要调用文件系统或 swap 层的 I/O 操作,是性能瓶颈的主要来源。
  • 预读机制通过提前触发轻微缺页,将后续的严重缺页转化为轻微缺页,从而降低访问延迟。

3.3 匿名页与文件页的本质区别

Linux 内核将物理页面分为两大类别:文件页(File-backed Page) 和 匿名页(Anonymous Page)。两者的生命周期、回收策略和 I/O 行为截然不同。

对比维度 文件页(File-backed Page) 匿名页(Anonymous Page)
数据来源 磁盘上的文件(普通文件、块设备、mmap) 进程堆、栈、数据段、动态分配内存
后备存储 原始文件(有磁盘映像) Swap 分区或 Swap 文件(无磁盘映像)
回写路径 通过文件系统写回磁盘(writeback) 通过 Swap 换出到交换空间
回收代价 低:脏页需回写,干净页直接丢弃 高:必须换出到 Swap,涉及 I/O
预读支持 支持(Page Cache 预读机制) 不支持(按需分配,无预读)
缺页处理 do_file_page() → 从文件读取 do_anonymous_page() → 分配零页或 swap 换入
典型用途 文件读写、mmap、共享库、可执行文件 malloc()、mmap(MAP_ANONYMOUS)、栈、堆
性能陷阱:匿名页的回收代价远高于文件页。当系统内存不足时,内核优先回收文件页(尤其是干净页),因为可以直接丢弃。匿名页必须换出到 Swap,这会导致严重的性能抖动。因此,过度使用匿名内存(如大量 malloc 不释放)会加剧内存压力,而合理利用文件页(如 mmap 文件)可以利用 Page Cache 的预读和缓存优势。

3.4 文件页缺页处理详解

文件页的缺页处理是 Page Cache 预读机制的核心入口。当进程访问一个尚未缓存的文件页时,内核执行以下步骤:

  1. 查找 Page Cache:通过文件索引节点(inode)和页偏移量在 radix tree(现为 xarray)中查找。
  2. Cache 命中:如果页面已在 Page Cache 中,建立页表映射,返回用户态(轻微缺页)。
  3. Cache 未命中:触发严重缺页,调用 do_file_page() → filemap_fault()。
  4. 触发预读:filemap_fault() 调用 page_cache_sync_readahead() 或 page_cache_async_readahead()。
  5. 分配物理页:分配新的物理页面,加入 Page Cache。
  6. 发起 I/O:通过 submit_bio() 向块层提交读取请求。
  7. 等待 I/O 完成:进程睡眠等待,I/O 完成后唤醒。
  8. 建立映射:更新页表,返回用户态。
// 文件页缺页处理核心路径(简化)
// mm/filemap.c

vm_fault_t filemap_fault(struct vm_fault *vmf)
{
    struct file *file = vmf->vma->vm_file;
    struct address_space *mapping = file->f_mapping;
    pgoff_t offset = vmf->pgoff;
    struct page *page;
    int error;

    // 1. 查找 Page Cache
    page = find_get_page(mapping, offset);
    if (likely(page)) {
        // Cache 命中:轻微缺页
        vmf->page = page;
        return VM_FAULT_MAJOR | VM_FAULT_RETRY;
    }

    // 2. Cache 未命中:触发预读
    page_cache_sync_readahead(mapping, &file->f_ra, file, offset, 1);

    // 3. 再次查找(预读可能已加载)
    page = find_get_page(mapping, offset);
    if (!page) {
        // 4. 分配新页并提交 I/O
        page = page_cache_alloc(mapping);
        error = mapping->a_ops->readpage(file, page);
    }

    // 5. 建立映射
    vmf->page = page;
    return VM_FAULT_MAJOR | VM_FAULT_RETRY;
}

3.5 匿名页缺页处理详解

匿名页的缺页处理相对简单,因为没有文件系统参与。其核心路径如下:

  1. 判断是否为写时复制(COW):如果是 fork 后的写访问,触发 COW 处理。
  2. 分配零页:首次访问时,映射到全局零页(ZERO_PAGE),实现共享只读。
  3. 写时复制:写操作触发 COW,分配新物理页,复制数据,更新映射。
  4. Swap 换入:如果页面已被换出,从 Swap 分区读取。
// 匿名页缺页处理核心路径(简化)
// mm/memory.c

static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
    struct page *page;
    int ret;

    // 1. 检查是否为写时复制
    if (vmf->flags & FAULT_FLAG_WRITE) {
        // 分配新页
        page = alloc_zeroed_user_highpage(vmf->vma, vmf->address);
        if (!page)
            return VM_FAULT_OOM;
        // 建立写权限映射
        ret = vmf->vma->vm_ops->page_mkwrite(vmf);
        return VM_FAULT_MAJOR;
    }

    // 2. 只读访问:映射到零页
    ret = vmf->vma->vm_ops->fault(vmf);
    return ret;
}
零页优化:Linux 内核使用全局唯一的零页(ZERO_PAGE)处理匿名页的首次只读访问。所有进程的未写入匿名页共享同一个物理零页,直到发生写操作时才分配真实物理页。这种延迟分配策略大幅减少了物理内存的浪费。

3.6 文件页与匿名页的回收优先级

内核的页面回收算法(kswapd 和直接回收)在处理文件页和匿名页时采用不同的策略:

页面回收优先级决策流程 内存不足触发回收 扫描 LRU 链表 优先回收文件页 干净页:直接丢弃 脏页:写回后丢弃 仍不足?回收匿名页 匿名页换出到 Swap 回收策略总结 1. 优先回收文件页(干净页优先) 2. 脏文件页写回后回收 3. 最后才换出匿名页
回收策略对性能的影响:
  • 文件页优势:干净文件页可以零代价回收,再次访问时只需从文件重新读取(利用预读)。
  • 匿名页劣势:匿名页必须换出到 Swap,换入换出均涉及磁盘 I/O,且无法利用预读优化。
  • 调优建议:对于大量文件 I/O 的场景,适当提高 vm.vfs_cache_pressure 可以加速 dentry/inode 缓存回收;对于数据库等使用大量匿名内存的场景,需要合理配置 Swap 空间和 swappiness 参数。

3.7 本章小结

核心概念 关键要点
缺页中断分类 轻微缺页(无 I/O)、严重缺页(磁盘 I/O)、无效缺页(非法访问)
处理流程 do_page_fault() → 判断类型 → do_file_page() / do_anonymous_page() → 建立映射
文件页特征 有后备存储(文件),支持预读,回收代价低,干净页可丢弃
匿名页特征 无后备存储(需 Swap),不支持预读,回收代价高,必须换出
性能调优方向 减少严重缺页次数,利用预读将严重缺页转化为轻微缺页,合理配置 Swap