虚拟内存核心精讲:页表、VMA、缺页异常与mmap(单级页表 + 多级页表对照)

**前言:**很多同学学习虚拟内存时容易混淆两套模型:教学用的单级页表简化模型、Linux x86_64真实四级多级页表。尤其是 mmap、VMA、PTE、present位、缺页、段错误之间的关系,很容易越看越乱。本文把两套模型放在一起对照,从基础共识、时序流程、对比表格、树形示例一次性梳理清楚,同时解答高频疑问。

一、基础共识

先分清两个核心角色,一个硬件、一个内核软件,这是所有理解的根基:

PTE(页表项,硬件结构,给MMU使用) 核心字段:present位、物理页号PFN、权限位。

  • present=1:MMU可以直接完成虚拟地址→物理地址翻译;
  • present=0:硬件触发缺页异常。

VMA(vm_area_struct,内核软件结构) 内核用链表管理,描述一段连续的虚拟地址区间:记录起止地址、访问权限、映射类型(匿名映射 / 文件mmap / 代码段/数据段)、后备存储。

✅ 最重要判定规则(单级、多级通用): present=0 不等于非法地址,要看地址是否落在VMA中:

  • 地址落在VMA内:地址合法 → 触发缺页中断,内核负责加载数据、填充PTE;
  • 地址不在任何VMA内:地址非法 → 直接抛出段错误 SIGSEGV。

mmap 是系统调用:核心作用是新增一条VMA;默认懒加载,不会提前分配物理页,不会把PTE的present置1。

二、模型一:单级页表

核心特点

页表是一张固定长度的大数组,虚拟页号直接作为数组下标。进程创建时,整张页表数组一次性全部分配完毕。 虚拟页编号 → 数组下标 → 拿到PTE。 👉 重点:不存在缺少PTE的情况,所有虚拟页都预先拥有对应的PTE位置。

时序流程

进程刚创建时 整张页表数组已经存在,所有PTE就位,初始状态全部 present=0; 内核只预先创建基础VMA:代码段、数据段、栈;其余虚拟地址区间没有VMA,访问会段错误。

调用mmap(文件映射 / 匿名映射) 不会新增、创建PTE(PTE位置本来就已经存在); 仅仅新增一条VMA,标记一段连续虚拟地址合法,登记映射类型、后备存储信息; 该区间对应的PTE依旧保持 present=0。

第一次访问mmap对应的虚拟地址 MMU查询PTE发现 present=0 → 硬件触发缺页中断; 内核拿到虚拟地址,查找VMA确认地址合法,判断映射类型:

  • 匿名映射:分配清零的物理页,后备存储为Swap;
  • 文件映射:读取磁盘文件到Page Cache物理页,后备存储为原文件; 原地修改已经存在的PTE:present=1,填入物理页号。 后续访问:MMU直接完成地址翻译,不再触发缺页。

内存回收、Swap换出 匿名页:内存紧张时,页数据写入Swap分区,PTE置 present=0; 文件共享页:干净页可以直接丢弃;脏页刷回磁盘文件后,PTE置 present=0; 再次访问该地址,重新走一遍缺页流程。

✅ 单级页表一句话总结: PTE位置出厂全部配齐;VMA是虚拟地址使用权许可证。mmap就是申请一块虚拟地址的许可证,PTE本身不动,等到访问触发缺页再填充物理页信息。

❌ **缺点:**地址空间一大,页表数组体积巨大,严重浪费内存,真实操作系统不会采用。

单级页表示例表(VP0~VP7)

虚拟页号 VP PTE(present / 物理页号) 是否落在VMA(合法地址) 访问结果
VP0 present=1,PFN=5 ✅ 是(代码段VMA) 正常翻译
VP1 present=1,PFN=7 ✅ 是(代码段VMA) 正常翻译
VP2 present=0 ✅ 是(mmap新增VMA,文件映射) 缺页中断,读文件进page cache
VP3 present=0 ✅ 是(mmap新增VMA,文件映射) 缺页中断,读文件进page cache
VP4 present=0 ✅ 是(匿名VMA,堆) 缺页中断,分配清零物理页
VP5 present=0 ❌ 否 段错误 SIGSEGV
VP6 present=0 ❌ 否 段错误 SIGSEGV
VP7 present=0 ❌ 否 段错误 SIGSEGV

单级模型关键:所有VP天生就有PTE;mmap只是把VP标记为落在VMA中;present=0只是没有绑定物理页,合法性由VMA说了算。

三、模型二:多级页表

核心特点

页表是一棵树,CR3寄存器指向根页表PML4;各级页表节点、PTE都不是预先分配,按需懒创建,没有固定大数组。 虚拟地址拆分为多段索引:PML4索引 → PDPT索引 → PD索引 → PT索引 → 页内偏移 只有最底层PT页表里面存放PTE;前面几级都是目录项,用来指向下一级页表。 👉 重点:不存在的分支 = 没有下级页表、自然也没有PTE。

时序流程

进程刚创建时 仅仅分配顶级根页表PML4; 绝大多数虚拟地址对应的下级页表(PDPT/PD/PT)、PTE根本不存在; 同样预先创建基础VMA:代码段、数据段、栈。 很多虚拟地址:连PTE都不存在,而不是PTE present=0。

调用mmap 依旧只做一件事:新增一条VMA,标记一段虚拟地址合法,记录映射类型; 不会分配下级页表,不会创建PTE;这条地址对应的页树分支仍然是空的。

第一次访问mmap对应的虚拟地址 MMU沿着多级索引遍历页表树,走到某一级发现下级页表不存在 → 触发缺页中断; 内核处理步骤:

  • ① 查询VMA,确认地址合法,区分缺页类型(文件缺页 / 匿名缺页);
  • ② 按需分配路径上缺失的下级页表节点;
  • ③ 分配物理页(匿名页清零 / 文件读取到page cache);
  • ④ 新建PTE,present=1,填入物理页号,挂载到页表树中。 后续访问:顺着页树查到PTE,直接地址翻译。

内存回收、Swap换出 逻辑和单级模型大体一致:页面被回收后,PTE可以清空,present置0; 极端场景:地址长时间不访问,内核甚至可以释放闲置的下级页表节点,节约内存。

✅ 多级页表一句话总结: 只有页表树根PML4一开始存在;PTE和下级页表节点都是缺页的时候现场新建。VMA依旧是地址许可证,mmap只负责拿到许可证。

✅ **优点:**稀疏结构,只分配用到的分支,极大节省内存,现代操作系统标准方案。

多级页表树形演示

初始状态:进程刚创建,仅根PML4存在

复制代码
PML4(根,CR3指向它)
 ├─ 项0:指向PDPT0 → ❌ PDPT0 不存在(没分配)
 ├─ 项1:指向PDPT1 → ❌ PDPT1 不存在(没分配)
 └─ 项2:指向PDPT2 → ✅ PDPT2 存在(代码段提前建好)
         └─ PDPT2
             ├─ 项0:指向PD0 → ✅ PD0存在
             │     └─ PD0
             │         ├─ 项0:指向PT0 → ✅ PT0存在
             │         │     └─ PT0(底层页表,存放PTE)
             │         │         ├─ PTE0:present=1,有物理页号(代码页)
             │         │         └─ PTE1:present=1,有物理页号(代码页)
             │         └─ 其他PT指针:❌ PT不存在
             └─ 其他PD指针:❌ PD不存在

执行mmap之后:树结构完全不变,仅仅新增VMA

复制代码
PML4(根)
 ├─ 项0:指向PDPT0 → ❌ PDPT0【仍然不存在】 ← mmap不会创建页表节点
 ├─ 项1:指向PDPT1 → ❌ PDPT1 不存在
 └─ 项2:...(代码段分支不变)

第一次访问mmap地址,触发缺页,内核补齐整条分支:

复制代码
PML4(根)
 ├─ 项0:指向PDPT0 → ✅ 分配创建 PDPT0
 │       └─ PDPT0
 │           ├─ 项5:指向PD5 → ✅ 分配创建 PD5
 │           │     └─ PD5
 │           │         ├─ 项2:指向PT2 → ✅ 分配创建 PT2
 │           │         │     └─ PT2
 │           │         │         └─ PTE:✅ 新建PTE,present=1,填入page cache物理页号
 │           │         └─ 其余PT指针:❌ PT不存在
 │           └─ 其余PD指针:❌ PD不存在
 ├─ 项1:指向PDPT1 → ❌ PDPT1 不存在
 └─ 项2:...(代码段分支不变)

四、两套模型核心对比汇总表

项目 单级页表 Linux多级页表
PTE是否预先全部存在 ✅ 全部存在,固定数组 ❌ 按需懒创建,大部分虚拟地址没有PTE
mmap做什么 新增VMA,PTE本来就存在,维持present=0 新增VMA,此时无PTE、无下级页表
present=0含义 PTE存在,但没有绑定物理页 两种:①有PTE但present=0;②连PTE都不存在
缺页时动作 修改已存在PTE,置present=1、填物理页号 先分配缺失的页表节点,新建PTE
非法地址(无VMA)访问 段错误 段错误
内存开销 巨大,不实用 稀疏,节省内存
VMA作用 标记地址合法,记录缺页规则 和单级完全一致

五、高频经典问题答疑

Q1:单级页表每个虚拟页都有PTE,为什么还要区分合法/非法地址?

PTE只是硬件地址翻译记录,不负责管理这块虚拟地址有没有分配给进程,分配权限由内核软件结构VMA管理。 present=0 只代表:虚拟页没有绑定物理内存,无法区分两种场景:

  • 地址已经分配给进程(有VMA):合法 → 缺页中断,内核分配/加载物理页;
  • 地址从未分配给进程(无VMA):非法 → 段错误。

进程创建之初只有少量VMA(代码段、数据段、栈);brk/malloc、mmap 的本质,就是动态新增VMA,把原本非法的虚拟地址区间变成合法地址。

类比:虚拟地址空间是一本笔记本,单级页表就是本子上每一行都预先印好格子(PTE);VMA是内核用笔圈出来,告诉进程:只有圈出来的行你才可以使用。没有圈的行,格子虽然存在,但属于未分配区域,访问直接报错。

一句话:PTE存在 ≠ 地址合法;地址合法与否唯一判断依据:是否存在VMA。

Q2:为什么有Swap还会出现OOM?

Swap仅仅是内存的后备存储,不是无限兜底:Swap空间耗尽、内核关闭Swap、进程命中内存资源上限,依旧会触发OOM。页面换入换出本身存在性能开销,不能无限回收。

Q3:mmap是不是直接映射磁盘?

两套模型规则一致:CPU只能访问物理内存。文件mmap的后备存储是磁盘;只有缺页的时候才会把文件数据读到Page Cache物理页,用户虚拟地址最终映射到这块物理页,不是直接访问磁盘。

Q4:用户空间和内核空间页表关系?

每个进程CR3指向独立的页表;页表分为用户区域(进程间互相独立)、内核区域(所有进程共享同一份映射);依靠PTE中的U/S权限位,限制用户态代码无法访问内核映射。

Q5:MAP_POPULATE是什么?

属于mmap的特例,不属于默认懒加载行为:调用mmap阶段主动提前加载页面、填充PTE,预先规避后续访问的缺页中断。

六、总结

  • VMA是内核软件层的地址许可证,这套规则在单级、多级模型下完全统一;有没有VMA,区分合法缺页和段错误;
  • PTE是硬件MMU翻译用的记录,两套模型最大差异在于PTE和页表节点的创建时机:

单级:一次性全部建好PTE;缺页只是修改已有PTE;

Linux多级:树根常驻,其余节点、PTE缺页时现场创建;

  • mmap默认只新增VMA,懒加载,不会提前分配物理页、不会提前创建PTE;
  • present=0 含义在两套模型下不一样,不要混淆!
相关推荐
OpenAnolis小助手2 天前
RISC-V 基础软件的工程化与性能优化实践
性能优化·操作系统·龙蜥社区·risc-v
青梅味猪大肠3 天前
【操作系统-60】临近适应算法
java·linux·开发语言·嵌入式硬件·算法·操作系统
PMAOS6 天前
4G 功能机操作系统选型指南:KaiOS、Mocor、MAUI、PMAOS 的能力边界与验证方法
嵌入式硬件·ai·操作系统
罗四强6 天前
第10篇:打包上架:签名、打包 HAP、发布到华为应用市场
分布式·操作系统·harmonyos·arkts·鸿蒙系统
Liaiyang668 天前
空圈容错视角-HarmonyOS 7 架构体检:从被动容错到主动免疫的真伪验证与生态反推
操作系统·harmonyos·架构设计·ai-agent·端云协同·鸿蒙7·空圈容错
OpenAnolis小助手8 天前
龙蜥社区亮相 GOSIM AI Vision Forum 共话“Agentic Native”开源新
人工智能·ai·开源·操作系统·龙蜥社区·agent
阿里云基础软件8 天前
Agent 任务提速 10.71%,内存占用降低 21.60%|Alibaba Cloud Linux 4 重大发布
java·linux·人工智能·操作系统·agent
OpenAnolis小助手9 天前
多租户公有云共享 CXL 内存池场景下的内存卸载研究
人工智能·操作系统·龙蜥社区·risc-v·agentic os
OpenAnolis小助手12 天前
PHP 两个严重漏洞曝光,龙蜥社区 24 小时完成 3 版本修复
开发语言·操作系统·php·龙蜥社区