try ai
科普
编辑
分享
反馈
  • 缺页

缺页

SciencePedia玻尔百科
核心要点
  • 缺页并非错误,而是一种预先设计的硬件异常,它使操作系统能够按需管理虚拟内存。
  • 区分快速的次要缺页(仅涉及内存)和缓慢的主要缺页(涉及磁盘 I/O)对于理解系统性能至关重要。
  • 操作系统的核心功能,如按需分页、写时复制(Copy-on-Write, CoW)和内存映射文件,都构建在缺页机制之上。
  • 如果进程的总内存需求超过物理内存,导致过多的缺页,系统性能可能会严重下降,陷入颠簸状态。

引言

在现代计算中,每个程序都在一种宏大的幻觉下运行,仿佛拥有一个广阔、私有的内存空间,其大小远超可用的物理内存(RAM)。这一奇迹被称为虚拟内存,是多任务处理和资源管理的基础。但它也引出了一个关键问题:操作系统如何在不付出灾难性性能代价的情况下维持这种幻觉?答案在于一种复杂、预先设计好的中断,称为​​缺页(page fault)​​。本文将揭开这一核心概念的神秘面纱。在第一章“原理与机制”中,我们将剖析缺页的工作机制,从硬件陷阱到操作系统的响应,并区分快速的次要缺页和缓慢的主要缺页。随后的“应用与跨学科联系”一章将探讨这一机制如何支撑着从数据库性能、实时系统到计算机安全等方方面面,揭示缺页作为系统设计的基石所扮演的角色。

原理与机制

想象一下,你正在阅读一本有数百万页的巨著。你坐在一张小书桌前,桌上一次只能放几十页。你该如何阅读这本书?你当然不会试图把所有书页一次性都堆在桌上。相反,你会把整本书放在旁边的书架上。当需要阅读某一页时,你从书架上找到它,拿到桌上,并通过放回一页读完的书页来腾出空间。如果你的书桌是计算机的物理内存(RAM),而书架上的书是一个正在运行程序的广阔地址空间,那么你就已经直观地理解了虚拟内存的本质。意识到所需页面不在桌上并从书架上取回它的这个行为,原则上就是一次​​缺页​​。

宏大的幻觉与陷阱门

现代操作系统上演了一场精彩的魔术。它为每个程序呈现一个巨大、私有且完全连续的地址空间。程序可能认为自己独占了数 GB 的内存,这些内存整齐地、不间断地排列着。但这只是一个美丽的谎言。现实情况是,物理内存(RAM)是一种稀缺、碎片化的资源,被许多程序混乱地共享着。

这种幻觉是如何维持的呢?操作系统与 CPU 的​​内存管理单元(MMU)​​协同工作。它们将程序的虚拟地址空间分割成固定大小的块,称为​​页(page)​​,并将物理内存分割成相应的块,称为​​帧(frame)​​。MMU 使用一组称为​​页表(page table)​​的映射表,将程序使用的虚拟地址转换为 RAM 中帧的物理地址。

但巧妙之处在于:操作系统从一开始并不会将每一个虚拟页都映射到物理帧。为什么要将宝贵的 RAM 浪费在程序中可能永远不会被使用的部分上呢?这就是​​惰性分配(lazy allocation)​​的原则。想象一个程序分配了一个 200 MiB 的巨大数组,但只是稀疏地写入数据,比如每 64 KiB 写入一次。为一个大部分为空的数组分配 200 MiB 的物理内存将是极大的浪费。相反,操作系统在页表中留下“空洞”,将未被触及的页面标记为​​不存在(not present)​​。虚拟上的连续性仅仅是账本中的一个条目;物理现实中则几乎空无一物。

那么,当程序试图访问这些未映射的“空洞”页面中的一个地址时会发生什么呢?MMU 查看页表,找到“不存在”的标记,然后发现自己陷入了困境。它无法完成地址转换。但它并不会崩溃,而是做了一件非常聪明的事:它触发一种特殊的硬件异常,一道将控制权从程序转移到操作系统的陷阱门。这个陷阱就是​​缺页​​。缺页不是一个错误,而是一个基础的、预先设计好的机制。它相当于硬件在告诉操作系统:“我无法再独立维持这个幻觉了。现在轮到你介入,让它成为现实。”

作为舞台监督的内核

当发生缺页时,操作系统内核被唤醒。它就像戏剧中的舞台监督,在演员需要某个道具之前冲上台把它放好。内核检查缺页情况以了解状况,并决定采取适当的行动。大多数缺页可分为两大类:轻触式和重负荷式。

轻触式:次要缺页与惰性分配

假设一个程序首次接触一个页面,这会触发一次缺页。操作系统发现这是一个属于该程序的有效匿名内存区域,但尚未为其分配物理帧。修复过程很简单,而且关键的是,它不涉及缓慢的磁盘访问:

  1. 从它维护的空闲物理帧列表中取出一个。
  2. 为安全起见,防止程序看到该帧前一个用户留下的数据,操作系统会用零填充来清理它。
  3. 更新页表条目,将虚拟页映射到这个新准备好的帧,并将其标记为“存在”且可写。
  4. 将控制权返还给程序,程序会重新执行失败的指令。这一次,MMU 会找到一个有效的映射,无缝内存访问的幻觉得以恢复。

这整个序列被称为​​次要缺页(minor fault)​​或​​软缺页(soft fault)​​。它在 CPU 和 RAM 内部被快速解决,耗时仅为微秒级别。与预先分配并清零所有内存相比,这种“按需清零”的方法效率极高,因为如果程序稀疏地使用其内存,预先分配将是巨大的浪费。这种惰性策略在具有非统一内存访问(NUMA)架构的多处理器系统上还有一个意想不到的好处,因为它能确保内存在物理上分配到最接近实际需要它的 CPU 核心的 RAM 上,从而改善了局部性。

这也是一项奇妙优化背后的机制,该优化涉及一个特殊的、共享的、填满零的只读页面。当一个程序首次尝试从一个新的匿名页面读取数据时,操作系统甚至不需要分配一个新的帧;它可以直接将发生缺页的虚拟页映射到这个通用的零页面。只有当程序稍后尝试写入该页面时,才会发生另一种缺页——保护性缺页——促使操作系统最终创建一个私有的、可写的副本。这是​​写时复制(Copy-On-Write, COW)​​的一个例子,是另一个构建在缺页机制之上的强大技术。

重负荷式:主要缺页与分页的代价

但是,当舞台监督后台的道具用完时会发生什么?如果没有空闲的物理帧怎么办?为了腾出空间,操作系统必须选择一个正在使用的帧,将其内容换出,然后将该帧交给发生缺页的进程。这被称为​​页面置换(page replacement)​​。

如果被置换的页面自加载以来未被修改过(即它是“干净的”),操作系统可以直接丢弃其内容。但如果页面已被写入(即它是“脏的”,这一状态由页表条目中的dirty位跟踪),操作系统必须首先将其内容保存到硬盘或 SSD 上的一个称为​​交换空间(swap space)​​的特殊区域。这是一个​​换出(swap-out)​​操作。

现在,考虑相反的情况。一个程序试图访问它不久前使用过的一个页面,但该页面已被操作系统换出到交换空间。MMU 会发现该页面被标记为“不存在”并触发一次缺页。操作系统检查其内部记录,发现该页面的数据正在磁盘上等待。此时,它必须执行“重负荷”操作:

  1. 启动一个缓慢的磁盘 I/O 操作,将页面从交换空间读回到一个物理帧中(这个物理帧本身可能也是通过换出另一个页面而获得的)。
  2. 等待 I/O 操作完成。
  3. 更新页表,将虚拟页映射到现在已驻留内存的帧。
  4. 将控制权返还给程序。

这就是​​主要缺页(major fault)​​或​​硬缺页(hard fault)​​。与次要缺页的关键区别在于需要​​磁盘 I/O​​。次要缺页耗时微秒,而主要缺页则耗时毫秒——长达数千倍。这才是按需分页的真正“惩罚”。

其性能影响是惊人的。我们可以将内存的​​有效访问时间(EAT)​​建模为一个加权平均值。如果一次正常的内存访问耗时(比如)80 纳秒,而单次缺页的代价(包括操作系统开销和磁盘 I/O)约为 30 毫秒,那么即使缺页概率极小,仅为百万分之一(p=10−6p = 10^{-6}p=10−6),也能显著拖慢平均访问时间。你的程序闪电般的计算突然被中断,等待着机械硬盘旋转或固态硬盘响应。

EAT=(1−p)×tmem+p×tfault\text{EAT} = (1-p) \times t_{\text{mem}} + p \times t_{\text{fault}}EAT=(1−p)×tmem​+p×tfault​

这个方程式决定了虚拟内存系统的健康状况。保持 ppp 值低,幻觉就完美无瑕。让 ppp 值攀升,幻觉就开始滞后和卡顿。

遗忘的艺术,以及幻觉破灭之时

决定置换哪个页面至关重要。一个简单且看似公平的策略是​​先进先出(First-In, First-Out, FIFO)​​:置换在内存中停留时间最长的页面。然而,这个简单的规则可能导致一个被称为​​Belady 异常(Belady's Anomaly)​​的奇异且违反直觉的结果。可以构造出这样一种内存引用序列:给一个程序更多的物理内存帧,反而导致它遭受更多的缺页。这个优美的悖论表明,在资源管理的复杂博弈中,简单的直觉可能是不可靠的。更复杂的算法,如​​最近最少使用(Least Recently Used, LRU)​​,即置换最长时间未被访问的页面,表现更好,但实现起来更复杂。

当操作系统做出糟糕的置换决策时,或者当所有运行进程的内存需求总和——它们的总​​工作集(working set)​​——远超可用物理内存时,系统可能进入一种称为​​颠簸(thrashing)​​的死亡螺旋。在这种状态下,一个进程发生缺页,通过置换另一个页面来调入一个新页面。但那个被置换的页面立即又被另一个进程(甚至同一个进程)需要,从而引发另一次缺页,这次缺页又置换了另一个需要的页面,如此循环往复。系统几乎所有的时间都在执行 I/O,在磁盘和内存之间来回交换页面,而几乎没有进行有用的计算。缺页率飙升,机器运行停滞。颠簸是虚拟内存幻觉的最终破灭,支撑这个魔术的机制本身成了整场表演。即使是善意的优化,如​​预取(prefetching)​​(在页面被明确请求前加载它们),如果预测不准,用无用的页面污染了内存,也可能引发颠簸。

统一视图:缺页、未命中与抽象

将缺页置于内存层级结构的正确位置至关重要。学生们常常混淆三个不同的事件:

  • ​​缓存未命中(Cache Miss):​​ 最快的事件。CPU 需要的数据不在其超高速的硬件缓存中。这完全由硬件处理,硬件会从较慢的主内存(RAM)中获取数据。此过程不涉及操作系统。
  • ​​TLB 未命中(TLB Miss):​​ 下一个层次。MMU 需要翻译一个虚拟地址,但该翻译信息不在其特殊缓存,即​​转译后备缓冲器(Translation Lookaside Buffer, TLB)​​中。这通常也由硬件处理,硬件在主内存中执行一次“页表遍历”来找到正确的条目。如果条目有效,这不会导致缺页。
  • ​​缺页(Page Fault):​​ 最慢的事件。发生 TLB 未命中,硬件遍历页表,并发现一个无效条目(例如,“不存在”)。只有到这时,硬件才会陷入(trap)操作系统。

冷数据缓存会增加延迟,但不会导致缺页。热 TLB(得益于地址空间标识符或 ASID 等特性)减少了地址翻译的时间,但如果底层的页表条目无效,也无法阻止缺页的发生。它们是实现同一个基本目标的不同层次:尽快将正确的数据送到 CPU。

最后,我们讨论的原理具有惊人的普遍性。虽然 x86-64 处理器通过栈上的错误码和控制寄存器(CR2CR2CR2)中的故障地址来报告缺页,而 ARM 处理器则在其异常综合寄存器(ESR_EL1ESR\_EL1ESR_EL1)中编码类似的信息,但抽象信息是相同的:哪个地址导致了缺页,是“不存在”缺页还是“保护”缺页?操作系统在这些特定于硬件的行为之上构建了一个抽象层,使其能够在任何现代机器上实现按需分页和写时复制等通用概念。

因此,缺页并非一个简单的错误。它是现代计算的关键,一个优美而复杂的机制,支撑着无限、私有内存的宏大幻觉。它是硬件与软件之间的对话,是程序的逻辑世界与机器物理约束之间的桥梁。

应用与跨学科联系

在我们迄今为止的旅程中,我们已经看到缺页并非简单的错误,而是一种复杂的机制——操作系统为自己设下的一个优雅陷阱。这个陷阱并非失败的标志;它是一个拦截点,一个让操作系统可以暂停正常执行流程并执行非凡巧妙操作的时刻,而这一切都对运行中的程序隐藏。它是一个基础工具,使计算机能够伪装、优化和管理资源,其效率在没有它时是无法想象的。

现在,让我们探索这个美妙的机制将我们引向何方。我们将看到这一个思想如何演变成丰富多彩的应用,贯穿现代计算的方方面面,从操作系统和数据库的设计,到安全和高性能计算的前沿。

作为魔术师的操作系统:核心应用

从本质上讲,缺页机制让操作系统得以成为一名魔术大师。它为每个程序创造了一个虚拟世界,这个世界远比底层硬件的严酷物理现实更宏伟、更方便。

想象一下,你正在编写一个程序来处理一个庞大的数据集——比如一个巨大的数组,其大小是计算机物理内存的好几倍。没有虚拟内存,这是不可能的。但是通过按需分页,操作系统只加载你的程序在任何特定时刻需要的那一小部分数组——即那些页面。当你的程序踏入数组中尚未加载到内存的区域时,就会触发一次缺页。操作系统会迅速介入,从硬盘中获取所需的页面,将其放入一个空闲的内存帧中,然后恢复程序,仿佛什么都没发生过一样。这使你能够处理几乎无限大小的数据集。

当然,天下没有免费的午餐。如果你的程序访问模式混乱,或者试图在一个远大于内存的数组上进行紧密循环扫描,就可能导致一种被称为颠簸的状况。系统将所有时间都花在将页面换入换出内存上,硬盘不停地运转,而几乎没有完成任何有用的工作。例如,对一个比内存大 KKK 倍的数组进行简单的顺序扫描,必然会导致与数组总大小成正比的缺页次数,因为每个页面都必须至少从磁盘调入一次。这揭示了一个根本性的权衡:按需分页给了我们无限内存的幻觉,但当我们把这种幻觉推向极致时,就必须付出 I/O 延迟的代价。

这种“按需加载”的原则仅仅是个开始。考虑当一个程序创建自身的副本时会发生什么——这在类 UNIX 系统中是一个常见的操作,称为 [fork()](/sciencepedia/feynman/keyword/fork()|lang=zh-CN|style=Feynman)。一个天真的方法是为新的子进程复制父进程内存中的每一个页面。对于一个大型程序来说,这将是极其缓慢和浪费的。取而代之的是,操作系统使用了一种名为​​写时复制(Copy-on-Write, CoW)​​的绝妙优化。

最初,父进程和子进程被赋予访问相同物理页面的权限,但操作系统将它们全部标记为只读。两个进程和平地共享一切。但当其中一个试图写入某个页面时,砰的一声——发生了一次缺页!因为该页面是写保护的。操作系统捕获到这个缺页,并且只在此时才为进行写入操作的进程创建该页面的一个私有副本。这种惰性复制确保了只有在绝对必要时才进行工作。预期的缺页次数,也就是复制量,与两个进程的内存内容随时间推移的实际分歧程度直接相关。

这种通过缺页统一不同概念的主题在​​内存映射文件(memory-mapped files)​​中得以延续。程序可以请求操作系统将一个文件直接映射到其虚拟地址空间中。这看起来就好像整个文件是内存中的一个巨大数组。当程序首次触及这个“数组”的一部分时,会触发一次缺页,操作系统便尽职地从磁盘读取文件的相应部分。这优雅地模糊了文件 I/O 和内存访问之间的界限。对于像网络服务器这样为大型文件提供服务的应用来说,这是一个强大的工具。服务器重启后,其缓存是“冷的”,对热门文件的首次请求会引发一场缺页风暴,拖慢一切。一个聪明的管理员可以通过主动告知操作系统哪些文件将被需要来“预热”服务器,使其将这些文件预取到内存中,从而避免在真实流量到来时出现性能冲击。

与应用的共生:在基础上构建

缺页机制是如此基础,以至于高性能应用常常被专门设计来与之和谐共处。这是一种“机械共鸣”——将软件调整到与底层机器的自然节奏相符。

一个绝佳的例子来自数据库世界。大型数据库通常使用树状数据结构,如 B 树,来索引磁盘上的数据。一次查询可能涉及从树的根节点遍历到叶节点的路径。这条路径上的每个节点可能位于磁盘上的不同页面。要读取单个节点,数据库可能需要承受一次缺页。一个关键的设计问题是:B 树节点的最佳大小是多少?分析得出了一个异常简单的答案:理想的节点大小与系统的页面大小相同。通过将应用的数据单元(节点)与操作系统的 I/O 单元(页面)相匹配,数据库确保了一次缺页恰好带来一个有用的节点——不多也不少。这最大限度地减少了昂贵的 I/O 操作次数,是数据库性能工程的基石之一。

这种互动也可以更加微妙。在高性能数据管道中,如处理实时视频流,开发人员使用内存映射 I/O 来实现“零拷贝”数据传输。视频采集设备可以通过直接内存访问(DMA)将帧直接写入内存,而应用程序可以从同一内存区域处理它们。即便在这里,缺页也扮演着角色。当应用程序首次触及新帧的一个页面时,可能会导致一次次要缺页——这种缺页不需要磁盘 I/O,但仍涉及操作系统更新页表。虽然比主要缺页快得多,但这些微小延迟的总和可能会给处理时间带来不可预测的“抖动”,这对实时视频来说是致命的。为了解决这个问题,工程师使用像 mlock() 这样的系统调用来将视频缓冲区锁定在内存中并进行预缺页处理,预先支付这一小部分成本,以确保后续平滑、确定性的性能。

硬币的另一面:当缺页被禁止时

尽管缺页有诸多好处,但其固有的不可预测性——你不知道它何时会发生,也不知道处理它究竟需要多长时间——使其在某些领域成为一种负担。

在​​硬实时系统​​中,如飞机的飞行控制计算机、医疗设备或装配线上的机械臂,错过截止时间不仅仅是性能问题,而是灾难性故障。一个任务可能有 5 毫秒的截止时间,但单次主要缺页就可能使其停滞 8 毫秒或更长时间以等待磁盘。这一个事件就会导致系统无法兑现其保证。因此,真正的实时操作系统(RTOS)要么完全禁用按需分页,要么要求所有时间关键型任务的代码和数据在执行前必须被明确锁定在物理内存中。在这个世界里,可预测性至上,而缺页是必须被驱逐的流氓元素。

危险也可能更微妙,出现在内存管理和并发的交叉点。考虑一个多核处理器,其中多个线程试图获取一个锁以进入代码的临界区。一种常见的高性能实现是*自旋锁(spinlock)*,等待的线程在一个紧凑的循环中忙等待,反复检查锁。现在,想象当前持有锁的线程遭受了一次缺页。它被操作系统取消调度并进入休眠状态,等待磁盘。但在其他 CPU 核心上自旋的其他线程并不知道这一点。它们继续以 100% 的利用率消耗 CPU 周期,用检查锁的流量冲击内存系统,而锁的持有者却无法取得进展。一次长延迟的缺页实际上冻结了一台强大的多核机器的大部分功能。这揭示了一种危险的涌现行为,并告诉我们,锁机制的设计必须考虑到可能在其中发生的操作系统事件。

重新构想缺页:现代与前沿探索

缺页的故事远未结束。其作为一种拦截机制的基本性质使其能够被重新用于应对计算机体系结构和安全前沿的新的、惊人的挑战。

最令人兴奋的发展之一是在​​异构计算(heterogeneous computing)​​领域,其中系统将传统的 CPU 与强大的加速器(如图形处理单元 GPU)结合起来。在一个统一虚拟内存(UVM)系统中,CPU 和 GPU 共享一个单一的虚拟地址空间。但是,当 CPU 需要的数据当前只存在于 GPU 的私有显存(VRAM)中时会发生什么?缺页!CPU 的访问触发了一次缺页,但这次缺页处理程序不是从磁盘读取,而是协调一次从 GPU 内存到 CPU 主内存的高速 DMA 传输。这是对经典机制的惊人再利用:缺页现在充当了在系统中不同处理器之间进行数据迁移的触发器。

这个概念也像俄罗斯套娃一样,层层出现在​​虚拟化(virtualization)​​中。当你在虚拟机中运行一个客户操作系统时,存在多层内存转换。客户机中的程序可能发生缺页,由客户操作系统处理。但虚拟机管理程序(hypervisor,运行虚拟机的软件)也有自己的页表(在 Intel 硬件上称为扩展页表或 EPT),用于将客户机的“物理”内存映射到主机的实际物理内存。一次访问在客户机中可能有效,但违反了虚拟机管理程序的规则,从而触发一次 EPT 违例——这本质上是另一种缺页。此外,虚拟机管理程序本身可能正在使用写时复制来管理客户机的内存。要理清性能问题的根本原因,需要对所有三个层面——客户操作系统、虚拟机管理程序和主机操作系统——的事件进行检测和关联。

也许最令人惊讶和深刻的联系是与​​计算机安全​​。能够精确测量程序执行时间的攻击者有可能获知其秘密。想象一个函数访问一个数组,直到一个秘密索引 s。如果数组足够大,跨越了不在内存中的页面,那么该函数的总运行时间大部分会很平滑,但每当 s 跨越页面边界时,就会出现一次大的跳跃,触发一次缺页。持有秒表的攻击者可以“听到”磁盘访问的特有延迟,并通过观察这些跳跃发生的时间来推断出秘密 s 的值。原本设计为性能优化的机制,变成了一个​​时间侧信道(timing side-channel)​​,泄露了信息。这一发现表明,我们机器的物理现实,即使在操作系统内存管理的层面上,也具有深刻且往往不明显的安全影响。

从一个管理内存的简单技巧,到现代系统的基石,再到安全漏洞的载体,缺页证明了计算的丰富性和复杂性。它提醒我们,在计算机科学的世界里,最强大的思想往往是那些创造了一个间接点、一个抽象结构中的接缝的思想,我们可以在那里进行干预并改变游戏规则。