Linux 内核虚拟内存管理是现代操作系统的核心支柱之一。它通过为每个进程提供独立的虚拟地址空间,实现了进程隔离、内存超配(overcommit)、按需调页(demand paging)和内存映射文件等关键能力。本文将深入剖析 Linux 内核虚拟内存子系统的完整架构,从页表管理、VMA(Virtual Memory Area)区域组织、Page Fault 处理路径,到反向映射(reverse mapping)、KSM 页面合并以及大页(Huge Pages)机制,全面揭示内核如何高效地管理虚拟地址到物理地址的映射。
一、虚拟内存架构总览
Linux 采用多级页表(Multi-level Page Table)实现虚拟地址到物理地址的转换。以 x86_64 架构为例,使用 4 级页表(PGD→PUD→PMD→PTE),虚拟地址空间划分为用户态(0x0000_0000_0000_0000 ~ 0x0000_7FFF_FFFF_FFFF)和内核态(0xFFFF_8000_0000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF)。
// 四级页表地址翻译过程(x86_64, 4KB 页)
// Virtual Address (48-bit effective):
// | PGD index (9) | PUD index (9) | PMD index (9) | PTE index (9) | Offset (12) |
//
// 翻译过程:
// 1. CR3 寄存器 → PGD 基址
// 2. PGD[pgd_index] → PUD 基址
// 3. PUD[pud_index] → PMD 基址
// 4. PMD[pmd_index] → PTE 基址
// 5. PTE[pte_index] → Physical Page Frame
// 6. Page Frame + offset = 物理地址
TLB(Translation Lookaside Buffer)作为页表缓存,存储最近使用的虚拟→物理映射。x86_64 通常有 L1 DTLB(64 项, 4KB 页)、L2 TLB(512 项+)和 STLB(共享 TLB)。当发生 Page Miss(TLB 未命中)时,硬件 Page Table Walker 自动遍历页表填充 TLB。
二、VMA(Virtual Memory Area)管理
2.1 mm_struct 与 VMAs 组织
每个进程的内存管理由 struct mm_struct 描述,其中 VMAs 通过红黑树和双向链表两种结构组织,兼顾范围查询和遍历性能:
// 关键数据结构(简化)
struct mm_struct {
struct rb_root mm_rb; // VMA 红黑树根(按起始地址排序)
struct vm_area_struct *mmap; // VMA 链表头
atomic_t mm_users; // 用户引用计数
atomic_t mm_count; // 主引用计数
unsigned long start_code; // 代码段起始
unsigned long end_code; // 代码段结束
unsigned long start_data; // 数据段起始
unsigned long end_data; // 数据段结束
unsigned long start_brk; // 堆起始
unsigned long brk; // 堆当前顶部
unsigned long start_stack; // 栈起始
unsigned long rss; // 驻留页面计数
unsigned long total_vm; // 总虚拟页面数
pgd_t *pgd; // 全局页目录基址
rwlock_t page_table_lock;// 页表锁
};
struct vm_area_struct {
unsigned long vm_start; // 区域起始地址
unsigned long vm_end; // 区域结束(不包含)
struct mm_struct *vm_mm; // 所属 mm_struct
pgprot_t vm_page_prot; // 访问权限
unsigned long vm_flags; // 标志 (READ/WRITE/EXEC/SHARED)
struct rb_node vm_rb; // 红黑树节点
const struct vm_operations_struct *vm_ops; // 操作函数表
unsigned long vm_pgoff; // 文件映射时的页偏移
struct file *vm_file; // 映射的文件(如有)
};
2.2 典型进程内存布局
// x86_64 Linux 进程典型内存布局(从高地址到低地址):
//
// 0xFFFF_FFFF_FFFF_FFFF ┐
// │ 内核空间 (128TB)
// 0xFFFF_8000_0000_0000 ┘
// 0x0000_7FFF_FFFF_FFFF ┐
// │ 栈 (Stack) — 向下增长
// │ ↓
// │ 随机偏移 (ASLR)
// │
// │ mmap 区域(共享库/文件映射)
// │ ↓(Linux 默认栈向 mmap 区增长方向相反)
// │
// │ 堆 (Heap) — brk 向上增长
// │ ↑
// │ BSS 段
// │ Data 段
// │ Text 段 (代码段)
// 0x0000_0000_0040_0000 ┘ (典型 ELF 加载地址)
2.3 VMA 查找与合并
内核在每次 VMA 操作时通过 find_vma() 在红黑树中查找包含给定地址的 VMA。如果新映射区域与现有 VMA 相邻且属性相同,内核会自动合并它们,减少 VMA 节点数量:
// VMA 合并判断逻辑
struct vm_area_struct *vma = find_vma(mm, addr);
if (vma && vma->vm_start <= addr + len) {
// 存在重叠或相邻
if (vma->vm_flags == flags &&
vma->vm_pgoff + ((vma->vm_end - vma->vm_start) >> PAGE_SHIFT) == pgoff &&
!is_merge_constraint_violated()) {
// 可以合并,扩展现有 VMA
vma->vm_end = addr + len;
return vma;
}
}
三、Page Fault 处理机制
3.1 Page Fault 类型
CPU 在虚拟地址翻译失败时触发 Page Fault(异常 #14),Linux 内核根据具体情况分为三类:
- Minor Page Fault(次缺页):页面在物理内存中但未建立页表映射(如共享库首次访问、写时复制后)。无需磁盘 I/O,仅需修改页表项。
- Major Page Fault(主缺页):页面不在物理内存中,需从磁盘(swap/file)加载。涉及 I/O 操作,延迟高(毫秒级)。
- Invalid/Protection Fault(无效/保护缺页):访问未映射区域(如 NULL 指针解引用)或违反权限(写只读页,触发 COW)。导致 SIGSEGV 或 COW 分裂。
3.2 缺页处理主路径
x86_64 架构下,Page Fault 的处理入口是 do_page_fault()(已整合到 exc_page_fault()),核心调用链如下:
// Page Fault 处理流程(简化)
exc_page_fault (arch/x86/mm/fault.c)
└── __do_page_fault()
├── 1. 获取故障地址 (CR2 寄存器)
├── 2. 查找 VMA (find_vma)
│ ├── VMA 不存在 → SIGSEGV (SEGV_MAPERR)
│ ├── 权限不匹配 → SIGSEGV (SEGV_ACCERR)
│ └── VMA 有效 ↓
├── 3. handle_mm_fault()
│ ├── handle_pte_fault() — PTE 级处理
│ │ ├── do_anonymous_page() — 匿名页首次访问
│ │ ├── do_fault() — 文件映射缺页
│ │ │ ├── do_read_fault() — 读文件 (filemap_fault)
│ │ │ ├── do_cow_fault() — 写时复制
│ │ │ └── do_shared_fault()— 共享文件写
│ │ └── do_swap_page() — 从 swap 换入
│ ├── do_huge_pmd_anonymous_page() — 大页匿名映射
│ └── __handle_mm_fault (PMD/PUD 级)
└── 返回 (若成功,CPU 自动重试故障指令)
3.3 需求分页(Demand Paging)
Linux 使用延迟分配策略:当进程调用 mmap() 申请大块内存时,内核仅创建 VMA 而不分配物理页面。实际物理页面分配推迟到首次访问时通过 Page Fault 完成:
// do_anonymous_page: 匿名页首次访问处理
static vm_fault_t do_anonymous_page(struct vm_fault *vmf) {
// 1. 检查是否允许延迟分配(vma->vm_flags & VM_PFNMAP?)
// 2. 分配一个物理页面 (alloc_zeroed_user_highpage_movable)
// 3. 建立 PTE 映射 (mk_pte + set_pte_at)
// 4. 更新 RSS 计数器
// 5. 返回 FAULT_FLAG_WRITE 以允许后续写入
page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
if (!page) return VM_FAULT_OOM; // 内存不足
// 填入页表
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address, &ptl);
entry = mk_pte(page, vma->vm_page_prot);
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
return 0; // 成功
}
3.4 写时复制(Copy-on-Write)
当 fork() 创建子进程时,Linux 不会立即复制父进程的物理页面。父子共享同一组物理页,但页表项被标记为只读。任何一方尝试写入时触发 COW Fault,内核才真正复制页面:
vm_flags &= ~VM_WRITE)
//
// 2. 任一进程尝试写入共享页 → do_wp_page()
// - 检查 page->_refcount:
// - refcount == 1 → 无其他持有者,直接标记可写
// - refcount > 1 → 需要复制:
// a. 分配新物理页面
// b. 复制内容 (copy_user_highpage)
// c. 建立新 PTE 映射 (可写)
// d. 旧页面 refcount--
// do_wp_page 核心逻辑
static vm_fault_t do_wp_page(struct vm_fault *vmf) {
page = vmf->page;
if (page_count(page) == 1) {
// 独占页面,直接升级写权限
entry = pte_mkwrite(ppo_mkdirty(vmf->orig_pte));
set_pte_at_notify(mm, vmf->address, vmf->pte, entry);
return 0;
}
// 多持有者 → 复制新页面
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
copy_user_highpage(new_page, page, vmf->address, vma);
__SetPageUptodate(new_page);
set_pte_at_notify(mm, vmf->address, vmf->pte,
mk_pte(new_page, vma->vm_page_prot));
return 0;
}
四、反向映射(Reverse Mapping, RMAP)
4.1 为什么需要 RMAP
页表(PTE)是正向映射:虚拟地址→物理地址。但当内核需要回收或换出一个物理页面时,必须知道哪些进程的页表项指向它,才能全部失效(unmap)并更新。这就是反向映射的作用。
4.2 匿名页 RMAP(anon_vma)
匿名页通过 anon_vma 链表维护所有映射到该页的 VMA 实例:
// 匿名页 RMAP 结构 (简化)
struct anon_vma {
struct anon_vma *root; // 根 anon_vma(fork 时合并)
atomic_t refcount; // 引用计数
unsigneddegree degree; // VMA 嵌套层级
struct rb_root rb_root; // interval tree(区间树,用于高效查找)
};
struct anon_vma_chain {
struct vm_area_struct *vma; // 指向的 VMA
struct anon_vma *anon_vma; // 所属的 anon_vma
struct list_head same_vma; // 链入 VMA 的 anon_vma_chain 列表
struct list_head rb; // 按区间排序的红黑树节点
};
// 遍历同页面所有映射项
static void unmap_anon_vma(struct page *page) {
struct anon_vma *anon_vma = page_anon_vma(page);
struct anon_vma_chain *avc;
pgoff_t pgoff = page->index;
// 遍历所有 avc,对每个 VMA 调用 teardown
anon_vma_interval_tree_foreach(avc, &anon_vma->rb_root, pgoff, pgoff) {
struct mm_struct *mm = avc->vma->vm_mm;
unsigned long address = vma_address(page, avc->vma);
// 取消映射
pte_get_and_clear_full(mm, address, pte, 0);
}
}
4.3 文件页 RMAP(address_space)
文件映射(file-backed)页面的反向映射通过 address_space 的优先搜索树(priority search tree, PST)实现。由于文件页可能被数千个进程共享(如共享库的代码段),PST 支持高效的多索引查询:
五、Swap 与页面回收
5.1 LRU 页面回收算法
Linux 使用双链表 LRU(Least Recently Used)实现页面回收。每个内存区域(zone)维护 Active/Inactive 两个文件页和匿名页链表,共四个 LRU 链表:
5.2 Swap Slab 与换入换出
当页面需要被换出到 swap 时,内核通过 swap_entry_t 编码 swap 设备 ID 和页面偏移,填入 PTE。再次访问时触发 Page Fault,内核识别 PTE 类型后从 swap 读入:
六、大页(Huge Pages / THP)
6.1 透明大页(Transparent Huge Pages)
Linux 内核支持 2MB 甚至 1GB 的大页(x86_64),以减少 TLB 未命中和页表开销。THP(Transparent Huge Pages)允许内核自动将普通页面合并为大页:
6.2 hugetlb 静态大页
静态大页通过启动参数 hugepages= 预留内存(必须连续物理池),适用于 DPDK/数据库等场景:
七、KSM(Kernel Same-page Merging)
Kernel Same-page Merging 允许内核扫描已注册的红黑树中的页面,识别重复页面并合并:
八、内存压力与直接回收(Direct Reclaim)
当进程自身在分配内存时遇到 low watermark,会触发直接回收(Direct Reclaim):
九、mmap 与零拷贝内存映射
9.1 mmap 系统调用
mmap() 将文件或设备映射到进程地址空间,允许进程通过内存读写操作与文件交互:
9.2 MAP_POPULATE 与 MADVISE
对于希望避免 Minor Fault 的场景,可使用 MAP_POPULATE 预分配物理页面,或通过 madvise() 提供访问模式提示:
十、性能监控与调优
10.1 Page Fault 监控工具
10.2 减少 Major Fault 的最佳实践
- 增加 file cache 热页驻留:调整
vm.min_free_kbytes、vm.swappiness(降低值避免换出缓存页) - 预读优化:使用
MAP_POPULATE或posix_fadvise()提前加载大文件 - 大页加速 TLB 密集场景:数据库/Redis 等建议使用 THP 或 hugetlb
- <numactl 绑核:避免跨 NUMA 节点内存访问,
numactl --cpunodebind=0 --membind=0 ./app - 禁用 swap 对延迟敏感场景:
vm.swappiness=1或mlockall(MCL_CURRENT | MCL_FUTURE)
10.3 TLB Shootdown 注意事项
当一个进程 munmap() 或修改页表权限时,其他 CPU 上可能缓存了旧的 TLB 条目。此时内核发送 IPI(Inter-Processor Interrupt)请求这些 CPU 执行 TLB 失效,这被称为 TLB Shootdown:
十一、实战案例:高并发内存池的 Page Fault 优化
某高并发交易系统在使用 glibc malloc 时遇到启动延迟高的问题,分析发现原因是大量 Minor Fault:
十二、总结
Linux 内核虚拟内存管理是整个操作系统的核心子系统。它通过多级页表、VMA 组织、需求分页、写时复制、反向映射、LRU 回收、透明大页等机制,在安全性、隔离性和性能之间取得了精妙的平衡。
关键要点总结:
- 延迟分配:mmap 只创建 VMA,物理页面在首次访问(Page Fault)时分配
- COW 优化 fork:父子进程共享页面,写入时才复制
- RMAP 支持回收:通过 anon_vma(匿名页)和 address_space(文件页)快速找到所有持有者
- LRU 链表分级:Active/Inactive × File/Anonymous 四链表,回收时优先丢弃文件页缓存
- THP 大页加速:khugepaged 自动合并 4KB → 2MB 大页,减少 TLB Miss
- 性能调优指标:Major Fault 是关键指标,反映 cache 命中率;减少 Major Fault 就是要增加文件缓存驻留
深入理解虚拟内存机制,是诊断系统性能问题(抖动、OOM、TLB Miss)和开发高性能程序(内存池、预读、大页利用)的基础。

发表评论 取消回复