Linux虚拟内存和分页是进程隔离的底层前提;CPU访问的是虚拟地址,MMU通过进程私有页表将其映射到不同物理页,缺页异常分软/硬/无效三类,多级页表兼顾稀疏空间与性能。

Linux 的虚拟内存和分页不是“可选功能”,而是进程能独立运行、内存不互相踩踏的底层前提——关掉分页,连内核都起不来。
为什么每个进程看到的 0x7fff0000 都是自己的?
因为 CPU 访问的从来不是物理地址,而是线性地址(也叫虚拟地址)。分页机制在背后用 MMU 硬件单元实时做翻译:把进程 A 的 0x7fff0000 映射到物理内存的 0x1a2b3c00,同时把进程 B 的同个虚拟地址映射到 0x4d5e6f00。这个映射关系就存在每个进程私有的页表里。
常见错误现象:
- 误以为
malloc返回的指针是物理地址,试图用它直接操作硬件(实际会触发缺页异常或段错误) - 在内核模块里直接访问用户空间虚拟地址(没做
copy_from_user或access_ok检查),导致 Oops
关键点:
- 用户态代码永远只能看到虚拟地址;
/proc/pid/maps显示的就是该进程当前有效的虚拟地址区间 - 同一虚拟地址,在不同进程里映射的物理页可以完全不同,这是进程隔离的基石
- 内核态有自己独立的页表(通常共享部分 PGD),但用户态页表切换由
CR3寄存器控制,每次进程切换时刷新
缺页异常(Page Fault)不是 bug,是正常流程
当进程首次访问某块虚拟内存(比如刚 mmap 的文件区域,或第一次写 malloc 出来的内存),对应页表项还是空的,CPU 就会触发 #PF 异常,交由内核的 do_page_fault() 处理。
它分三种典型情况:
- 软缺页(Soft Page Fault):页已在内存中(如共享库代码页),只是当前进程页表还没建立映射,内核只需填好 PTE,开销极小
- 硬缺页(Hard Page Fault):页在磁盘交换区或文件中,需从磁盘读入,耗时毫秒级,是性能瓶颈常见来源
-
无效缺页(Invalid Fault):访问未分配/无权限地址(如 NULL 指针解引用、只读页写入),内核发
SEGV信号终止进程
实操建议:
- 用
perf stat -e page-faults,minor-faults,major-faults ./your_program区分软硬缺页比例 - 频繁
major-faults往往说明物理内存不足,或程序局部性差(随机访问大文件) - 避免在 tight loop 中反复
mmap/munmap,这会不断触发软缺页并增加 TLB 压力
多级页表不是为了炫技,是为省内存和适配稀疏地址空间
32 位系统若用单级页表,4GB 地址空间 / 4KB 每页 = 1048576 个页表项,每个 4 字节 → 要占满 4MB 连续物理内存。而现实中一个普通进程只用几 MB 虚拟内存,且高度分散(代码段、堆、栈、mmap 区各占一块)。多级页表(x86-64 是 4 级:PGD → P4D → PMD → PTE)只分配实际用到的路径节点。
容易踩的坑:
- 误以为“级数越多越慢”——现代 CPU 的
TLB(Translation Lookaside Buffer)会缓存多级转换结果,只要命中 TLB,速度和单级无异 - 在嵌入式场景强行启用 4K 页,却忽略 ARM64 支持 64KB 大页,对大内存服务(如数据库)可显著减少 TLB miss 和页表遍历开销
- 调试时看
/proc/pid/status的MMUPageSize并不反映实际页大小,要查/sys/kernel/mm/transparent_hugepage或用cat /proc/pid/smaps | grep -i "mmu.*page"
如何验证某个虚拟地址是否已映射到物理内存?
不能靠 cat /proc/pid/pagemap 直接读,因为该接口需要 root 权限,且返回的是 64 位帧号(PFN),还需结合 /proc/kpageflags 判断是否 present、swap、soft-dirty 等状态。
更实用的做法:
- 用
mincore()系统调用检查某段虚拟内存是否驻留在 RAM 中(注意:只对匿名映射或 mmap 文件有效,且需先mlock或触发过访问) - 在调试中,用
pstack+gdb attach查看info proc mappings,再配合cat /proc/pid/pagemap(需 root)解析特定地址 - 观察
/proc/pid/statm的第 2 列(resident):单位是页,即当前驻留物理内存的页数
示例(检查进程 1234 的 0x7ffff7ffa000 是否在内存):
python3 -c "
import struct
with open('/proc/1234/pagemap', 'rb') as f:
f.seek(0x7ffff7ffa000 // 0x1000 * 8)
entry = struct.unpack('Q', f.read(8))[0]
print('Present:', bool(entry & (1<<63)))
print('Page Frame Number:', entry & ((1<<55)-1))
"注意:该脚本需 root 运行,且目标进程不能被 ptrace 保护(ptrace_scope=1 时受限)。
真正复杂的地方在于:页表项本身可能被换出、被 KSM 合并、被透明大页拆分、甚至被 CMA 预留区绕过——这些都不是靠读一个文件就能说清的,得结合具体内核版本和配置来看。









