核心比喻 把虚拟地址空间想象成一栋超高层大楼的房间编号系统。 每个进程都有自己的一份房间号码本页表同样的房间号在不同进程的号码本里指向不同的实际物理房间。内核就像大楼的物业 住在每栋楼的顶层所有楼共用同一个物业办公室。 --- 地址空间布局 x86_64 进程虚拟地址空间简化 高地址 0xFFFFFFFFFFFFFFFF ┌─────────────────────────────┐ │ │ │ 内核空间 │ ← 所有进程共享同一份内核 │(128TB)│ 用户态无法直接访问 │ │ ├─────────────────────────────┤ ← 0xFFFF800000000000 │ │ │ 不可用空洞 │ ← x86_64 地址空间空洞 │ │ ├─────────────────────────────┤ ← 0x00007FFFFFFFFFFF │ 栈(Stack)│ ← 向下增长局部变量、函数调用 │ ↓ │ │ │ │ 内存映射区 │ ← mmap、动态库加载在这里 │ ↓ │ │ │ │ 堆(Heap)│ ← malloc 从这里分配向上增长 │ ↑ │ ├─────────────────────────────┤ │ BSS 段 │ ← 未初始化全局变量 │ 数据段 │ ← 已初始化全局变量 │ 代码段 │ ← 程序指令只读 │ │ 低地址 0x0000000000000000 --- PHP 模拟示例 用 PHP 模拟两个进程各自的虚拟地址空间以及页表映射关系?php //// 模拟物理内存就是一块真实存在的内存数组 // 下标物理页帧号PFN值实际存储的数据 //$physicalMemory[0[物理页0] 进程A的代码,1[物理页1] 进程B的代码,2[物理页2] 进程A的全局变量 x100,3[物理页3] 进程B的全局变量 x999,4[物理页4] 内核代码所有进程共享,5[物理页5] 进程A的堆数据,6[物理页6] 进程B的堆数据,];//// 页表虚拟页号物理页帧号 // 每个进程有自己独立的页表就是 CR3 寄存器指向的那张表 //$pageTableA[// 虚拟页号物理页帧号 0x10000, // 进程A 代码段 -物理页0 0x20002, // 进程A 数据段 -物理页2 0x30005, // 进程A 堆 -物理页5 0xF0004, // 内核空间 -物理页4共享];$pageTableB[// 同样的虚拟地址映射到不同物理页 0x10001, // 进程B 代码段 -物理页1 0x20003, // 进程B 数据段 -物理页3 0x30006, // 进程B 堆 -物理页6 0xF0004, // 内核空间 -物理页4共享和A一样];//// MMU 地址翻译虚拟地址 -物理地址 // 真实硬件由 CPU 的 MMU 完成这里用函数模拟 //functiontranslateAddress(int$virtualAddr, array$pageTable, array$physicalMemory): string{// 取虚拟页号简化直接用地址作为页号$vpn$virtualAddr;if(!isset($pageTable[$vpn])){// 页表里没有 -触发缺页异常Page Faultreturn缺页异常虚拟地址 0x.dechex($virtualAddr). 未映射内核介入处理;}$pfn$pageTable[$vpn];// 物理页帧号return$physicalMemory[$pfn];}//// 模拟两个进程访问相同虚拟地址//echo 进程 A 访问虚拟地址 0x1000代码段\n;echotranslateAddress(0x1000,$pageTableA,$physicalMemory).\n\n;echo 进程 B 访问虚拟地址 0x1000代码段\n;echotranslateAddress(0x1000,$pageTableB,$physicalMemory).\n\n;echo 关键相同虚拟地址不同物理内存\n;echo进程A 0x1000 - 物理页 .$pageTableA[0x1000].\n;echo进程B 0x1000 - 物理页 .$pageTableB[0x1000].\n\n;echo 两个进程访问内核空间共享\n;echo进程A 内核: .translateAddress(0xF000,$pageTableA,$physicalMemory).\n;echo进程B 内核: .translateAddress(0xF000,$pageTableB,$physicalMemory).\n\n;echo 访问未映射地址触发缺页\n;echotranslateAddress(0x9999,$pageTableA,$physicalMemory).\n;输出结果进程 A 访问虚拟地址 0x1000代码段[物理页0]进程A的代码进程 B 访问虚拟地址 0x1000代码段[物理页1]进程B的代码关键相同虚拟地址不同物理内存进程A 0x1000 -物理页0进程B 0x1000 -物理页1两个进程访问内核空间共享进程A 内核:[物理页4]内核代码所有进程共享 进程B 内核:[物理页4]内核代码所有进程共享访问未映射地址触发缺页缺页异常虚拟地址 0x9999 未映射内核介入处理 --- 进程切换时发生了什么?php //// 模拟 CPU 的 CR3 寄存器存放当前进程页表的物理地址 // 进程切换换一张页表修改 CR3 //class CPU{// CR3 寄存器指向当前进程的页表 private array$cr3[];private string$currentProcess;publicfunctioncontextSwitch(string$processName, array$pageTable): void{echo 上下文切换{$this-currentProcess} - {$processName}\n;echo CR3 寄存器更新为 {$processName} 的页表\n;echo TLB 全部刷新旧的地址翻译缓存失效\n\n;$this-cr3$pageTable;$this-currentProcess$processName;}publicfunctionaccessMemory(int$virtualAddr, array$physicalMemory): string{if(!isset($this-cr3[$virtualAddr])){return缺页异常;}$pfn$this-cr3[$virtualAddr];return[{$this-currentProcess}] 读到: .$physicalMemory[$pfn];}}$physicalMemory[0进程A的数据,1进程B的数据,];$pageTableA[0x20000];$pageTableB[0x20001];$cpunew CPU();// 调度器让进程A运行$cpu-contextSwitch(进程A,$pageTableA);echo$cpu-accessMemory(0x2000,$physicalMemory).\n\n;// 调度器切换到进程B$cpu-contextSwitch(进程B,$pageTableB);echo$cpu-accessMemory(0x2000,$physicalMemory).\n;输出上下文切换 -进程A CR3 寄存器更新为 进程A 的页表 TLB 全部刷新旧的地址翻译缓存失效[进程A]读到: 进程A的数据上下文切换进程A -进程B CR3 寄存器更新为 进程B 的页表 TLB 全部刷新旧的地址翻译缓存失效[进程B]读到: 进程B的数据 --- 一句话总结 ┌──────────────┬──────────────────────────────────────────────┐ │ 概念 │ 大白话 │ ├──────────────┼──────────────────────────────────────────────┤ │ 虚拟地址 │ 进程以为自己用的地址是假的 │ ├──────────────┼──────────────────────────────────────────────┤ │ 物理地址 │ 内存条上真实的位置 │ ├──────────────┼──────────────────────────────────────────────┤ │ 页表 │ 虚拟地址到物理地址的翻译字典 │ ├──────────────┼──────────────────────────────────────────────┤ │ CR3寄存器 │ CPU当前用哪本字典 │ ├──────────────┼──────────────────────────────────────────────┤ │ 进程切换 │ 换一本字典同一个地址翻出不同结果 │ ├──────────────┼──────────────────────────────────────────────┤ │ 缺页异常 │ 字典里查不到内核来补页 │ ├──────────────┼──────────────────────────────────────────────┤ │ 内核空间共享 │ 所有进程的字典里内核那页指向同一个物理位置 │