一、物理内存
主板上真实的内存条,硬件实体,使用物理地址寻址。CPU 最终访问内存芯片时,发送的一定是物理地址。物理内存是系统全局资源,所有进程共享。 物理内存可寻址的最大上限,由CPU 芯片内部物理地址总线的硬件位宽决定。同一种架构下,不同型号 CPU 物理地址位宽可以不一样。
二、虚拟内存与虚拟地址空间
虚拟内存不是硬件,是操作系统依托 CPU 硬件能力实现的内存抽象。 每个进程拥有独立完整的虚拟地址空间。进程代码中操作的地址全部是虚拟地址。
关键:虚拟地址只是地址编号,本身不存储任何数据。 数据真正存放位置:物理内存;或者磁盘(ELF 可执行文件、swap 交换分区)。 同一个虚拟地址,在不同进程中,可以映射到完全不同的物理页。
虚拟地址空间的最大上限,由CPU 架构规范(指令集架构)确定,操作系统无法突破这个硬件上限。
x86_32:架构规定虚拟地址 32bit,单进程虚拟地址空间上限 \(2^{32}=4\mathrm{GB}\)
x86_64(AMD64):架构统一规定有效虚拟地址 48bit,单进程虚拟地址空间上限 \(2^{48}=256\mathrm{TB}\)
虚拟内存带来的好处:
进程地址空间隔离,防止进程互相越界访问;
物理内存不足时,冷页面可以换入 swap 磁盘;
程序编译链接无需关心物理内存实际位置;
页面可配置 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 查询页表,翻译成物理地址 → 访问物理内存。 两种异常情况:
虚拟页不在物理内存,PTE 有效位为 0 → 缺页异常 Page Fault,陷入内核处理;
地址无映射,或者权限不匹配 → 触发 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)每一段比特作为对应层级页表数组的下标,硬件自动拆分地址比特,地址本身的值不变。
查表完整流程
CR3 寄存器存放顶级页表 PGD 的物理地址;每个进程拥有独立的 CR3。进程切换时修改 CR3,同时刷新 TLB。
CR3 存的是物理地址,MMU 直接访问物理内存读取 PGD,不需要虚拟地址翻译。
使用 PGD 索引,在 PGD 中找到 PUD 的物理地址;
使用 PUD 索引,在 PUD 中找到 PMD 的物理地址;
使用 PMD 索引,在 PMD 中找到 PTE 的物理地址;
使用 PTE 索引,读取 PTE,拿到物理页框号;
物理页框号拼接页内偏移,得到最终物理地址,访问内存。
没有 TLB 命中的情况下,4 级页表需要 4 次访问物理内存读取各级页表,再加一次读取目标数据。 多级页表是典型时间换空间:节省物理内存,但 TLB 未命中时,多次访存查表,降低地址翻译速度。TLB 用来弥补这个性能损耗。
32 位 x86 页表补充
32 位系统:
无 PAE:可以单级页表,Linux 默认采用两级页表;
开启 PAE:硬件强制三级页表,不再支持单级 / 两级。
重要知识点
所有层级的页表 PGD/PUD/PMD/PTE全部存放在物理内存;用户态进程无法直接访问页表。
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,触发缺页异常,进入内核处理。 分三类场景:
请求调页:页面还保存在磁盘(ELF 文件或者 swap)。内核分配物理页,将数据从磁盘加载到内存,更新页表,返回用户态重新执行指令。属于正常情况。
COW 写时复制缺页:fork 产生,页面是共享只读页面,进程执行写操作触发缺页。
非法访问:虚拟地址没有映射,或者权限不匹配,发送 SIGSEGV,终止进程。
八、COW 写时复制 Copy On Write
fork 创建子进程时,不会立刻拷贝全部内存。
fork 后父子进程各自拥有独立虚拟地址空间,两套独立页表;
父子页表项指向同一块物理页,页面标记只读;
只要只做读操作,就持续共享同一份物理内存;
任意进程尝试写页面 → 触发缺页异常;
内核分配一块新的物理页框,把旧物理页的数据拷贝到新物理页,修改写操作进程的页表映射,并改为可写权限。
COW 复制的是物理内存中的页面数据,进程的虚拟地址保持不变,只修改页表映射关系。 COW 只拷贝被修改的单个页面,不会一次性拷贝整个进程内存。
九、swap 交换分区
物理内存资源耗尽时,内核选择长期不访问的冷页面,写入磁盘 swap 分区,释放物理内存;后续访问该页面时,再从 swap 换回内存。 缺点:磁盘速度远低于内存,大量 swap 读写会造成系统严重卡顿。
十、页表项 PTE 标记位
Present 有效位:1 = 页面映射在物理内存;0 = 页面不在内存
R/W 读写位:控制页面是否允许写
X 执行位:控制页面能否执行,用于内存保护
Dirty 脏位:页面数据被修改,和磁盘副本不一致;页面换出时必须写回磁盘
Accessed 访问位:页面被访问,内核用来判断页面冷热,挑选需要换出的页面
十一、分页和分段简单对比
分页:面向硬件,页面固定大小;核心用于虚实地址转换、内存隔离、内存复用,现代 Linux 以分页为主。
分段:面向程序逻辑(代码段、数据段),段长度不固定;现代 Linux 中分段机制已经弱化。
核心要点汇总
虚拟地址是进程视角的编号,不存储数据;真实数据保存在物理内存或者磁盘。MMU 硬件负责虚实地址转换。
虚拟地址最大上限由 CPU 架构规范决定;物理内存最大寻址上限由 CPU 芯片物理地址总线硬件决定。二者相互独立,大小可以差距很大。
分页本质:对同一个地址二进制比特做逻辑切分,拆成页号 + 页内偏移,地址本身不发生变化。多级页表则继续把页号部分拆成多段索引。TLB 和多级页表只负责虚拟页号到物理页框号的查找,页内偏移保持原样不变。
单级页表是一维数组,必须一次性全部分配;多级页表是树形结构,只有用到的分支才创建,节省物理内存;代价是 TLB 未命中时多次访存,TLB 缓存抵消性能损耗。
各级页表都保存在物理内存,CR3 保存顶级页表的物理地址。
基础页面大小由硬件规定,同时支持大页;大页减少页面数量,降低 TLB 压力,但会增大内存内部碎片。
TLB 缓存虚实映射,TLB miss 和缺页异常是两个完全不同的概念。
缺页异常分为正常请求调页、COW 缺页、非法访问三类。
COW 在写的时候才拷贝物理页,虚拟地址不变,fork 依靠这个机制提升创建速度。
swap 是内存不足时将冷页面换至磁盘,会带来性能损耗。