操作系统学习笔记 · 第 16 课 · 虚拟内存——每个进程的"独立世界"
上一课我们留了个悬念:程序用的是"虚拟地址",中间隔着 MMU 翻译。那为什么要费这么大劲造这层假象? 答案藏在三个词里:隔离、超卖、共享。这一课,我们走进虚拟内存的核心,看清它如何给每个进程造出一个"独立世界",以及翻译背后的机制——分页、页表、缺页、TLB。
从"每个人都觉得自己有整条内存"说起
想象一栋楼里有 100 个住户,但房子只有 10 套。怎么让 100 个人都"住"进去?
答案:让每个人都"以为"自己有一套完整的房子,实际上大家共享 10 套,谁要用谁就临时占一套,不用了腾出来。
虚拟内存干的就是这件事:每个进程都"以为"自己独占了一大片连续内存,实际上它只占用了物理内存的一小块,其余部分要么在磁盘上,要么是"想用的时候才分配"。
16.1 虚拟内存的三个核心好处
一句话理解:虚拟内存给每个进程一个"独立、超大、可共享"的地址空间,带来三大好处。
| 好处 | 含义 |
|---|---|
| 隔离 | 每个进程有独立地址空间,互不干扰,一个崩不连累别人 |
| 大于物理内存 | 程序可用比物理内存更大的空间,靠换页实现 |
| 共享 | 同一段内存可映射到多个进程(如共享库、mmap) |
- 隔离:进程 A 的地址
0x1000和进程 B 的地址0x1000,被 MMU 翻译到不同的物理地址,谁也碰不到谁。所以一个程序崩溃(比如野指针写飞了),不会拖垮别的程序,更不会搞挂系统; - 大于物理内存:你机器只有 8GB 内存,却可以跑一个需要 20GB 的程序——因为没在用的部分可以先放在磁盘上,用到哪页再换进哪页(下一课讲"置换");
- 共享:
libc这样的共享库,物理内存里只存一份,却同时映射进几十个进程的地址空间。省内存、也省加载时间。
这三条,就是"费劲造假象"的全部理由。它们让现代多进程、多任务系统成为可能。
16.2 分页(Paging)
虚拟内存的实现,核心手段叫分页。
一句话理解:把虚拟内存和物理内存都切成固定大小的"页"(通常 4KB),按页映射,而不是按字节。这样管理简单、碎片少。
为什么不按"字节"管理,而要按"页"?
- 如果按字节,每个字节都要一条"虚拟→物理"的对应关系,映射表大得不可想象;
- 切成固定大小的页(page,通常 4KB),一整页一整页地映射,映射表就小得多,管理也整齐。
示例:
虚拟页 0 → 物理页 100
虚拟页 1 → 物理页 50
虚拟页 2 → 物理页 3注意两个特点:映射可以乱序(虚拟页 0 对应物理页 100,虚拟页 1 对应物理页 50,不连续);物理页可以不连续。这带来一个巨大好处——程序不需要一段"连续的大块物理内存",东一块西一块拼起来就行,外部碎片问题大大缓解。
16.3 页表与地址翻译
那"虚拟页 → 物理页"的对照关系,存在哪?存在页表里。
一句话理解:页表就是"虚拟页 → 物理页"的对照表,CPU 的 MMU 每次访存都查它。
一次地址翻译长这样:
虚拟地址 = 页号 + 页内偏移
↓ 查页表
物理地址 = 物理页号 + 页内偏移(偏移不变)关键点:翻译时,只有"页号"被替换,"页内偏移"原样保留。因为页是固定大小,页内位置不会变。
多级页表:页表本身也可能很大(比如 64 位地址空间),于是用多级页表——像一本书的"章 → 节 → 页"目录,按需展开,只给真正用到的部分建表,节省大量内存。
记住:页号被翻译,偏移不变;多级页表是为了"省内存、按需建表"。
16.4 缺页中断(Page Fault)
既然"大于物理内存",那访问一个"还没在物理内存里"的页,会怎样?
一句话理解:访问的页不在物理内存里(还没加载或已被换出),触发缺页异常,内核把页从磁盘换进来,再重跑那条指令。
过程:
- 程序访问某个虚拟地址;
- MMU 查页表,发现这页不在内存里(标记为"无效"或"在磁盘上");
- 触发缺页中断——这又是一个"异常",和 15.3 课讲的中断/异常是一家人;
- 内核介入:从磁盘把这一页加载进物理内存,更新页表;
- 重新执行刚才那条被中断的指令——这次就成功了。
生活类比:你要找一本书,书不在书架上(在仓库),就得先去仓库取来放上书架,再继续读。第一次读文件慢、第二次快,正是因为第一页已经被换进内存缓存了(页缓存,第 21 课细讲)。
16.5 TLB(快表)
每次访存都要查页表,而查页表本身也要访存——这会导致"为了一次访存,反而多访存好几次",太慢。怎么办?
一句话理解:TLB 是 CPU 里的小缓存,把最近用过的"虚拟页→物理页"映射存起来,命中就不用查页表。
- TLB(Translation Lookaside Buffer,快表):CPU 内部的一小块高速缓存,专门存"最近用过的虚拟页→物理页"映射;
- 访问地址时,先查 TLB;命中就直接拿到物理页,省去查页表;没命中才去查页表,然后把结果存进 TLB。
生活类比:TLB 就像常用电话号码本——先翻小本本,翻到了直接打;翻不到才去查大电话簿,查完把号码抄进小本本。
一句话总结这一课的主线:分页切空间,页表做翻译,缺页管补货,TLB 加速翻译。
动手实验:命令观察地址翻译
cat /proc/self/maps # 看当前进程的虚拟内存映射
cat /proc/self/smaps | head # 更详细(含 RSS 等)
getconf PAGE_SIZE # 看本机页大小(通常 4096)观察 /proc/self/maps 每一行,它长这样(示意):
地址范围 权限 偏移 设备 inode 路径
00400000-00401000 r-xp 00000000 fd:01 1234 /usr/bin/xxx
00600000-00601000 rw-p 00000000 fd:01 1234 /usr/bin/xxx
7fff1234000-... rw-p 00000000 00:00 0 [stack]关注两点:
- 权限位:代码段是
r-x(可读可执行、不可写),数据段是rw-(可读可写、不可执行),栈是rw-且地址较高; - 地址高低:栈在高地址,代码段在低地址,和上一课的内存布局完全对应。
注意:你看到的这些地址全是虚拟地址——它们的存在,恰恰证明了"每个进程都活在自己的虚拟世界"里。
深入点:两个进阶话题
① mmap 与共享内存
mmap 能把文件(或匿名内存)直接映射进进程的地址空间。好处:
- 访问文件像访问内存一样快(内存映射 I/O);
- 多个进程把同一块物理内存映射进各自地址空间,就实现了共享内存(还记得第 9 课的 IPC 吗?共享内存就是靠这个)。
mmap 是高性能 I/O 和共享内存的基石,后面第 21 课还会遇到它。② 大页(Huge Page)
默认页是 4KB。如果程序要管理几百 GB 内存,页表会非常庞大,TLB 装不下、频繁换出,性能暴跌。
于是有了大页:用 2MB 甚至 1GB 的页。页更大 → 页表项更少 → TLB 命中率更高。数据库(如 PostgreSQL、MySQL)常建议开大页,就是为了这个。
小结与思考题
这一课,我们揭开了虚拟内存的真相:
- 三大好处:隔离(互不干扰)、大于物理内存(靠换页)、共享(一份内存多处映射);
- 分页:按固定大小的页映射,简单、碎片少、可乱序;
- 页表 + MMU:翻译"虚拟页 → 物理页",页号变、偏移不变;
- 缺页中断:访问不在内存的页,内核从磁盘补货,再重跑指令;
- TLB:快表缓存常用映射,加速翻译。
留三个问题:
- 虚拟内存的三大好处?
- 缺页中断是谁触发的?内核做了什么事?
- TLB 解决什么性能问题?
现在我们知道:内存"不够用"时,靠缺页中断把页从磁盘换进来。但换进来之前,总得先腾个位置吧? 物理内存满了,要加载新页,就得踢掉一个旧页——踢谁?这一脚踢得好不好,直接决定系统是飞快还是卡成幻灯片。下一课,我们聊页面置换,看看操作系统怎么"挑倒霉蛋"。