引言

虚拟内存是Linux内核最核心的子系统之一。它不仅是进程隔离的基石,更是现代高性能计算中NUMA优化、大页映射、内存去重等功能的技术前提。本文将从x86-64架构的多级页表结构出发,深入解析虚拟地址到物理地址的完整转换链路,并详细拆解缺页异常(Page Fault)的全流程处理机制,最后结合COW、透明大页(THP)、KSM等高级特性,给出工程实践中的优化策略。

一、x86-64多级页表结构

在x86-64架构中,Linux采用四级(或五级)页表结构完成虚拟地址到物理地址的转换。以48位虚拟地址空间为例,地址被划分为若干索引字段,依次索引PGD→P4D→PUD→PMD→PTE各级页表。

// arch/x86/include/asm/pgtable_64.h
// 48位虚拟地址空间下的页表索引布局
// [47:39] PGD索引  | [38:30] P4D索引 | [29:21] PUD索引
// [20:12] PMD索引  | [11:0]  页内偏移

#define __PAGE_OFFSET_BASE      _AC(0xffff888000000000, UL)
#define PAGE_OFFSET             __PAGE_OFFSET_BASE
#define PGDIR_SHIFT             39
#define P4D_SHIFT               39
#define PUD_SHIFT               30
#define PMD_SHIFT               21
#define PAGE_SHIFT              12

Linux内核通过pgd_offset()→p4d_offset()→pud_offset()→pmd_offset()→pte_offset_map()的宏调用链逐级遍历页表。当启用5级页表(LA57)时,在PGD之上增加了P4D层级,支持57位虚拟地址空间(128PB)。实际遍历流程如下:

// 页表遍历的核心路径 (简化自 follow_page_pte)
static inline pte_t *follow_pte(struct mm_struct *mm, unsigned long addr, ...)
{
    pgd_t *pgd = pgd_offset(mm, addr);
    p4d_t *p4d = p4d_offset(pgd, addr);
    if (p4d_none(*p4d) || p4d_bad(*p4d))
        return NULL;
    
    pud_t *pud = pud_offset(p4d, addr);
    if (pud_none(*pud) || pud_bad(*pud))
        return NULL;
    
    pmd_t *pmd = pmd_offset(pud, addr);
    if (pmd_none(*pmd) || pmd_bad(*pmd))
        return NULL;
    
    return pte_offset_map(pmd, addr);
}

1.1 TLB与地址转换加速

每次页表遍历需要4次内存访问,代价极高。CPU通过TLB(Translation Lookaside Buffer)缓存近期使用的页表项,硬件的Page Walk Cache则缓存中间级页表。invpcid(单PCID失效)和tlbiel(本地TLB失效)指令用于进程切换时的选择性TLB维护。

// 进程切换时的TLB管理 (context_switch)
// 若CR4.PCIDE置位,则使用PCID避免全量flush
if (next->mm != prev->mm) {
    load_cr3(next->pgd);  // 切换页表根
    // 若支持INVPCID则选择性失效
    if (cpu_feature_enabled(X86_FEATURE_INVPCID))
        invpcid_flush_single_context(next->context);
}

二、缺页异常全链路处理

当CPU访问的虚拟地址在TLB中未命中,且页表中也找不到有效PTE时,触发缺页异常(Page Fault,中断向量14)。Linux内核的异常处理入口是do_page_fault(),该函数最终调用到通用的handle_mm_fault()。

// arch/x86/mm/fault.c
DEFINE_IDTENTRY_RAW_ERRORCODE(exc_page_fault)
{
    unsigned long address = read_cr2();  // CR2保存触发异常的虚拟地址
    do_page_fault(regs, error_code, address);
}

// mm/memory.c — 核心处理入口
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
                           unsigned int flags, struct pt_regs *regs)
{
    // 1. 查找或创建VMA
    // 2. 权限检查(读/写/执行)
    // 3. 分级处理: handle_pte_fault -> do_fault -> do_anonymous_page / do_read_fault
    ...
}

2.1 异常分类与分发

异常类型触发条件处理函数
匿名页面缺页首次访问malloc/mmap(ANON)分配的内存do_anonymous_page()
文件映射缺页首次访问mmap映射的文件内容do_fault() → do_read_fault()
写时复制(COW)写访问只读页(共享匿名页或文件页)do_wp_page()
交换区缺页页面被swap出,重新访问时读回do_swap_page()
大页缺页透明大页或hugetlbfs中的大页未分配do_huge_pmd_anonymous_page()
权限错误用户态写只读内核页或执行不可执行页发送SIGSEGV

2.2 写时复制(COW)详解

COW是fork()高效实现的关键。子进程继承父进程的页表,但所有可写页面标记为只读。任一进程写入时触发COW缺页,内核分配新物理页,复制内容,更新PTE。

// mm/memory.c — COW处理核心
static vm_fault_t do_wp_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    pte_t entry = vmf->orig_pte;
    
    // 情况1: 匿名页且只有一个引用 → 提升为可写,无需复制
    if (PageAnon(vmf->page) && page_mapcount(vmf->page) == 1) {
        pte_unprotect(vmf);  // 提升写权限
        return VM_FAULT_WRITE;
    }
    
    // 情况2: 多人引用 → 分配新页 + 复制内容
    new_page = alloc_page_vma(GFP_HIGHUSER, vma, vmf->address);
    copy_user_highpage(new_page, vmf->page, vmf->address, vma);
    set_pte_at(vma->vm_mm, vmf->address, vmf->pte, mk_pte(new_page, vma->vm_page_prot));
    return VM_FAULT_WRITE;
}

COW在容器场景中尤为重要:容器引擎共享基础镜像层的基础页面,仅在写入新数据时才触发页面复制,节省大量内存。

三、大页(Huge Pages)工程实践

3.1 透明大页(THP)

THP自动将连续的2MB(或1GB)小页面合并为大页,减少TLB压力和页表遍历开销。内核通过khugepaged内核线程后台扫描合并。

# 查看THP状态
cat /sys/kernel/mm/transparent_hugepage/enabled    # [madvise|always|never]
cat /sys/kernel/mm/transparent_hugepage/defrag      # 碎片整理策略

# 编程中使用madvise提示内核使用大页
madvise(addr, length, MADV_HUGEPAGE);   # 标记为适合大页
madvise(addr, length, MADV_NOHUGEPAGE); # 标记为不适合大页
// THP在缺页路径中的处理
// mm/huge_memory.c
vm_fault_t do_huge_pmd_anonymous_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    
    // 如果VMA太小不适合大页,或不满足对齐要求,回退到普通页
    if (!vma->vm_ops && transhuge_vma_suitable(vma, haddr)) {
        // 分配2MB透明大页
        page = alloc_hugepage_vma(GFP_TRANSHUGE, vma, haddr);
        set_pmd_at(vma->vm_mm, haddr, vmf->pmd, 
                   mk_pmd(page, vma->vm_page_prot));
        return VM_FAULT_DONE;
    }
    // 回退到普通PTE处理
}

THP使用建议:数据库(PostgreSQL、Redis)、JVM堆、DPDK等大块内存密集场景推荐启用。频繁内存分配释放的短生命周期进程可能因khugepaged的合并开销而劣化。

3.2 Hugetlbfs静态大页

对于需要确定性延迟的高性能场景(实时系统、NFV数据面),静态大页提供稳定的大页预留:

# 启动时预留1GB大页 (GRUB参数)
hugepagesz=1G hugepages=16 default_hugepagesz=1G

# 或运行时挂载
mount -t hugetlbfs hugetlbfs /dev/hugepages -o pagesize=1G

# 代码中显式使用
fd = open("/dev/hugepages/my_region", O_CREAT|O_RDWR, 0755);
ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_HUGETLB, fd, 0);
// 或使用MAP_HUGE_2MB / MAP_HUGE_1GB 指定大小

四、KSM与内存去重

Kernel Same-page Merging (KSM) 通过扫描可合并的匿名页面,将内容相同的页面合并为一个只读页面,减少内存占用——在虚拟化场景中极有价值(多个相同OS镜像的VM)。

// mm/ksm.c — KSM扫描与合并
static struct rmap_item *scan_get_next_rmap_item(struct mm_struct *mm, ...)
{
    // 遍历每个VMA的稳定/不稳定红黑树节点
    // 逐页计算checksum,与已注册页面比较
    // 找到重复页面则合并: replace_page()
}

// 合并操作
static int replace_page(struct mm_struct *mm, ...)
{
    // 1. 加锁页面,锁定两页引用计数
    // 2. 用读合并页替换原PTE
    // 3. 释放多余物理页
    // 写入时触发COW再次分页
}
# 启用KSM (默认关闭)
echo 1 > /sys/kernel/mm/ksm/run
echo 1000 > /sys/kernel/mm/ksm/pages_to_scan   # 每次扫描页数
echo 20 > /sys/kernel/mm/ksm/sleep_millisecs  # 扫描间隔

五、进程地址空间管理(VMA)

每个进程的mm_struct通过红黑树和区间链表维护VMA(Virtual Memory Area)。VMA描述了一段连续虚拟地址区域的属性(权限、映射类型、文件后端等)。

// 典型进程地址空间布局 (cat /proc/PID/maps)
// 地址范围          权限  offset   dev inode  路径
// 55a0e0a00000-55a0e0a01000 r--p ...         主程序代码段
// 55a0e0a01000-55a0e0a02000 r-xp ...         主程序可执行段
// 55a0e0a02000-55a0e0a03000 r--p ...         主程序只读段
// 7f1234000000-7f1234021000 rw-p ...         [heap]
// 7f1234021000-7f1234022000 ---p ...         保护页
// 7f7f80000000-7f7f80028000 r--p ...         libc.so
// 7ffe10000000-7ffe10020000 rw-p ...         [stack]
// 7ffe10020000-7ffe10021000 r--p ...         [vvar]
// 7ffe10021000-7ffe10022000 r-xp ...         [vdso]

// 快速VMA查找
struct vm_area_struct *vma = find_vma(mm, address);
if (vma && address >= vma->vm_start)
    return vma;  // 找到包含该地址的VMA
return NULL;     // 未映射地址 → 发送SIGSEGV

六、工程观测与调优工具

6.1 页表相关指标

# 查看进程内存映射
cat /proc/PID/smaps_rollup | grep -E 'Rss|Pss|Swap|Anon|File'

# 查看TLB相关perf事件
perf stat -e dTLB-load-misses,dTLB-store-misses,iTLB-load-misses ./bench

# 大页使用情况
cat /proc/meminfo | grep -i huge

# 异常统计
cat /proc/vmstat | grep -E 'pgfault|pgmajfault|thp|ksm'

6.2 eBPF追踪缺页事件

// BPF程序追踪do_page_fault
SEC("kprobe/handle_mm_fault")
int trace_mm_fault(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 addr = PT_REGS_PARM2(ctx);  // 触发异常的地址
    
    struct event e = {};
    e.pid = pid;
    e.addr = addr;
    bpf_get_current_comm(&e.comm, sizeof(e.comm));
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &e, sizeof(e));
    return 0;
}

6.3 常见问题排查

大量minor fault:正常行为,首次访问匿名页必触发。若频率异常高可能暗示内存碎片化或大页未生效。

major fault飙升:频繁从swap或文件读取页面,暗示物理内存不足或工作集过大。

COW风暴:fork()后父子进程同时写入大量共享页面,可尝试使用vfork()或posix_spawn()替代,或改为共享内存。

七、总结

Linux的虚拟内存管理系统通过多级页表、缺页异常分级处理、COW延迟复制、大页优化、KSM去重等机制,在通用性和性能之间取得了良好的平衡。深入理解这些机制对于系统调优和性能诊断至关重要:

  • 数据库/AI训练等大内存工作集:启用THP + 静态大页
  • 高并发短请求服务:评估THP收益,可能never更优
  • 容器/VPS场景:开启KSM去重同构页面
  • DPDK/SPDK:必须使用静态大页避免TLB miss
  • 实时系统:静态大页 + mlockall锁定物理页

每一层抽象都有对应的性能代价,选择正确的策略需要对应用场景有深刻理解——而这,正是内核工程师的核心价值所在。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部