缺页中断:内存不够时操作系统怎么办?补页与换页

物理内存永远不够用,但操作系统总能让程序以为自己独占一大片内存——秘密就在「缺页」这套随用随补的机制里。

一、背景与痛点

你有没有遇到过这种情况:程序冷启动后第一次点某个功能,界面会「顿」一下;手机切回一个后台很久的 App,要转圈一两秒才恢复;容器集群里某个 Pod 突然内存飙升,被 OOM Killer 干掉。这些看似无关的现象,背后其实是同一件事——缺页(page fault)与页面置换在起作用。

问题的源头很朴素:物理内存是有限的,而所有进程的虚拟地址空间加起来可以远超物理 RAM。程序以为自己拥有一整片连续内存,可真要一个字节一个字节全塞进物理内存,机器早就爆了。操作系统用一个巧妙的机制化解了矛盾:绝大多数页根本不必立刻驻留内存,用到时再补;当物理内存真的不够、又有新页要进来时,就把最不值得留的旧页请出去。前者叫缺页,后者叫页面置换。本文先把「内存不够」这件事的发生过程和缺页的完整流程讲清楚,下一期再专门讲「选谁走」的算法。

二、核心原理

现代系统几乎都采用按需分页(demand paging)。程序启动时,内核并不把整个可执行文件、堆、栈一股脑搬进内存,而是先建立页表映射,把存在位(present)清成 000,表示「这一页现在不在内存里」。当 CPU 首次访问某个虚拟页,MMU 查页表发现存在位为 000,无法译出物理地址,于是触发缺页异常,陷入内核。内核接手后分配一个空闲物理帧,把该页内容(来自可执行文件、全零的匿名页、或换出到磁盘的页)读进来,填好页表项的存在位与物理帧号,再让出错指令重新执行——这一次翻译就成功了。

值得强调的是:page fault 是个中性术语,不是崩溃级错误。按成因它分好几类。次要缺页(minor/soft fault)是指页不在内存但内容就在别处(例如共享库已被别的进程加载、或页还在页缓存里),内核只需建立映射,不用读盘,代价很小。主要缺页(major/hard fault)是指页既不在内存也不在缓存,必须真的从磁盘(文件或交换区)读进来,代价是毫秒级,是性能尖刺的主要来源。保护缺页则是权限不匹配(写只读页、执行不可执行页),内核可能做写时复制或直接发 SIGSEGV。真异常是访问了根本没映射的地址(空指针、越界)。

那么「内存真的不够」是怎么发生的?内核维护一个空闲帧链表。启动初期内存宽裕,缺页就从链表拿一帧填上即可。但当链表被耗尽——所有物理帧都被占满,而又有新缺页要进场——矛盾就正面爆发:没有空位留给新页。此时内核必须从已在内存的页里挑一个腾出来。如果那页是干净的(脏位 D=0D=0D=0),直接丢弃即可;如果是脏的(D=1D=1D=1),得先把内容写回磁盘才能腾帧,这一步带来一次写盘开销。腾出的帧分配给新缺页的进程,流程继续。这个「腾旧页给新页」的动作就是页面置换,被挑中请走的页叫 victim。注意置换发生在缺页处理内部:不是预先腾好,而是「撞到墙了才换」,这正是按需分页随需随换的哲学。

用数学刻画性能,缺页率 ppp 直接决定有效访问时间:

EAT=(1−p) tmem+p tfaultEAT = (1-p)\,t_{mem} + p\,t_{fault}EAT=(1p)tmem+ptfault

其中 tmemt_{mem}tmem 是命中时的访问时间(纳秒级),tfaultt_{fault}tfault 是一次主要缺页的处理时间(毫秒级),两者相差五六个数量级。所以哪怕 ppp 只有 0.0010.0010.001EATEATEAT 也会被 tfaultt_{fault}tfault 拉高好几倍。更危险的是抖动(thrashing):当所有进程的工作集总和 ∑iWi\sum_i W_iiWi 超过可用物理内存 MMM,即

∑iWi>M\sum_i W_i > MiWi>M

系统就会陷入「一直在换页、没空干活」的恶性循环,吞吐量断崖式下跌。

三、代码实战

下面用纯标准库仿真一个「随需随换」的内存系统。它维护一个有限的物理帧池和一个空闲链表,给定一条访问虚拟页的序列:命中就直接访问;未命中且有空闲帧则记为次要缺页;空闲耗尽则触发置换并记一次换盘(主要缺页)。为了突出「内存不够」的临界点,这里先用最简单的占位置换策略,下一期再换成真正的 FIFO/LRU 算法。

import random

random.seed(3)
N_VPAGES = 200
working = list(range(40))          # 工作集:40 个常用虚拟页
seq = []
for _ in range(2000):
    if random.random() < 0.85:
        seq.append(random.choice(working))          # 85% 落回工作集
    else:
        seq.append(random.randint(0, N_VPAGES - 1)) # 15% 冷页

def simulate(frames):
    in_mem = set()                   # 当前驻留内存的页
    free = list(range(frames))       # 空闲物理帧池
    minor = major = 0
    for v in seq:
        if v in in_mem:              # 命中,直接访问
            continue
        if free:                     # 有空闲帧:次要缺页
            free.pop()
            in_mem.add(v)
            minor += 1
        else:                        # 空闲耗尽:置换并换盘,主要缺页
            victim = next(iter(in_mem))   # 占位策略,第 033 期换 FIFO/LRU
            in_mem.discard(victim)
            in_mem.add(v)
            major += 1
    return minor, major

for f in [10, 20, 30, 40, 60, 80, 120, 200]:
    mi, ma = simulate(f)
    total = mi + ma
    print(f"帧数={f:3d}  次要缺页={mi:4d}  主要缺页={ma:4d}  总缺页率={total/len(seq)*100:.1f}%")

运行结果:

帧数= 10  次要缺页=  10  主要缺页=1923  总缺页率=96.7%
帧数= 20  次要缺页=  20  主要缺页=1875  总缺页率=94.8%
帧数= 30  次要缺页=  30  主要缺页=1793  总缺页率=91.1%
帧数= 40  次要缺页=  40  主要缺页=1719  总缺页率=87.9%
帧数= 60  次要缺页=  60  主要缺页=1517  总缺页率=78.8%
帧数= 80  次要缺页=  80  主要缺页=1276  总缺页率=67.8%
帧数=120  次要缺页= 120  主要缺页= 572  总缺页率=34.6%
帧数=200  次要缺页= 163  主要缺页=   0  总缺页率= 8.2%

可以看到非常清晰的规律:当帧数远小于工作集(40 页)时,访问几乎每次都会撞上置换,总缺页率高达 96.7%,而且几乎全是昂贵的主要缺页;随着帧数增大,主要缺页快速减少,总缺页率一路降到 8.2%。当帧数达到 200(能装下全部页)时,主要缺页直接归零,只剩少量良性次要缺页。关键临界点就落在帧数接近工作集大小附近——这直观解释了「内存够不够」为什么是性能的分水岭:一旦物理帧装不下工作集,系统就会持续换盘,把 ns 级访问拖成 ms 级等待。

四、关键经验/避坑

  1. 别把 page fault 当故障报警。绝大多数缺页是正常的按需加载路径,只有访问未映射地址、或无法处理的权限冲突才会演变成 SIGSEGV。一见 fault 就报警,只会制造大量假阳性。
  2. 看指标要把 minor 与 major 拆开。minor 基本无害,major 才说明内存吃紧、在频繁换盘;把两者混为一谈,会把无害的懒加载噪声当成系统故障。
  3. 「关掉 swap 就更快」是错觉。swap 不是用来日常跑的,而是兜底防 OOM。没有 swap,内存一旦耗尽只能杀进程,可能干掉关键服务;保留一点 swap 反而更稳。
  4. 置换不只发生在内存 100% 占满那一刻。内核有空闲水位线,会在真正耗尽前就后台回收;而且锁定页、内核页、正进行 IO 的页都不能换出,所以「还有空闲内存却在换出」并不矛盾。
  5. 区分文件页与匿名页。文件页背后有磁盘文件,脏了写回原文件、干净了直接丢,换出便宜;匿名页(堆/栈/malloc)没有文件做后盾,只能写进 swap,更加金贵。看到「已用内存很高但很流畅」多半是文件页缓存,不必惊慌。

五、完整系列推荐

📚 本文选自《操作系统内核与驱动详解》100 期系统教程(第 032 期:缺页与页面置换),每期配可运行 Python 代码。

完整系列(100 期正文 + 3 篇番外,每期文章+代码)已在 ima 知识号【Kruptos】持续更新:

  • 🗂 70+ 技术知识库:操作系统、图神经网络、强化学习、数据库系统、推荐系统、编译原理……几乎覆盖全部软硬件技术栈
  • 🧠 8 款 AI 技能:系列生产、知识库管理、CMMI 受管开发、自进化 Agent 等,已在 ima 技能广场上架,即装即用
  • ✅ 全部免费订阅,后续更新自动推送

🔍 订阅方式:打开 ima(腾讯智能工作台)→ 搜索「Kruptos」→ 一键订阅;或在 ima 内直接搜索《操作系统内核与驱动详解》。


作者:Kruptos(西电毕业,13 年无线通信/DSP/嵌入式科研,现深耕 AI 与云原生)
原创内容,转载注明出处。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐