虚拟内存是什么?分页和缺页中断是怎么回事?
一句话回答
虚拟内存让每个进程都以为自己独占一块连续的大地址空间,程序用的是虚拟地址,由 CPU 的 MMU 查页表翻译成物理地址,TLB 缓存最近的翻译结果。内存按固定大小的页(常见 4KB)管理,页表里没有映射的页被访问时触发缺页异常,内核再分配物理页或从磁盘读入,所以程序可以按需加载。好处是进程之间相互隔离、可以使用比物理内存更大的空间、共享库和 fork 可以共享物理页。
详细解析
为什么需要虚拟内存
- 隔离:每个进程有自己的页表,访问不到别人的物理内存,一个进程写坏内存不会影响其他进程
- 连续的地址空间:物理内存是零散分配的,但进程看到的堆、栈都是连续的地址,编译和链接也简单
- 按需加载:程序启动时不用把整个可执行文件读进内存,访问到哪页再加载哪页
- 共享与写时复制:多个进程的虚拟页可以映射到同一个物理页,比如共享库的代码段只占一份物理内存
地址转换:页表、MMU、TLB
虚拟地址 = 虚拟页号 + 页内偏移
│
├─> TLB 命中 ───────────────> 物理页号 + 页内偏移 = 物理地址
│
└─> TLB 未命中 ─> MMU 逐级查页表(x86-64 常见 4 级,部分新 CPU 支持 5 级)
├─ 页表项有效 ─> 得到物理页号,写入 TLB
└─ 页表项无效 ─> 触发缺页异常,交给内核处理
页表是多级的:如果用一张平铺的大表映射整个 64 位地址空间,表本身就大得放不下。多级页表只为实际用到的区域分配下级页表。代价是每次转换要多次访存,所以有了 TLB 这个专用缓存。进程切换时页表换了,TLB 里的旧映射大多失效(带进程标识的 TLB,如 x86 的 PCID,可以减少刷新),这是进程切换比线程切换贵的原因之一。
缺页异常的处理
- CPU 访问虚拟地址,页表项标记为"不在内存",触发缺页异常,从用户态陷入内核
- 内核查找这个地址属于进程的哪个虚拟内存区域(VMA)。不属于任何区域或权限不符,就给进程发 SIGSEGV,也就是常见的段错误
- 地址合法,根据类型处理:
- 匿名内存(堆、栈)第一次访问:分配一个清零的物理页
- 文件映射(代码段、mmap 的文件):页面在 page cache 里就直接映射,不在就从磁盘读
- 页面被换出到 swap:从 swap 读回来
- 写一个写时复制的只读共享页:复制一份再改为可写
- 更新页表,返回用户态,重新执行触发异常的那条指令
需要读磁盘的叫主缺页(major fault),耗时长;不需要读磁盘的叫次缺页(minor fault),开销小得多。malloc 一大块内存后,top 里 RES 不会马上涨,就是因为物理页在第一次写入时才分配。
页面置换、swap 和 OOM
物理内存不够时,内核要挑一些页腾出来:干净的文件页直接丢弃,下次再从文件读;脏页先写回文件;匿名页写到 swap。挑哪些页由置换算法决定:
| 算法 | 思路 | 说明 |
|---|---|---|
| OPT | 淘汰将来最久不用的页 | 理论最优,无法实现,用来做对比 |
| FIFO | 淘汰最早进来的页 | 简单,但可能淘汰常用页 |
| LRU | 淘汰最近最久没用的页 | 效果好,精确实现要在每次访问时维护顺序,硬件成本高 |
| Clock(二次机会) | 页面排成环,有访问位就清零跳过,没有就淘汰 | LRU 的近似,开销低 |
Linux 用 active 和 inactive 两组链表近似 LRU,页面被再次访问才提升到 active 链表;较新的内核还提供了多代 LRU(MGLRU),是否启用取决于内核配置和发行版。
swap 用得多时,进程频繁地换出换入,出现大量主缺页,系统会明显变慢(抖动)。内存和 swap 都耗尽时,内核启动 OOM Killer,按 oom_score 挑一个进程杀掉,可以通过 /proc/<pid>/oom_score_adj 调整某个进程被选中的倾向。容器里超过 cgroup 的内存上限也会触发 OOM,见 requests 和 limits。
fork 和写时复制
fork 时内核不复制物理内存,只复制页表,把父子双方的页都标成只读。任何一方写某页时触发缺页异常,内核才复制这一页。Redis 的 RDB 快照就利用了这一点,见 RDB 和 AOF。
代码示例
观察进程的内存和缺页情况:
# VSZ 是虚拟内存大小,RSS 是实际占用的物理内存;min_flt、maj_flt 是累计的次缺页、主缺页次数
ps -o pid,vsz,rss,min_flt,maj_flt,cmd -p 1234
# 进程的虚拟内存区域:代码段、堆、栈、共享库、mmap 区域
cat /proc/1234/maps
# 系统层面:si、so 列是每秒换入、换出 swap 的量,持续不为 0 说明内存紧张
vmstat 1
面试官可能追问
VSZ 很大但 RSS 很小,有问题吗?
通常没有。VSZ 统计的是进程申请的全部虚拟地址空间,包括还没访问过的 malloc 内存、映射了但没读的文件、线程栈预留的空间。真正占用物理内存的是 RSS。Go、Java 这类运行时会预留大段地址空间,VSZ 看起来很吓人,判断内存问题应该看 RSS 和它的增长趋势。
为什么 malloc 了一大块内存不会报错,用的时候却被 OOM Killer 杀了?
Linux 默认允许内存超额分配(vm.overcommit_memory 为 0 时用启发式判断),malloc 只分配虚拟地址,物理页在第一次写入时才通过缺页分配。真正写入时物理内存和 swap 都不够了,内核只能通过 OOM Killer 杀进程来回收内存。设为 2 可以严格限制分配总量,但很多程序依赖超额分配,要谨慎调整。
大页(Huge Page)是做什么的?
页越大,同样的 TLB 条目能覆盖的内存越多,TLB 未命中和页表的层级都会减少,x86-64 上常见的大页是 2MB。数据库等大内存应用常配置大页提升性能。但透明大页(THP)会让写时复制的粒度变大、后台合并页面带来延迟抖动,Redis 的文档就建议关闭它。是否开启要看具体软件的官方建议。
易错点
- 缺页异常是正常机制,不是错误。只有访问非法地址时才会变成段错误
- "虚拟内存 = swap"是误解:虚拟内存是地址转换和隔离的机制,swap 只是把匿名页换到磁盘的一种手段,没有 swap 同样有虚拟内存
- TLB 缓存的是页表项(虚拟页到物理页的映射),不是数据本身;数据缓存是 CPU 的 L1/L2/L3
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。