蔚淦丞
发布于 2026-09-11 / 15 阅读
0
0

虚拟内存、物理内存、MMU、分页、多级页表

一、物理内存

主板上真实的内存条,硬件实体,使用物理地址寻址。CPU 最终访问内存芯片时,发送的一定是物理地址。物理内存是系统全局资源,所有进程共享。 物理内存可寻址的最大上限,由CPU 芯片内部物理地址总线的硬件位宽决定。同一种架构下,不同型号 CPU 物理地址位宽可以不一样。

二、虚拟内存与虚拟地址空间

虚拟内存不是硬件,是操作系统依托 CPU 硬件能力实现的内存抽象。 每个进程拥有独立完整的虚拟地址空间。进程代码中操作的地址全部是虚拟地址

关键:虚拟地址只是地址编号,本身不存储任何数据。 数据真正存放位置:物理内存;或者磁盘(ELF 可执行文件、swap 交换分区)。 同一个虚拟地址,在不同进程中,可以映射到完全不同的物理页。

虚拟地址空间的最大上限,由CPU 架构规范(指令集架构)确定,操作系统无法突破这个硬件上限。

  • x86_32:架构规定虚拟地址 32bit,单进程虚拟地址空间上限 \(2^{32}=4\mathrm{GB}\)

  • x86_64(AMD64):架构统一规定有效虚拟地址 48bit,单进程虚拟地址空间上限 \(2^{48}=256\mathrm{TB}\)

虚拟内存带来的好处:

  1. 进程地址空间隔离,防止进程互相越界访问;

  2. 物理内存不足时,冷页面可以换入 swap 磁盘;

  3. 程序编译链接无需关心物理内存实际位置;

  4. 页面可配置 r/w/x 权限,实现内存保护,非法访问触发段错误。

PAE 物理地址扩展(仅 32 位 x86)

PAE 是 32 位 x86 的硬件补丁,只扩展物理地址位宽,不改变 32 位虚拟地址

  • 开启 PAE 后,物理地址变为 36bit,整机最多寻址 \(2^{36}=64\mathrm{GB}\) 物理内存。

  • 单个进程虚拟地址仍然是 32bit,上限 4GB。多个进程可以共用超过 4GB 物理内存,但单个进程同一时刻最多只能映射 4GB 虚拟地址。

x86_64 架构原生支持大物理地址,不需要 PAE。

Linux 地址空间划分

32 位 Linux:4GB 虚拟地址切分为用户态 3GB(进程独立),内核态 1GB(所有进程共享)。 x86_64 Linux:256TB 虚拟地址分为两块,用户态 128TB(每个进程独立),内核态 128TB(全部进程共享内核映射)。

重要区分:虚拟地址上限与物理地址上限是两套独立硬件参数,二者理论最大值不需要相等。 例:x86_64 高端 CPU,虚拟地址 48bit (256TB),物理地址最高支持 52bit (4PB),物理寻址上限远大于单进程虚拟地址上限。

三、MMU(内存管理单元)

集成在 CPU 内部的硬件单元。 核心职责:完成虚拟地址 → 物理地址的地址翻译。 执行流程:CPU 发出虚拟地址 → MMU 查询页表,翻译成物理地址 → 访问物理内存。 两种异常情况:

  1. 虚拟页不在物理内存,PTE 有效位为 0 → 缺页异常 Page Fault,陷入内核处理;

  2. 地址无映射,或者权限不匹配 → 触发 SIGSEGV 段错误,杀死进程。

无 MMU 的嵌入式系统没有虚拟内存,程序直接使用物理地址。

四、分页基础

分页是虚拟内存的实现方式。把虚拟地址空间、物理内存切割成固定大小的块,称为页。

  • 虚拟侧:虚拟页

  • 物理侧:物理页框(page frame)

x86 硬件原生基础页面大小为 4KB,由 CPU 硬件架构规定,系统运行时不能动态修改;CPU 同时支持可选大页(2M、1G),大页与 4K 基础页共存,按需选用。

核心理解:地址本身二进制数值不变。分页只是硬件把地址比特位逻辑切分:高位作为页编号,低位作为页内偏移。 4KB 页面,低 12bit 固定为页内偏移;剩余高位是页号。不是修改地址,只是改变解读方式。 TLB、多级页表的作用,只负责完成「虚拟页号 → 物理页框号」的映射查找;页内偏移直接复用,虚拟地址与物理地址的页内偏移完全一致,不需要翻译。

虚拟地址分为两段:虚拟页号 + 页内偏移 物理地址分为两段:物理页框号 + 页内偏移

重点:页内偏移在虚实地址中完全相同,不需要转换

物理地址计算公式 物理地址 = (物理页框号 << 页内偏移位数) | 页内偏移

  • << 左移:将物理页框号向高位移动,右侧空出对应 bit,用来存放页内偏移。

  • | 按位或:完成比特拼接。左移之后低位全部为 0,按位或直接把页内偏移填充到低位。

运算符区分:| 按位或;& 按位与。按位与一般用来做掩码提取比特段,例如 addr & 0xFFF 取出低 12 位页内偏移。

页表:内核维护在物理内存的数据结构。每一条页表项 PTE 记录:虚拟页映射到哪个物理页框,附带属性标记:present 有效位、读写权限、执行权限、脏位、访问位。

页面大小的取舍

页面越大:同等内存对应的页面数量越少,页表占用内存更小;TLB 单条条目能覆盖更大内存,降低 TLB miss 概率。 缺点:内存分配粒度变粗,内部碎片浪费更多内存。

五、多级页表

核心原理

单纯多级分页结构本身,不能减少页表项总数量。如果进程用满全部虚拟地址空间,多级页表条目总数反而更多(多出中间目录层)。 省内存的关键:多级页表树形结构 + 未使用区域不分配下级页表

  • 顶级 PGD 根表必须分配、常驻物理内存,记录下一级页表的物理地址;

  • 下层 PUD、PMD、PTE,只有访问对应虚拟地址触发缺页时,内核才分配物理内存并创建;从未访问的区域,对应的下级页表完全不创建,不占用内存。

对比单级页表:单级页表是一维数组,数组下标直接对应虚拟页号,必须一次性完整分配,无法只创建部分条目,哪怕绝大多数页面不用,全部 PTE 也要预先占用内存。

多级页表同样是硬件对虚拟地址比特做固定切分,不是软件动态拆分。 x86_64,4KB 页,48 位有效虚拟地址切分: PGD索引(9bit) | PUD索引(9bit) | PMD索引(9bit) | PTE索引(9bit) | 页内偏移(12bit) 每一段比特作为对应层级页表数组的下标,硬件自动拆分地址比特,地址本身的值不变。

查表完整流程

  1. CR3 寄存器存放顶级页表 PGD 的物理地址;每个进程拥有独立的 CR3。进程切换时修改 CR3,同时刷新 TLB。

CR3 存的是物理地址,MMU 直接访问物理内存读取 PGD,不需要虚拟地址翻译。

  1. 使用 PGD 索引,在 PGD 中找到 PUD 的物理地址;

  2. 使用 PUD 索引,在 PUD 中找到 PMD 的物理地址;

  3. 使用 PMD 索引,在 PMD 中找到 PTE 的物理地址;

  4. 使用 PTE 索引,读取 PTE,拿到物理页框号;

  5. 物理页框号拼接页内偏移,得到最终物理地址,访问内存。

没有 TLB 命中的情况下,4 级页表需要 4 次访问物理内存读取各级页表,再加一次读取目标数据。 多级页表是典型时间换空间:节省物理内存,但 TLB 未命中时,多次访存查表,降低地址翻译速度。TLB 用来弥补这个性能损耗。

32 位 x86 页表补充

32 位系统:

  1. 无 PAE:可以单级页表,Linux 默认采用两级页表;

  2. 开启 PAE:硬件强制三级页表,不再支持单级 / 两级。

重要知识点

  1. 所有层级的页表 PGD/PUD/PMD/PTE全部存放在物理内存;用户态进程无法直接访问页表。

  2. MMU 硬件自动完成多级查表,不是内核软件循环遍历。

六、TLB 快表(Translation Lookaside Buffer)

TLB 是 CPU 内部高速缓存,缓存虚拟页号→物理页框号映射条目。一条 TLB 条目对应一个页面的映射。

  • TLB 命中:直接拿到映射,无需访问内存页表,速度很快;

  • TLB miss:访问内存查询多级页表,同时把新映射条目存入 TLB。

页面尺寸越小,同等内存对应的页面数量越多,需要的 TLB 条目越多,对 TLB 容量要求越高;大页能大幅减少页面总数,降低 TLB 压力,减少 TLB miss。

进程切换时,必须刷新 TLB,避免复用上一个进程旧的虚实映射。

区分:TLB miss 只是映射缓存没命中;Page Fault 缺页异常,代表页面不在物理内存。二者不是一回事。

七、缺页异常 Page Fault

MMU 查询 PTE,发现 present 有效位为 0,触发缺页异常,进入内核处理。 分三类场景:

  1. 请求调页:页面还保存在磁盘(ELF 文件或者 swap)。内核分配物理页,将数据从磁盘加载到内存,更新页表,返回用户态重新执行指令。属于正常情况。

  2. COW 写时复制缺页:fork 产生,页面是共享只读页面,进程执行写操作触发缺页。

  3. 非法访问:虚拟地址没有映射,或者权限不匹配,发送 SIGSEGV,终止进程。

八、COW 写时复制 Copy On Write

fork 创建子进程时,不会立刻拷贝全部内存

  1. fork 后父子进程各自拥有独立虚拟地址空间,两套独立页表;

  2. 父子页表项指向同一块物理页,页面标记只读;

  3. 只要只做读操作,就持续共享同一份物理内存;

  4. 任意进程尝试写页面 → 触发缺页异常;

  5. 内核分配一块新的物理页框,把旧物理页的数据拷贝到新物理页,修改写操作进程的页表映射,并改为可写权限。

COW 复制的是物理内存中的页面数据,进程的虚拟地址保持不变,只修改页表映射关系。 COW 只拷贝被修改的单个页面,不会一次性拷贝整个进程内存。

九、swap 交换分区

物理内存资源耗尽时,内核选择长期不访问的冷页面,写入磁盘 swap 分区,释放物理内存;后续访问该页面时,再从 swap 换回内存。 缺点:磁盘速度远低于内存,大量 swap 读写会造成系统严重卡顿。

十、页表项 PTE 标记位

  • Present 有效位:1 = 页面映射在物理内存;0 = 页面不在内存

  • R/W 读写位:控制页面是否允许写

  • X 执行位:控制页面能否执行,用于内存保护

  • Dirty 脏位:页面数据被修改,和磁盘副本不一致;页面换出时必须写回磁盘

  • Accessed 访问位:页面被访问,内核用来判断页面冷热,挑选需要换出的页面

十一、分页和分段简单对比

  • 分页:面向硬件,页面固定大小;核心用于虚实地址转换、内存隔离、内存复用,现代 Linux 以分页为主。

  • 分段:面向程序逻辑(代码段、数据段),段长度不固定;现代 Linux 中分段机制已经弱化。

核心要点汇总

  1. 虚拟地址是进程视角的编号,不存储数据;真实数据保存在物理内存或者磁盘。MMU 硬件负责虚实地址转换。

  2. 虚拟地址最大上限由 CPU 架构规范决定;物理内存最大寻址上限由 CPU 芯片物理地址总线硬件决定。二者相互独立,大小可以差距很大。

  3. 分页本质:对同一个地址二进制比特做逻辑切分,拆成页号 + 页内偏移,地址本身不发生变化。多级页表则继续把页号部分拆成多段索引。TLB 和多级页表只负责虚拟页号到物理页框号的查找,页内偏移保持原样不变。

  4. 单级页表是一维数组,必须一次性全部分配;多级页表是树形结构,只有用到的分支才创建,节省物理内存;代价是 TLB 未命中时多次访存,TLB 缓存抵消性能损耗。

  5. 各级页表都保存在物理内存,CR3 保存顶级页表的物理地址。

  6. 基础页面大小由硬件规定,同时支持大页;大页减少页面数量,降低 TLB 压力,但会增大内存内部碎片。

  7. TLB 缓存虚实映射,TLB miss 和缺页异常是两个完全不同的概念。

  8. 缺页异常分为正常请求调页、COW 缺页、非法访问三类。

  9. COW 在写的时候才拷贝物理页,虚拟地址不变,fork 依靠这个机制提升创建速度。

  10. swap 是内存不足时将冷页面换至磁盘,会带来性能损耗。


评论