Linux 匿名页与文件页映射机制:深入解析 do_anonymous_page 源码实现

封面信息图

在 Linux 操作系统中,当用户态程序调用 malloc(100 * 1024 * 1024)mmap(MAP_ANONYMOUS | MAP_PRIVATE) 申请 100MB 内存时,内核并没有真正从物理内存池中划拨出 25600 个 4KB 物理页帧。内核仅仅是在该进程的进程地址空间(struct mm_struct)中创建或扩展了一个虚拟内存区域(struct vm_area_struct, 简称 VMA),并设置了对应的访问权限。

这种**按需调页(Demand Paging)**机制使得物理内存的分配被推迟到 CPU 第一次通过虚拟地址执行读/写指令的那一刻。此时,MMU 发现虚拟地址对应的页表项(PTE)尚未建立有效映射,便会触发 CPU 缺页异常(Page Fault),CPU 陷入内核态执行缺页处理例程。

如果是没有后备文件支持的堆、栈或 MAP_ANONYMOUS 映射区,内核将最终调用 do_anonymous_page() 函数。本文将深入 Linux 内核源码,逐行拆解该核心函数的实现精髓。


一、 缺页异常处理函数调用全景调用链

[ 用户态触发内存读写: *ptr = 0x42 ]
                 │
                 ▼
[ CPU MMU 触发 Page Fault 异常向量 (Vector 14) ]
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│ 1. do_page_fault() / exc_page_fault() (体系结构相关入口)     │
│   - 读取 CR2 寄存器获取导致缺页的虚拟地址 (Fault Address)     │
│   - 获取当前进程 mm->mmap_lock 读锁                         │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│ 2. handle_mm_fault() -> __handle_mm_fault()                 │
│   - 逐级遍历/创建页表: PGD -> P4D -> PUD -> PMD             │
│   - 进入 handle_pte_fault()                                 │
└──────────────────────────────┬──────────────────────────────┘
                               │ (判断 PTE 为空且 VMA 为匿名映射)
                               ▼
┌─────────────────────────────────────────────────────────────┐
│ 3. do_anonymous_page() [核心逻辑]                           │
│   ├── 读缺页 (Read Fault)  ──> 映射全局只读共享 Zero Page   │
│   └── 写缺页 (Write Fault) ──> 分配真实物理页 + 标记可写    │
└─────────────────────────────────────────────────────────────┘

二、 深入内核源码:do_anonymous_page 核心逻辑逐行剖析

以下源码摘录自 Linux 5.x / 6.x 内核 mm/memory.c(为增强可读性,保留关键骨架并附详细中文注释):

static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct page *page;
    pte_t entry;

    /* 1. 如果是非匿名 VMA (例如存在 vm_ops),说明代码逻辑异常,直接退出 */
    if (vma->vm_flags & VM_SHARED)
        return VM_FAULT_SIGBUS;

    /* 2. 检查页表锁,确保 PMD 级别并发安全 */
    if (pte_alloc(vma->vm_mm, vmf->pmd))
        return VM_FAULT_OOM;

    /* 3. 场景 A: 读缺页优化 (Read Fault Optimization) */
    if (!(vmf->flags & FAULT_FLAG_WRITE)) {
        /*
         * 如果用户只是读取这块未初始化的内存,内核绝不浪费分配真实物理页,
         * 而是直接返回全局预置的全零物理页帧 (ZERO_PAGE)!
         * 这个零页被标记为只读 (read-only)。
         */
        entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
                                      vma->vm_page_prot));
        vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
                                       vmf->address, &vmf->ptl);
        if (!pte_none(*vmf->pte))
            goto unlock;

        /* 将只读零页填入当前进程的 PTE 页表 */
        set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
        update_mmu_cache(vma, vmf->address, vmf->pte);
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        return 0;
    }

    /* 4. 场景 B: 写缺页处理 (Write Fault - 真实分配物理内存) */
    if (unlikely(anon_vma_prepare(vma)))
        return VM_FAULT_OOM;

    /*
     * 从伙伴系统 (Buddy System) 申请一个全零的高端物理页
     * GFP_HIGHUSER_MOVABLE: 允许该页在内存碎片整理时被移动
     */
    page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
    if (!page)
        return VM_FAULT_OOM;

    /* 设置该物理页为可移动匿名页,增加引用计数 */
    __SetPageUptodate(page);

    /* 生成具有可读、可写、脏标志位的 PTE 条目 */
    entry = mk_pte(page, vma->vm_page_prot);
    entry = pte_mkdirty(pte_mkwrite(pte_mkyoung(entry)));

    /* 获取 PTE 自旋锁,进行最终赋值与内存统计记账 */
    vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
                                   vmf->address, &vmf->ptl);
    if (!pte_none(*vmf->pte)) {
        /* 并发竞态检测:已有其他线程抢先映射,释放多余的物理页 */
        put_page(page);
        goto unlock;
    }

    /* 将该页加入反向映射匿名 VMA (RMAP 系统) */
    page_add_new_anon_rmap(page, vma, vmf->address, false);
    lru_cache_add_inactive_or_unevictable(page, vma);

    /* 真正写硬件页表并更新 TLB 缓存 */
    set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
    update_mmu_cache(vma, vmf->address, vmf->pte);

    /* 增加进程常驻物理内存计数器 (Resident Set Size, RSS) */
    inc_mm_counter(vma->vm_mm, MM_ANONPAGES);

unlock:
    pte_unmap_unlock(vmf->pte, vmf->ptl);
    return 0;
}

三、 匿名页 vs 文件页映射机制的本质差异

内核对待“匿名页”与“文件页”的处理机制截然不同,这直接决定了内存回收(Reclaim)与换页行为:

机制属性匿名页映射 (do_anonymous_page)文件页映射 (do_fault / filemap_fault)
物理后备介质 (Backing Store)无后备文件,仅存在于物理 RAM对应磁盘上的真实文件 inode 与 Page Cache
初始缺页数据源读缺页映射 ZERO_PAGE,写缺页清零物理页从磁盘驱动执行 block I/O 读入文件数据块
内存紧张时的回收手段必须被换出至 Swap 分区;若未开启 Swap 则不可释放直接将 Dirty 页面刷回磁盘,Clean 页面可直接丢弃释放
反向映射 (RMAP)依赖 struct anon_vma 链表追踪映射该页的进程依赖 struct address_space 和基数树/XArray

四、 生产环境实战:用 perf 与 ftrace 追踪缺页开销

在高并发网络服务或大模型推理框架中,频繁的大块内存申请与缺页会产生显著的 CPU 系统态(sys%)消耗。我们可以通过 perf 工具精准捕获 do_anonymous_page 的触发频率:

# 1. 监控指定进程的匿名页缺页事件
perf stat -e minor-faults,major-faults -p $(pgrep -f "inference_server") sleep 10

# 2. 利用 ftrace 跟踪 do_anonymous_page 调用耗时与调用栈
cd /sys/kernel/debug/tracing
echo 0 > tracing_on
echo 'p:my_anon_fault do_anonymous_page' > kprobe_events
echo 1 > events/kprobes/my_anon_fault/enable
echo 1 > tracing_on
sleep 2
echo 0 > tracing_on
cat trace | head -n 20
工程优化建议:
  1. 预热大块内存(Pre-faulting):对于实时性要求极高的交易引擎或推理服务,在初始化阶段使用 memset(ptr, 0, size)madvise(MADV_WILLNEED) 提前触发缺页,避免在核心处理链路中产生不可预测的上下文开销。
  2. 启用透明大页(Transparent Huge Pages, THP):将 4KB 缺页转换为 2MB 巨页(Huge Page),可将 PMD 级别的缺页次数降低 512 倍,大幅提升 TLB 缓存命中率。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐