仙味轩是城里最火的餐厅,可它的厨房只有一张两米长的操作台。偏偏老板野心大,菜单越印越厚:鱼翅、鲍鱼、三千道菜,看得客人眼花缭乱。你可能会问:厨房这么小,这么多菜怎么做?老主厨王师傅神秘一笑——他把厨房底下挖了个大冷库,食材全部标准份装盒,堆在冷库里;客人点菜时,他只把手上要用的几盒放到操作台,用完了送回去,换下一批。客人眼里,菜单永远是完整的;操作台,却永远只有最需要的那几样。这个故事,就是操作系统里大名鼎鼎的「虚拟内存」:用一个小厨房(物理内存)加一个大冷库(磁盘),配合一套聪明的记账和搬运规矩,让每个程序都以为,自己独占了一座无比宽敞的厨房。这套机制藏在每一台电脑、每一部手机里,是操作系统最性感的魔术之一。

岗位关联

理解虚拟内存,是通向操作系统、数据库、云原生与性能调优的必修课。无论排查内存泄漏、优化服务延迟,还是为大模型训练腾内存,你都会感谢今天读懂了这套「厨房管理学」。

目录
  1. 可视化演示
  2. 地址空间抽象
  3. 分页映射机制
  4. 页表与快表
  5. 缺页中断处理
  6. 页面置换算法
  7. 性能与优化
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示泰勒级数如何用多项式逐步逼近目标函数,随项数增加近似曲线收敛到真实函数,观察截断误差的变化。

本节配有交互动效(进入视口后自动加载,约 3KB)。

泰勒级数展开 目标函数 泰勒近似 f(x)≈Σfⁿ(0)xⁿ/n! 项数越多近似越精确

地址空间抽象

仙味轩的招牌菜越做越多,可厨房操作台只有两米长。老主厨想了个妙招:给每位客人发一本「完整菜单」,上面印着上千道菜,看起来应有尽有——至于食材此刻在操作台上还是冷库里,客人根本不用关心。这本菜单,就是程序眼里的「虚拟地址空间」。

虚拟内存的第一步,是把「程序看到的世界」和「物理世界的真实地盘」彻底分开。每个进程都以为自己独占了一块从 0 开始、连续又巨大的地址空间,这就是地址空间抽象。就像每桌客人各拿一本自己的菜单,没有人会觉得自己在跟别人抢菜谱;两个程序即使都用了地址 0x1000,也互不干扰,因为翻译出来的物理位置可能天差地别,这就是地址隔离。

32 位系统里,菜单能「印」出 4GB(2³²)的地址;64 位更是天文数字。但操作台(物理内存)可能只有 8GB、16GB。程序发出的每个地址都是虚拟地址,必须经过转换才能落到物理地址。这个转换的第一步,是把虚拟地址拆成两段:页号和页内偏移——就像把菜谱页码拆成「第几页」加「页内第几行」。

有了「菜单 + 坐标」的抽象,主厨再也不用担心操作台放不放得下:客人眼里菜品永远齐全,缺的食材让助手从冷库补就行。这种「看得见的完整,看不见的搬运」,正是虚拟内存的灵魂——程序只跟连续的假象打交道,物理内存的零散、大小、归属,都被悄悄藏在了菜单背后。

V=P×2ω+O\displaystyle V = P \times 2^\omega + O
一个虚拟地址 V 等于「页号 P 乘以页面大小 2^ω 再加上页内偏移 O」。页面大小通常为 4KB,此时 ω = 12。
  1. 虚拟地址 V程序每条指令拿到的地址,相当于菜单上的「第几页第几行」坐标。
    V\displaystyle V
  2. 页面大小 2^ω每个保鲜盒能装的固定份量。ω 是偏移位数,4KB 盒子的 ω = 12。
    2ω\displaystyle 2^\omega
  3. 页号 P地址除以页面大小取整,就是「第几个保鲜盒」。
    P=⌊V/2ω⌋\displaystyle P = \lfloor V / 2^\omega \rfloor
  4. 页内偏移 O除以页面大小的余数,即盒子里第几格。
    O=V mod 2ω\displaystyle O = V \bmod 2^\omega

分页映射机制

光有菜单还不够,还得有「标准保鲜盒」。仙味轩定下规矩:所有食材一律切成标准份,每份装进方方正正的保鲜盒。这样不管这盒食材来自冷库哪个角落,操作台上任何一格都能放下——这就是「页」与「页框」的由来。

在操作系统里,虚拟地址空间被切成固定大小的块,叫「页」(Page);物理内存被切成同样大小的格子,叫「页框」(Page Frame)。页和页框大小完全相等,通常都是 4KB。这一刀切的标准化,让任意页可以装进任意页框,既消灭了外部碎片,又让搬运变成了模块化的流水线作业。

映射关系全靠操作系统维护。程序访问虚拟页 P 时,系统查页表找到它被放到了哪个页框 F,再拼上页内偏移,就得到真实的物理地址。注意页框号是「随缘」的——页面 0 可能装在页框 2,页面 1 装在页框 0,完全看当时哪个格子空着。页表项里还记录权限位,相当于保鲜盒上「仅可看不可吃」的标签,越界访问或写入只读页都会触发保护异常,这就是内存保护。

标准保鲜盒让「搬食材」变成流水线:不管顾客点的是鲍鱼还是青菜,助手只管搬相同的盒子。操作系统也一样,不管程序要的是代码还是数据,统一按页搬运、按页映射,简单又高效。分页机制的伟大之处,就在于用「标准化的笨」换来了「全局的巧」。

PA=F×2ω+O\displaystyle PA = F \times 2^\omega + O
物理地址 = 页框号 F × 页面大小 + 页内偏移 O。页框号 F 就是查页表得到的「操作台格子编号」。
  1. 查页表得到页框号 F以虚拟页号 P 为索引,在账本(页表)里找到它对应的操作台格子编号。
    F=PT[P]\displaystyle F = \mathrm{PT}[P]
  2. 页框起始地址 F × 2^ω第 F 个页框的起点,等于格子编号乘以每格大小。
    F×2ω\displaystyle F \times 2^\omega
  3. 加上页内偏移 O在格子内部定位到精确字节,偏移量与虚拟页内的偏移完全一致。
    PA=F×2ω+O\displaystyle PA = F \times 2^\omega + O

页表与快表

盒子多了,就得记账。仙味轩的账房先生捧着一本大账本,记录每个保鲜盒目前在哪儿、权限如何。可问题来了:账本厚达几十万条,每次取食材都要先翻账本,翻完还要去操作台,菜还没下锅,时间先没了大半。

这个账本就是页表(Page Table)。每个进程都有自己的一份页表,记录虚拟页号到页框号的映射。但 32 位系统 4KB 页面意味着约 100 万个页表项,每项 4 字节就是 4MB——每个进程光记账就要 4MB。于是多级页表登场:把账本拆成「总目录 + 分册」,稀疏的地址空间不用再为用不到的页预留条目,相当于账房只给常用的保鲜盒建册,省下了大量内存。

更狠的加速是 TLB(快表)。它缓存最近用过的页表项,相当于主厨手边贴着的「常用食材速查卡」。查 TLB 命中,一次内存访问搞定;未命中,才去翻完整页表,代价几乎翻倍。程序的时间局部性保证了 TLB 命中率通常高达 99% 以上,所以「先查速记卡」是每次内存访问的必经之路。

仙味轩最忙的时候,主厨根本不用翻账本——手边速记卡记着最常用的十几个位置,一抓一个准。TLB 就是操作系统的「手边速记卡」:用极小的硬件代价,换来绝大多数地址转换的零额外开销。账本可以厚,但速记卡必须薄;这是计算机工程里「缓存」思想最经典的一次亮相。

EAT=h(tT+tM)+(1−h)(tT+2tM)\displaystyle EAT = h (t_T + t_M) + (1 - h)(t_T + 2 t_M)
有效访问时间 EAT 是「命中与未命中」两种情况的加权平均。h 是 TLB 命中率,t_T 是查快表时间,t_M 是访问内存时间。
  1. 命中情形 h(t_T + t_M)快表里直接查出页框号,只需一次内存访问就能拿到数据。
    h(tT+tM)\displaystyle h (t_T + t_M)
  2. 未命中情形 (1-h)(t_T + 2t_M)先查 TLB 失败,再访存查页表,最后还要访存取数据,共两次内存访问。
    (1−h)(tT+2tM)\displaystyle (1 - h)(t_T + 2 t_M)
  3. 加权求期望两类情形按概率加权,得到平均一次访存的耗时。
    EAT=h(tT+tM)+(1−h)(tT+2tM)\displaystyle EAT = h (t_T + t_M) + (1 - h)(t_T + 2 t_M)

缺页中断处理

菜单再全,也有食材不在操作台的时候。仙味轩的规矩是:主厨要什么,助手立刻放下手里的活,跑一趟冷库取来。虽然慢,但总比告诉客人「这道菜做不了」强。这在操作系统里,就是「缺页中断」。

当程序访问的虚拟页在页表中标记为「不在内存」时,CPU 会触发缺页中断,陷入操作系统内核。内核先检查访问是否合法——越界或权限不足就直接报段错误;合法则找一块空闲页框,发起磁盘 I/O 把页从交换空间读入,更新页表,最后重新执行那条被中断的指令。整个过程对上层程序完全透明,程序只觉得自己「稍微卡了一下」。

这里有个关键设计:交换空间(Swap)就是冷库的专门货架。页面换出到磁盘时,只有被修改过的「脏页」才需要写回;干净页(比如只读的代码页)直接丢弃,反正冷库里还有副本。这样能省下大量无谓的磁盘 I/O——毕竟让助手扛着没变过的盒子回冷库,纯属白费力气。

助手跑一趟冷库,可能比主厨炒十道菜还久。缺页中断也是这样:一次磁盘 I/O 约几毫秒,而一次内存访问只有几十纳秒,相差近十万倍。所以虚拟内存的命脉,是让「跑冷库」的频率低到尘埃里——这就要看下一页的置换算法了。

EAT=(1−p)⋅tM+p⋅tPF\displaystyle EAT = (1 - p) \cdot t_M + p \cdot t_{PF}
p 是缺页率(访问内存时缺页的概率),t_PF 是一次缺页处理的耗时(主要是磁盘 I/O)。一旦缺页,代价是普通访存的几十万倍。
  1. 不缺页 (1-p)·t_M绝大多数访问都命中页框,直接访问内存,耗时就是 t_M。
    (1−p)⋅tM\displaystyle (1 - p) \cdot t_M
  2. 缺页 p·t_PF缺页时要完成磁盘读入、更新页表、重新执行指令,t_PF 动辄毫秒级。
    p⋅tPF\displaystyle p \cdot t_{PF}
  3. 权衡的真相只要 p 足够小(比如 0.0001),EAT 就接近 t_M;一旦 p 上升,EAT 会爆炸式增长。
    EAT=(1−p)⋅tM+p⋅tPF\displaystyle EAT = (1 - p) \cdot t_M + p \cdot t_{PF}

页面置换算法

操作台满了,还得腾地方。仙味轩对「扔哪盒食材」定了三条规矩:最老实的做法是按进冷柜的顺序扔,先来的先走;聪明点的做法是看谁最久没被用,谁就下岗;还有折中的转盘法,像食堂取餐转台一样一圈圈扫。这三条规矩,对应 FIFO、LRU 和 Clock 算法。

FIFO(先进先出)用队列实现,简单但有著名怪癖——Belady 异常:有时把操作台从 3 格加到 4 格,缺页次数反而变多,就像厨房变大后助手反而手忙脚乱。LRU(最近最久未使用)利用时间局部性,把最久没碰过的页换出去,理论效果很好但实现昂贵——每次访问都要记录时间戳。工程上常用 Clock 算法近似 LRU:给每个页框挂一个访问位,指针循环扫描,遇 1 清零继续走,遇 0 就换出,开销小又效果好。

三种算法的差距靠「缺页率」来衡量:缺页次数除以总访问次数。对于典型参考串,LRU 明显优于 FIFO,Clock 则非常接近 LRU。这也是主流操作系统(如 Linux)采用类 Clock 算法的原因——性能与成本的最好折中。

好的置换策略,就是好的厨房理货员:他不追求记住所有食材,只保证「常点的菜永远在手边」。页面置换算法的本质,是在不知道未来点单的情况下,用过去猜未来——时间局部性,就是这场赌博唯一的胜率来源。

f=FN\displaystyle f = \frac{F}{N}
缺页率 f 是缺页次数 F 除以内存访问总次数 N。缺页率越低,置换算法越优秀,虚拟内存的「体验」越好。
  1. 总访问次数 N程序的每次访存都算一次「点菜」,是分母。
    N\displaystyle N
  2. 缺页次数 F其中需要跑冷库取页的次数,是分子。
    F\displaystyle F
  3. 缺页率 f两者相除,表示虚拟内存的「服务质量」。
    f=FN\displaystyle f = \frac{F}{N}

性能与优化

开业一个月,仙味轩迎来了晚市高峰:客人翻倍,下单频率暴涨。主厨却发现菜反而上不来了——助手不停往返冷库,操作台上永远是空的,全店陷入了「搬货—做菜—又缺货」的死循环。这就是操作系统的「抖动」(Thrashing)。

抖动的根源,是同时运行的进程太多,它们的「工作集」之和超过了物理内存。工作集是进程在一段时间窗口内实际访问的页面集合,代表它运转的「必需品」。如果每个进程的必需品都摆不满操作台,大家就只能抢,抢不到的就去冷库搬,结果所有人都在搬货,没有人在做菜。

应对抖动有三板斧:一是降低多道程序度,让一部分进程挂起,把页框让给活跃进程;二是利用工作集模型,动态调整每个进程的页框数;三是优化程序局部性——比如让循环尽量紧凑、数组按行访问,减少「跳东跳西」的访问模式。此外,监控工具里的「命中率」「swap 吞吐」就是厨房的体温计:命中率长期低于 95%,就该考虑加内存或降负载了。

老主厨最终悟了:厨房再大,也装不下一本菜单;真正要管理的不是「装下所有」,而是「把最常用的留在手边」。虚拟内存的性能秘诀,从来不是让冷库变快,而是让跑冷库的次数趋近于零——记住这一点,你就抓住了整个虚拟内存机制的牛鼻子。

W(t,Δ)={p∣p 在时间窗 (t−Δ, t] 内被访问}\displaystyle W(t,\Delta) = \{ p \mid p \text{ 在时间窗 } (t-\Delta,\, t] \text{ 内被访问} \}
工作集就是最近 Δ 时间内进程碰过的所有页面。只有保证每个进程的页框数不小于它的工作集大小,进程才能少缺页、有效率地跑。
  1. 时间窗口 Δ观察「最近这段多长的时间」。窗口太短看不见规律,太长又失去局部性意义。
    Δ\displaystyle \Delta
  2. 集合 W(t,Δ)把窗口内被访问过的页收集起来,就是进程此刻的必需品清单。
    W(t,Δ)={p∣p∈(t−Δ, t]}\displaystyle W(t,\Delta) = \{ p \mid p \in (t-\Delta,\, t] \}
  3. 分配原则分配给进程的物理页框数 m,必须不小于工作集大小 |W|,否则必然频繁缺页。
    m≥∣W∣\displaystyle m \ge |W|

AI 应用场景

数据库缓冲池调优

阶段:数据平台性能优化

解决的问题:数据库的缓冲池(Buffer Pool)就是数据库自己的「操作台」,磁盘就是「冷库」。传统调优只会一味加内存,但不懂换页策略的话,一次全表扫描就能把热数据全部挤出缓冲池,命中率上不去,加再多内存也是浪费。掌握 LRU 变体与命中率分析,才能精准定位到底该加内存还是该改查询。

具体案例:MySQL 的 InnoDB Buffer Pool 默认用改进版 LRU(冷区/热区)防止大查询污染热数据。DBA 通过 SHOW ENGINE INNODB STATUS 观察 Buffer pool hit rate,发现其长期在 95% 以下,于是把 innodb_buffer_pool_size 调到物理内存的 70% 并优化了几条全表扫描慢查询,命中率回升到 99.8%,线上查询延迟从 200ms 降到 5ms。

云原生容器环境的内存排查

阶段:在线推理服务稳定性保障

解决的问题:云上跑大模型推理或向量检索时,容器内存 limit 设置不当,进程被迫频繁 swap,表现为「CPU 不高但延迟暴涨」。传统排查只看 CPU 和内存用量,不懂 swap 与缺页的关联,很容易把问题误判为网络抖动或上游故障。

具体案例:SRE 监控某推荐服务的 cgroup 指标,发现 pgmajfault(主缺页)每秒上万次,定位到容器 swap 分区开启且工作集超过内存 limit。调整 limit 并关闭 swap 后,服务 P99 延迟从 8s 降回 80ms——那「8 秒的卡顿」,正是助手在冷库和操作台之间疲于奔命的代价。

AI 训练数据集的内存映射加载

阶段:数据预处理与训练加速

解决的问题:训练数据动辄几十 GB,传统 read 全量读入内存既慢又占内存;用 mmap 把文件映射进虚拟地址空间,操作系统按页懒加载,配合 page cache,多个 epoch 重复读同一批数据时几乎不再产生磁盘 I/O——这正是虚拟内存「按需搬运」思想在 AI 工程里的直接应用。

具体案例:某 CV 团队用 mmap 读取 60GB 图像数据集。第一个 epoch 有少量缺页(冷库搬运),第二个 epoch 开始命中 page cache,数据加载耗时从每轮 30 分钟降到 4 分钟,GPU 利用率从 45% 提到 92%。

Python 演示

python30 行
def simulate(reference, capacity, algorithm):    # 模拟页面置换, 返回缺页次数; algorithm 取值 'fifo' 或 'lru'    frames = []    last_used = {}    faults = 0    for tick, page in enumerate(reference):        if page in frames:            last_used[page] = tick            continue        faults += 1        if len(frames) >= capacity:            if algorithm == "lru":                victim = min(frames, key=lambda p: last_used.get(p, -1))            else:                victim = frames[0]            frames.remove(victim)        frames.append(page)        last_used[page] = tick    return faults  if __name__ == "__main__":    refs = [0, 1, 2, 3, 0, 1, 4, 0, 1, 2, 3, 4]    for cap in (3, 4):        f = simulate(refs, cap, "fifo")        l = simulate(refs, cap, "lru")        print(f"页框数={cap}: FIFO缺页={f}, LRU缺页={l}")    f3 = simulate(refs, 3, "fifo")    f4 = simulate(refs, 4, "fifo")    print(f"FIFO 页框从 3 加到 4, 缺页反而从 {f3} 升到 {f4}, Belady 异常复现")
目的:用同一段经典参考访问串分别跑 FIFO 和 LRU 页面置换,对比缺页次数,并复现 FIFO 的 Belady 异常——「加了内存反而更卡」。
  1. if page in frames:先看目标页是否已在操作台(页框)中,在则命中,只更新最近使用时间即可
  2. faults += 1不在则记一次缺页,这就是「助手要跑冷库」的次数
  3. victim = min(frames, key=lambda p: last_used.get(p, -1))LRU 的核心:在所有页框中挑「最近使用时间最早」的那一个换出
  4. victim = frames[0]FIFO 则简单粗暴:直接扔队头,不管谁更常用
  5. for cap in (3, 4):分别用 3 个和 4 个页框跑一遍,观察缺页数随物理内存增加的变化
  6. print(f"FIFO 页框从 3 加到 4, 缺页反而从 {f3} 升到 {f4}, Belady 异常复现")运行后你会看到 3 框 9 次、4 框 10 次:物理内存变多,缺页反而变多

常见误区

误区我一直以为虚拟内存就是把硬盘划一块出来当内存条用,内存不够了就往硬盘里塞,等于免费扩容。既然能无限扩容,为什么程序还会卡?

正解虚拟内存的核心不是「扩容」,而是「地址抽象」。每个进程看到的是一个独立连续的虚拟地址空间,物理内存只是它背后的真实资源池。换页(借冷库)只是兜底手段,磁盘比内存慢十万倍,频繁换页就是抖动,系统吞吐反而暴跌。真正的扩容只能靠加物理内存,虚拟内存解决的是「装得下」和「管得好」的问题,而不是「变快」的问题。

为何常见:「虚拟内存」这个名字太有误导性,加上早期 Windows 确实用「虚拟内存设置」界面来管理页面文件,让人误以为那是在调内存容量。

误区既然每次搬运都有固定开销,那把页面设大一点,比如 1MB,一次搬更多食材不是更省事吗?反正页表也变薄了。

正解页面变大,页表确实变薄,但「页内碎片」会变大——程序往往只用到页面的一小部分,整页装进来就是浪费。4KB 是几十年的经验折中:让页表不算太厚、碎片不算太多、磁盘传输不算太亏。大数据平台常用的 2MB 大页是特定场景的优化,不能普适。

为何常见:「批量搬运更高效」在单次传输上成立,但内存管理的成本不只是搬运,还有碎片、页表、换入换出的整体平衡。

误区教科书说 LRU 最优,那操作系统肯定用的就是 LRU,工程里直接照抄这个算法就行了。

正解精确 LRU 每次访问都要更新「最近使用」的时间戳,代价高得无法接受,所以工程上都是近似实现:Linux 用改进的 Clock 算法(LRU 近似),数据库 InnoDB 还用冷热分带的改良 LRU 防止全表扫描污染缓冲池。理解算法思想是第一步,理解工程折中是第二步。

为何常见:教材为了讲清原理,会先给理想模型;而工程问题永远是「效果 vs 代价」的权衡,初学者容易把理想模型当成真实实现。

误区看到系统日志里一堆 major page fault,是不是内存坏了?缺页一定是性能 bug 吧?

正解缺页是虚拟内存的正常工作机制:进程第一次访问某个页面,该页不在内存,必须从磁盘调入,这本身就是一次缺页。真正要警惕的不是「有缺页」,而是「缺页率过高」(抖动)。只要命中率维持在 99% 以上,系统就是健康的。

为何常见:「fault」在英文里很容易被当作「错误」——实际上这里的 fault 只是「缺失」的意思,是个中性事件,就像助手跑趟冷库而已。

面试题

Q1. 虚拟内存最核心的价值是什么?

单选题 · easy

  • A.让程序使用内存条之外的磁盘空间
  • B.为每个进程提供独立连续的地址空间抽象← 正确答案
  • C.把磁盘上的文件缓存到内存里
  • D.对物理内存中的数据进行压缩

答案:B。虚拟内存的本质是地址抽象与隔离,让每个进程都以为自己独占一块连续大空间,物理内存只是背后的资源池。A 说的是换页的副作用而非核心,C 是页缓存,D 是数据压缩技术,都与虚拟内存的本质无关。

Q2. 页式虚拟内存中,「页」与「页框」的关系是?

单选题 · easy

  • A.页在磁盘,页框在内存,二者大小相等← 正确答案
  • B.页在内存,页框在磁盘,大小不等
  • C.页比页框大,便于一次搬更多数据
  • D.页与页框大小必须严格不同

答案:A。虚拟地址空间被切成「页」,物理内存被切成同样大小的「页框」,二者大小必须相等(如都是 4KB),才能让任意页装进任意页框——就像标准保鲜盒必须适配操作台上的每一格。B、C、D 都违背了分页的基本设定。

Q3. 某系统页面大小为 4KB,虚拟地址 0x3F5C 的页号是?

单选题 · mid

  • A.3 号页← 正确答案
  • B.0x3F 号页
  • C.15 号页
  • D.0xF5C 号页

答案:A。4KB = 0x1000,页号 = 地址 >> 12,即 0x3F5C 右移 12 位得到 0x3,页内偏移是低 12 位 0xF5C。展开成十进制:16156 ÷ 4096 = 3 余 3484,所以是 3 号页。这就像查菜单「第 3 页第 F5C 行」。

Q4. 关于 TLB(快表),下列说法正确的是?

多选题 · mid

  • A.缓存了最近使用的页表项← 正确答案
  • B.命中时能省掉一次内存访问← 正确答案
  • C.未命中时意味着必须去磁盘取数据
  • D.本质是一种高速缓存← 正确答案

答案:ABD。TLB 是页表项的高速缓存,命中时免去查页表的那次访存,直接得到页框号。C 错:TLB 未命中只是要查内存中的完整页表,不等于缺页——只有页表项显示页面不在内存才需要去磁盘,两者层级完全不同。

Q5. 关于缺页中断,下列说法正确的是?

单选题 · mid

  • A.指令执行期间可触发,处理完后重新执行该指令← 正确答案
  • B.只能等当前指令结束后再统一处理
  • C.处理过程完全不涉及磁盘 I/O
  • D.一旦触发进程就崩溃退出

答案:A。缺页中断是内中断,在指令执行期间发现要访问的页不在内存就会触发;内核完成磁盘读入、页表更新后,重新执行被中断的指令。B 是普通外中断的处理时机,C 忽略了磁盘 I/O 这一主要耗时,D 混淆了缺页与致命错误。

Q6. 下列哪种情况会触发缺页?

单选题 · easy

  • A.访问的页不在内存且页表项无效← 正确答案
  • B.页表项有效但 TLB 未命中
  • C.内存条出现硬件故障
  • D.程序存在逻辑 bug

答案:A。页表项有效说明页面已在某页框中,即使 TLB 未命中,查页表就能找到;页表项无效才意味着页面在磁盘上,访问它必然触发缺页中断。C、D 属于硬件故障和程序错误,与缺页的正常机制无关。

Q7. 关于 FIFO 与 LRU 置换算法,正确的是?

多选题 · hard

  • A.FIFO 可能出现 Belady 异常← 正确答案
  • B.LRU 不会出现 Belady 异常← 正确答案
  • C.LRU 利用了程序的时间局部性← 正确答案
  • D.FIFO 的实现代价通常高于 LRU

答案:ABC。FIFO 按进入顺序换出,页框增多时可能缺页反而变多(Belady 异常);LRU 按最近使用时间换出,满足栈性质,不会出现该异常,且它的设计正是基于时间局部性。D 说反了:FIFO 用队列即可,实现代价远低于 LRU。

Q8. 工作集模型的核心思想是什么?

单选题 · mid

  • A.进程在时间窗口内访问的页面集合,分配的页框数不能少于它← 正确答案
  • B.把最常访问的页面永久锁在内存里
  • C.按进程大小等比例分配页框
  • D.让所有进程共享同一张页表

答案:A。工作集是进程在 Δ 时间窗口内实际访问的页面集合,它刻画了进程运转的「必需品」。只有保证分配给进程的页框数 ≥ 工作集大小,进程才不会频繁缺页。B 是内存锁定,C 是静态分配,D 违背了地址隔离原则。

Q9. Clock(时钟)置换算法中,指针扫描时遇到访问位为 1 的页会怎么做?

单选题 · hard

  • A.把访问位清 0 并继续扫描← 正确答案
  • B.立即将该页换出
  • C.停止扫描直接替换该页
  • D.触发一次缺页中断

答案:A。Clock 算法是 LRU 的工程近似:指针循环扫描,遇到访问位为 1 的页说明它最近被用过,先「给一次机会」——清零后跳过;遇到访问位为 0 的页才换出。这就像转盘绕圈,一圈之内没人碰过的盒子才会被扔掉。

Q10. 系统出现抖动(缺页率飙升、CPU 利用率骤降)时,可采取哪些措施?

多选题 · hard

  • A.降低多道程序设计度← 正确答案
  • B.按工作集大小补足页框← 正确答案
  • C.把 swap 分区扩大一倍
  • D.优化代码的访存局部性← 正确答案

答案:ABD。抖动的根源是活跃进程的工作集之和超过物理内存。降低多道度、按工作集补页框、优化局部性(让访问更集中)都能直接缓解。C 扩大 swap 只是多了个更大的冷库,搬运距离没变,缺页次数不减反可能增加,治标不治本。