重读 Linux VM:给系统研究生的虚拟内存导读
Linux VM 容易读散。它表面上在讲 mmap、页表、缺页异常、匿名页、page cache、folio、LRU、swap、NUMA、huge page、slab、cgroup memory 和 OOM killer,读起来像一串彼此相邻但没有主线的内核机制;主线可以压成一句话:
Linux VM 是一套把虚拟地址、物理页、文件、进程、CPU MMU 和 I/O 统一到同一个状态转移系统里的内核机制。
这句话一旦立住,虚拟内存就不再只是“地址翻译”或“把内存变大”。地址空间描述进程能够访问什么;页表描述 CPU 当前怎样翻译地址;物理页描述内核手里真实可调度的资源;page cache 把文件内容放进同一套页管理体系;缺页异常负责把尚未兑现的映射变成真实页面;页回收和 swap 则在内存压力下重新分配这些页面。
这套系列不打算复述操作系统教材。教材通常从分页、分段、TLB、页面置换算法讲起,适合建立基本概念;内核源码则要反过来读:先抓住对象关系,再顺着热路径看一次状态变化。目标读者默认已经学过操作系统和体系结构,能读 C 代码,也愿意用实验验证内核行为。
为什么 Linux VM 会显得难
难点不在术语数量,而在视角切换太频繁。
从用户态看,内存像一段连续地址。malloc 返回一个指针,mmap 返回一段区间,读写指针就像读写数组。这个视角很干净,但它隐藏了三件事:地址区间未必已经有物理页,物理页未必只属于一个进程,文件内容也可能通过 page cache 参与同一套内存管理。
从 CPU 看,内存访问是虚拟地址到物理地址的翻译。MMU 查 TLB,TLB 未命中就走页表,页表项不存在或权限不满足就触发异常。这个视角很硬,但它只看见硬件翻译,不解释为什么一个地址区间存在、它来自匿名内存还是文件、写入时是否需要 copy-on-write。
从内核看,虚拟内存是一批可以延迟兑现的承诺。mmap 建立的是虚拟地址区间,页表项可以稍后填;fork 复制的是地址空间描述和页表关系,物理页可以等到写入时再复制;文件映射建立的是文件偏移到虚拟区间的关系,页面可以等到访问时再读入。
这三个视角同时成立,Linux VM 的复杂性也从这里来。一个页面既可能是 CPU 正在翻译的结果,也可能是某个 VMA 里的映射对象,还可能在 LRU 链表里等待回收,或者在 page cache 里代表文件的一段内容。
先把主线画出来
后续文章会反复使用这条路径:
1 | |
process 是读者在用户态看到的进程。到了内核里,地址空间由 struct mm_struct 描述。同一个进程的多个线程共享同一个 mm_struct,因为它们共享虚拟地址空间。
mm_struct 下面不是每个虚拟地址一条记录,而是一组 VMA。VMA 是 virtual memory area,表示一段连续虚拟地址区间,以及这段区间的权限、来源和操作方法。现代 Linux 用 maple tree 组织一个地址空间中的 VMA,这一点比很多旧书里的红黑树版本更新。
页表是硬件可消费的翻译结构。VMA 说明“这个地址范围原则上应该怎样处理”,页表说明“这个虚拟页此刻是否已经映射到某个物理页,以及权限是什么”。二者不是同一层。没有这个分层,就很难理解为什么 mmap 可以很快返回,而第一次访问却可能触发 page fault。
struct page 和 folio 是内核管理物理内存的核心对象。它们不是用户态指针,也不是页表项本身,而是内核给物理页或一组连续页附上的元数据。页是否在 LRU 上,是否脏,是否正在写回,是否属于匿名页或文件页,都要落到这些元数据和相关结构里。
模式提炼:承诺和兑现分离
1 | |
虚拟内存的关键模式是“先承诺,再兑现”。地址空间可以先建立,物理页可以稍后分配,文件内容可以稍后读入,私有副本可以等到写入时再生成。
| 场景 | 先建立的承诺 | 兑现时机 | 兑现动作 |
|---|---|---|---|
| 匿名映射 | 一段可读写虚拟区间 | 第一次访问 | 分配物理页并填页表 |
| 文件映射 | 文件偏移到虚拟区间的关系 | 第一次读写 | 从 page cache 或磁盘取得页面 |
fork 后 COW |
父子进程共享只读页 | 任一方写入 | 复制新页并更新页表 |
| swap | 页面内容已转移到交换区 | 再次访问 | 换入页面并恢复映射 |
这个模式比“虚拟地址翻译成物理地址”更有解释力。地址翻译只描述已经兑现的状态,缺页异常描述的是兑现过程。
第一条线:地址空间如何被表示
读 Linux VM,第一条线是对象表示。
1 | |
task_struct 是调度器眼里的任务。任务如果有用户态地址空间,就通过 mm 指向 struct mm_struct。内核线程通常没有自己的用户态地址空间,这是理解 current->mm 和 active_mm 差异的入口。
mm_struct 是进程地址空间的总账。它记录页表根、VMA 集合、地址空间锁、统计计数,以及和用户态内存管理相关的一批字段。读源码时不要把它当成“进程对象”,它只负责内存视角。
vm_area_struct 是区间对象。一个 VMA 覆盖 [vm_start, vm_end),包含权限位、映射标志、文件指针、偏移量和一组 vm_ops。/proc/<pid>/maps 看到的每一行,大致都能对应到一个 VMA。
页表是翻译对象。Linux 当前以五级页表抽象组织代码:PGD、P4D、PUD、PMD、PTE。具体架构可以折叠某些层级,但内核通用代码倾向按五级模型书写,以便跨架构。
这一层的阅读目标不是背字段,而是建立区分:
| 对象 | 回答的问题 |
|---|---|
mm_struct |
这个任务组共享哪一个地址空间 |
vm_area_struct |
这个虚拟地址落在哪个区间,区间规则是什么 |
| 页表项 | 这个虚拟页此刻是否可翻译,权限和 PFN 是什么 |
struct page / folio |
物理页本身处在什么状态 |
第二条线:一次访问如何变成状态转移
第二条线是运行时路径。一个用户态读写可以有很多结果。
1 | |
TLB 命中时,内核不会参与。CPU 直接拿到物理地址,访问继续执行。这个路径太快,也最容易让人误以为“虚拟内存就是硬件翻译”。
页表能找到有效映射但 TLB 没命中时,硬件或架构相关代码会完成 page table walk,并把结果放进 TLB。这里仍然未必进入内核的 VM 热路径。
真正有意思的是 page fault。缺页异常不一定是错误。匿名页的第一次访问、COW 写入、文件映射按需读入、swapin,都可能以缺页异常开场。只有地址不属于合法 VMA、权限不满足且无法修复、内核无法分配所需资源时,缺页才会变成 SIGSEGV 或 OOM。
这条线会拆成多篇文章,因为它是 Linux VM 的心脏。后面会分别看匿名缺页、文件缺页、COW、swapin 和 huge page fault。
模式提炼:缺页异常是延迟计算
1 | |
page fault 不是单纯的“找不到页”。它更像解释器遇到一个尚未求值的表达式:先查上下文,再根据规则把它规约成下一步状态。
| 缺页原因 | VMA 是否合法 | 内核动作 |
|---|---|---|
| 匿名页第一次访问 | 合法 | 分配并清零页面 |
| COW 写入 | 合法 | 复制页面,改写页表权限 |
| 文件页未在内存 | 合法 | 通过 page cache 读取或复用页面 |
| 页面在 swap | 合法 | swapin 后恢复映射 |
| 越界访问 | 不合法 | 发送 SIGSEGV |
读源码时可以带着这个问题前进:当前缺的到底是翻译、物理页、权限,还是合法地址区间本身。
第三条线:内存压力下怎样做近似决策
第三条线是策略。
经典教材喜欢讲 OPT、FIFO、LRU、Clock。真实内核不能知道未来访问序列,也不能为每次访问维护昂贵的全局顺序。Linux VM 做的是工程近似:用访问位、活跃/非活跃链表、workingset 估计、refault 信息、memcg 边界、NUMA 策略和后台回收线程,把“不知道未来”转成一组可执行的局部规则。
1 | |
这里要避免一个常见误解:页回收不是“内存满了才突然开始”。内核一直在维护水位线、统计页状态、调整 LRU、把脏页写回、让 kswapd 在后台工作。直接回收只是压力已经落到分配路径上的表现。
这条线最后会走到 OOM killer。OOM 不是 Linux VM 的普通目标,而是多轮回收、压缩、写回、swap 或 memcg 限制之后仍然无法满足分配时的兜底动作。理解 OOM,必须先理解前面的所有失败尝试。
这套系列的文章安排
整个系列先按 18 篇设计。每篇只解决一个核心问题,并配一个能在普通 Linux 机器上运行的小实验。
| 篇目 | 层次 | 标题 | 核心问题 | 实验入口 |
|---|---|---|---|---|
| 0 | 导读 | 重读 Linux VM | Linux VM 到底管什么 | 画出地址到页面的路径 |
| 1 | 表示层 | 地址空间不是数组:mm_struct 和 vm_area_struct |
mm_struct 和 VMA 怎样描述进程内存 |
/proc/<pid>/maps |
| 2 | 表示层 | 页表:CPU 能读懂的翻译结构 | 虚拟地址如何落到 PFN | /proc/<pid>/pagemap |
| 3 | 执行层 | 缺页异常 | 一次 fault 穿过哪些路径 | minor/major fault 计数 |
| 4 | 执行层 | 匿名页和 COW | fork 为什么便宜,写入为什么变贵 |
fork 后写大数组 |
| 5 | 执行层 | 文件映射和 page cache | 文件 I/O 为什么属于内存管理 | mmap 文件和 read 对比 |
| 6 | 对象层 | Folio | 为什么 struct page 不再够用 |
读 folio API 和 page cache 路径 |
| 7 | 对象层 | 反向映射 | 从物理页怎样找回映射它的进程 | 共享映射和回收场景 |
| 8 | 策略层 | LRU | 内核如何近似“最近使用” | /proc/vmstat |
| 9 | 策略层 | 页回收 | kswapd 和 direct reclaim 分别做什么 |
内存压测 |
| 10 | 策略层 | Swap | 换出页怎样回来 | swapin/swapout 观察 |
| 11 | 硬件层 | NUMA | 内存为什么有远近 | numactl 延迟实验 |
| 12 | 硬件层 | Huge Page | 大页解决 TLB 和页表开销 | THP/HugeTLB 对比 |
| 13 | 分配层 | Buddy system | 物理页如何按阶分配 | /proc/buddyinfo |
| 14 | 分配层 | SLUB | 小对象为何不能直接按页分配 | /proc/slabinfo |
| 15 | 隔离层 | cgroup memory | 容器内存限制如何改变回收边界 | memory cgroup |
| 16 | 失败层 | OOM Killer | 内核什么时候决定杀进程 | 人工触发 OOM |
| 17 | 收束 | 回到工程 | Linux VM 如何改变性能诊断 | VM 故障诊断表 |
这个顺序有意避开“先讲所有硬件细节”的路线。硬件当然重要,但过早进入页表位、TLB shootdown 和 cache coherence,会把主线打散。先理解对象和状态,再回头看硬件,源码会更容易落位。
最小实验:从 /proc/self/maps 开始
下一篇会从这个最小实验开始:
1 | |
这个程序会建立一段匿名映射,然后暂停。运行时可以在另一个终端观察:
1 | |
第一阶段只能看到 VMA 已经出现。第二阶段写入第一个字节后,匿名页才真正被分配。这个差异就是本系列的入口:地址空间里已经有一段合法区间,不代表物理页已经存在。
后面每篇都会保留这种实验形状:一个小程序,一个 /proc 或 trace 入口,一个源码路径。读 Linux VM 不能只靠源码,也不能只靠命令输出。源码说明规则,实验说明规则是否在当前机器上发生。
系统研究生应该带走什么
系统研究生读 Linux VM,重点不是把每个函数都背下来。内核版本会变,数据结构会变,folio、maple tree、multi-gen LRU 这类变化会继续发生。真正稳定的是几组问题。
第一,任何用户态地址都要问:它落在哪个 VMA?权限是什么?是否有文件 backing?页表是否已经建立?物理页在哪里?
第二,任何 page fault 都要问:这是合法的延迟兑现,还是非法访问?如果合法,兑现路径是匿名页、COW、文件页、swap,还是 huge page?
第三,任何内存压力问题都要问:压力发生在全局还是 memcg?回收目标是匿名页还是文件页?是否受 dirty/writeback、pinned page、unevictable page、NUMA policy 或 huge page 碎片影响?
第四,任何性能结论都要问:证据来自哪里?/proc/vmstat、/proc/meminfo、perf、tracepoint、BPF、page owner、slabinfo、buddyinfo 看到的是不同层次。指标不分层,结论通常会飘。
模式提炼:先定位层次,再解释现象
1 | |
Linux VM 的诊断顺序可以固定下来:先判断现象属于哪一层,再找对应对象,接着看状态转移,最后用指标验证。
| 现象 | 优先层次 | 关键对象 | 证据入口 |
|---|---|---|---|
| 进程 RSS 异常增长 | 地址空间 / 页面状态 | VMA、匿名页、page cache | smaps、pmap |
| 延迟突然升高 | 缺页 / 回收 | page fault、direct reclaim | perf、vmstat |
| 容器内存被杀 | 隔离边界 | memcg、OOM | cgroup event、kernel log |
| 文件 I/O 很快但内存下降 | page cache | file-backed folio | meminfo、vmtouch |
| 大页效果不稳定 | 硬件和分配 | THP、buddy、碎片 | thp sysfs、buddyinfo |
这张表不是调优手册,只是读源码的索引。复杂故障通常跨层,但入口必须先选对。
读源码的锚点
这套系列会优先围绕几个稳定锚点展开:
| 主题 | 典型源码位置 |
|---|---|
| 地址空间和 VMA | include/linux/mm_types.h、mm/mmap.c |
| 缺页处理 | mm/memory.c、架构相关 fault 文件 |
| 匿名页和 COW | mm/memory.c、mm/rmap.c |
| page cache | mm/filemap.c |
| 页分配 | mm/page_alloc.c |
| slab/slub | mm/slub.c |
| 页回收 | mm/vmscan.c、mm/workingset.c |
| swap | mm/swapfile.c、mm/page_io.c |
| memcg | mm/memcontrol.c |
| OOM | mm/oom_kill.c |
源码阅读会尽量贴着函数路径走,但不会把函数调用树当文章结构。函数调用树太细,容易把核心机制淹没。每篇只抓一个转移:比如“缺页怎样生成匿名页”,“COW 怎样从共享只读变成私有可写”,“文件页怎样从 page cache 进入页表”。
常见误解
第一个误解是把虚拟内存等同于 swap。swap 只是虚拟内存体系在内存压力下的一种 backing store。没有 swap,Linux 仍然有虚拟地址、页表、VMA、缺页异常、COW、page cache 和页回收。
第二个误解是把 page cache 当成文件系统自己的缓存。page cache 属于内存管理和文件系统的交界处。文件读写、mmap、回收、写回、folio,都绕不开它。
第三个误解是把 page fault 当成错误。大量 page fault 是正常机制,尤其是 lazy allocation 和 COW。真正要区分的是 minor fault、major fault、权限错误、非法地址和内存压力下的分配失败。
第四个误解是把 LRU 当成教材里的精确算法。Linux VM 的页回收是近似系统,背后是访问证据、链表状态、refault 信息、memcg 边界和脏页写回共同作用。
后续阅读方式
读这套系列时,最好保持三件工具在手边。
第一是内核文档。官方文档不会覆盖所有细节,但能给出术语边界,尤其是 page table、process address、page cache、physical memory、pagemap 这些主题。
第二是源码搜索。rg "handle_mm_fault"、rg "do_anonymous_page"、rg "do_wp_page"、rg "filemap_fault" 这类搜索比顺着目录逐个文件读更有效。
第三是可观测性工具。/proc 提供静态和累计视图,perf 提供采样,tracepoint 和 BPF 提供事件流。只读源码不看事件,容易把很少发生的路径误认为主路径。
练习
第一,运行上面的 mmap 程序,在第一次暂停和第二次暂停时分别记录 /proc/<pid>/maps 与 /proc/<pid>/smaps,观察 VMA 和 RSS 的差异。
第二,写一个程序 malloc(1GB) 但不访问,再逐页写入,观察 minor fault 和 RSS 如何变化。
第三,用 fork 创建子进程,父子进程分别读同一块内存,再让子进程写入,观察 COW 对 RSS 和 page fault 的影响。
第四,找一台启用 THP 的机器,读取 /sys/kernel/mm/transparent_hugepage/enabled,再用大数组访问实验观察匿名大页是否出现。
第五,阅读 Documentation/mm/page_tables.rst 和 Documentation/mm/process_addrs.rst,把“VMA 合法”和“PTE present”这两个判断分开画成流程图。
系列导航
- 本文:重读 Linux VM:给系统研究生的虚拟内存导读
- 下一篇:地址空间不是数组:
mm_struct和vm_area_struct
参考资料
- Memory Management Documentation - The Linux Kernel documentation
- Page Tables - The Linux Kernel documentation
- Process Addresses - The Linux Kernel documentation
- Examining Process Page Tables - The Linux Kernel documentation
- Physical Memory - The Linux Kernel documentation
- Page Cache - The Linux Kernel documentation


