
在任何现代计算机中,一个根本性的挑战是管理CPU主存的速度与二级存储的缓慢之间存在的巨大鸿沟。当系统耗尽高速物理内存时,操作系统必须决定将哪一块数据——即“页面”——淘汰出去,以便为新数据腾出空间。理想的选择是淘汰在最长时间内不会被用到的页面,这一原则被称为最佳算法,但这需要预测未来。一种更实用的方法是最近最少使用(LRU)算法,它回顾过去,但要完美实现它的成本高得令人望而却步。这就产生了一个关键的知识鸿沟:我们如何才能在没有LRU致命开销的情况下实现其效率?
本文深入探讨了第二次机会算法,即时钟算法,它是一个优雅且被广泛应用的解决方案。它在性能和实现成本之间取得了大师级的平衡。在接下来的章节中,你将对这个基础的操作系统概念有深刻的理解。“原理与机制”部分将剖析该算法如何使用一个简单的引用位工作,探索其使用“脏位”的更复杂变体,并分析其可能失效的条件。随后,“应用与跨学科联系”部分将揭示该算法非凡的通用性,展示其核心思想如何被应用于解决进程调度、NUMA系统等现代硬件、虚拟化乃至数据库管理中的问题。
想象你有一个小工作台,但要处理一个有数百个工具的大项目。你任何时候只能在工作台上放几个工具;其余的都在几步之遥的一个大工具箱里。每次你需要一个不在工作台上的工具时,你都必须停下手中的活,走到工具箱前,找到工具,再把它拿回来,并把另一个工具放回去以腾出空间。你问题的核心是:为了高效工作,你应该把哪个工具放回工具箱?
如果你能预见未来,答案会很简单:把你在最长时间内不会用到的那个工具放回去。这就是最佳页面置换算法(常被称为MIN或Bélády算法)的精髓,就像预知未来一样,它在真实的计算机系统中是一个诱人但无法实现的梦想。
计算机的“工作台”是其高速物理内存(DRAM),“工具箱”则是速度慢得多但容量大得多的存储设备,如SSD或HDD。“工具”就是页面——小块的、固定大小的数据块。当CPU需要一个不在物理内存中的页面时,就会发生缺页中断,操作系统(OS)必须从存储设备中获取它,这个过程比直接访问内存要慢数千倍。如果内存已满,操作系统必须首先选择一个“牺牲”页面来淘汰。
既然我们无法预测未来,我们便退而求其次,采用一个启发式方法:回顾过去。最近最少使用(LRU)算法正是这样做的。它淘汰最长时间未被访问的页面。这是对最佳算法的一个很好的替代,但要完美地实现它却出奇地困难。这需要系统为每个页面记录每次内存访问的精确时间,并不断对它们进行排序,这种开销会让任何现代计算机陷入停顿。这无异于剜肉补疮。
因此,操作系统设计者面临的挑战不是构建一个完美的系统,而是一个巧妙的系统。我们如何能创造出一个几乎和LRU一样好,但实现成本却低得多的算法呢?
于是,第二次机会算法,或更亲切地称为时钟算法,应运而生。它是操作系统中最优美和应用最广泛的思想之一,是实用性折衷的杰作。它不是用昂贵的时间戳来近似LRU,而是用一个微不足道的信息位和一个巧妙的机制。
想象一下,所有的物理页框都排成一个圆圈,就像时钟的表盘。一个指针,即“时钟指针”,指向其中一个页框。对于每个页面,硬件都会维护一个引用位(或R位)。每当一个页面被读取或写入时,它的R位就被设置为。
现在,发生了一次缺页中断,我们需要找一个牺牲页面。操作系统并不慌张,它只是查阅时钟。它查看指针所指的页框:
就是这样。这个检查、清除和前进的简单舞蹈就是整个算法。R位为的页面实际上是“热”的,可以留在内存中,尽管在这个过程中它失去了“热”的状态。R位为的页面是“冷”的,是第一个被淘汰的。该算法优雅地将页面仅分为两类——自上次检查后被使用过的和未被使用过的——而这种粗略的分类是近似LRU的一种非常有效且廉价的方式。
时钟算法的魔力完全在于引用位。如果这个信息来源丢失了会发生什么?通过探究算法如何失效,我们可以更好地欣赏它的设计。
想象一个教学实验,我们暂时禁用了硬件设置R位的功能。一次缺页中断发生了。时钟指针扫过,找到R位为的页面(在实验开始前设置的),尽职地将它们清零为并继续前进。最终,它找到了一个R位为的牺牲页面。但现在,在这个“抑制窗口”期间对页面的任何后续访问都不会将它们的R位重新设置回。时钟变得“盲目”了。在指针最多转过一整圈后,内存中的每一个页面的R位都将被设置为。
从这时起,发生缺页中断时会怎样?指针指向一个页框,发现其R位为,并立即淘汰它。下一次中断发生,指针在下一个页框,发现R位为,并淘汰它。该算法已经退化为一种简单的、机械的先进先出(FIFO)策略。它不再有任何关于使用模式的记忆;它只是按照页面碰巧所在的循环顺序淘汰它们。
这种退化不仅仅是假设的硬件故障。在某些工作负载下它也可能自然发生。考虑一个访问一长串唯一页面的引用序列,例如在一个有4个页框的系统上访问。在前4个页面填满内存后,第5个页面导致了缺页中断。时钟指针扫过,清除了页面和的R位。它绕回来淘汰了页面。然后页面发生缺页中断,指针发现页面的R位现在是,依此类推。因为没有页面在内存中被重新引用,所以没有R位会因为“命中”而被重新设置为。算法再次表现得与FIFO完全相同。
即使是系统级别的策略也可能破坏该算法。如果一个操作系统组件决定每次内存引用就全局重置所有R位,它可能会无意中破坏时钟。如果重置周期太短——特别是,短于页框的数量——那么一个页面在进入内存时设置的R位,会在时钟指针有机会检查它之前就被全局重置清除了。算法再次变得盲目,并退化为FIFO。这些场景揭示了一个深刻的真理:时钟算法本质上是FIFO,但带有一个由R位提供的豁免条款。
简单的时钟算法平等地对待所有页面,但实际上它们并非如此。有些页面是“干净”的(自从从存储中读出后没有被修改过),而另一些是“脏”的(被写入过)。淘汰一个干净的页面代价很小;操作系统可以直接覆盖它的页框。淘汰一个脏页面则代价昂贵;它的内容必须首先被写回磁盘或SSD以保存更改,这个操作以CPU的时间尺度来看,几乎是永恒。
这时,硬件提供的另一个位就派上用场了:修改位,或脏位(-bit),硬件在任何写操作时会将其设置为。一个更复杂的操作系统会使用增强型第二次机会算法,它同时考虑R位和D位。
这种增强建立了一个淘汰优先级的层次结构。页面现在可以被分为四类:
增强型算法通过多轮扫描来实现这个逻辑。在发生缺页中断时,时钟指针开始扫描:
因为第一轮扫描清除了所有最近使用页面的R位,所以第二轮扫描保证能找到一个牺牲品。这个优雅的修改保留了时钟算法的有界扫描时间,同时做出了更智能、更具成本意识的决策。
淘汰干净页面优先于脏页面的偏好似乎显而易见,但这种偏好应该有多强呢?如果最近的干净页面非常远,需要长时间扫描,而一个脏页面就在时钟指针下,该怎么办?答案很巧妙,它取决于你的硬件。
让我们为一次淘汰的总成本建模。成本是扫描所花的时间加上淘汰本身所花的时间。想象一个场景,最近的未引用干净页面()在个页框之外,而最近的未引用脏页面()仅在个页框之外。我们应该选择哪一个?
在配备慢速硬盘驱动器(HDD)的系统上: 对HDD的随机写入速度极其缓慢,主要受限于磁盘磁头的物理移动(寻道时间)和盘片旋转。假设这需要大约秒。扫描额外800个页框的时间相比之下微不足道(大约秒)。在这个世界里,一次脏页写入的成本是如此灾难性的高,以至于几乎总是值得扫描更远的距离来找到一个干净的页面进行淘汰。最佳策略是极不情愿地淘汰脏页。
在配备快速固态硬盘(SSD)的系统上: SSD没有移动部件。一次随机写入要快得多,也许大约在秒。现在,权衡看起来非常不同。淘汰附近脏页的成本(小的扫描成本+SSD写入成本)实际上可能低于长距离扫描以到达远处干净页面的成本。对于SSD系统来说,承受小的I/O冲击并淘汰脏页是更划算的。
这个有力的例子表明,没有单一的“最佳”算法。最优策略是软件逻辑与它所运行的硬件物理现实之间的动态舞蹈。
即使是增强型算法也有其怪癖。在内存压力高的系统中,当活跃使用的页面集仅比可用内存稍大时,可能会出现一种病态行为。进程快速地循环使用其页面,当指针转到某个页面时,它又刚刚被使用过,所以它的R位总是。这可能导致“振荡行为”,即时钟指针在几乎每次缺页中断时都必须执行一次完整的、无用的扫描,只是为了清除位,最终淘汰一个片刻之后就会被再次需要的页面。
为了解决这个问题,人们设计了更复杂的时钟。
这些改进展示了算法设计的迭代过程:识别一个弱点,并通过增加恰到好处的额外信息来克服它,同时努力保持使该算法之所以伟大的核心简洁性和效率。这段从单个位到计数器和多指针的旅程,本身就是计算机系统演化的一个缩影。
我们不要忘记,这些额外的信息并非没有代价。数百万甚至数十亿个页框的R位、D位和计数器必须存储在某个地方。对于拥有TB级物理内存的服务器来说,仅这些元数据就可能占用GB级的RAM,这是为了获取知识而付出的一个不可忽视的成本。在系统设计的世界里,每个决定都是一种权衡,而时钟算法的美妙之处就在于它驾驭这些权衡的优雅。
在掌握了第二次机会算法的优雅机制后,我们可能会倾向于将其归类为一个巧妙但狭隘的、针对特定问题的解决方案。但这样做无异于只见树木,不见森林。时钟算法真正的天才之处不仅在于其简洁性,更在于其深刻的适应性。如同宏大交响乐中的一个基本主旋律,其核心思想——基于单个历史位给予“第二次机会”——在各种惊人的不同情境中重现、演变并找到新的意义。让我们踏上一段旅程,从我们熟悉的操作系统领域,到现代硬件的前沿,甚至进入其他复杂软件的范畴,见证这个简单循环非凡的通用性。
我们的第一站是该算法的原生环境:操作系统内核。在这里,它所做的不仅仅是替换页面;它成为了系统资源的智能仲裁者,展现出一种近乎涌现出的智慧。
思考一下增强型第二次机会算法,它不仅使用引用位(),还使用修改位或“脏”位()。这个第二位标记了自加载到内存以来被写入过的页面。算法的策略被精炼了:它首先寻找一个既未被引用又干净()的牺牲品。只有在不存在这样的页面时,它才不情愿地考虑一个未被引用但脏的页面(),这种页面在被覆盖前必须先保存到磁盘——一个昂贵的操作。
这个简单的偏好带来了深远的影响。现代系统管理着不同类型的内存。一些页面,如应用程序堆的页面,是“匿名”的;如果被淘汰,它们必须被写入磁盘上的一个特殊交换文件。其他页面是“文件支持的”,是内存映射文件的一部分。这些页面只是磁盘上已有数据的副本。如果一个文件支持的页面在干净()时被淘汰,它可以被简单地丢弃;原始数据在文件系统中仍然是安全的。
增强型时钟算法,在没有明确了解这些页面类型的情况下,巧妙地驾驭了这种复杂情况。它自然地倾向于淘汰干净的、文件支持的页面,而不是脏的、匿名的页面。为什么?因为后者通常是程序“热”工作数据的一部分,频繁地被写入(设置)和引用(设置)。而来自文件的较冷的、只读的数据更有可能处于梦寐以求的()状态。该算法仅仅通过避免写入成本,最终就在内存中保留了最活跃和最关键的应用程序数据。
现实世界给我们带来了其他复杂情况。当一个页面根本不能被淘汰时会发生什么?这对于涉及输入/输出(I/O)操作的页面来说是一个常见场景。一个持有正在发送到网卡或磁盘驱动器的数据的缓冲区在内存中是“钉住”的;淘汰它将是灾难性的。时钟算法优雅地处理了这种情况。时钟指针只是简单地跳过任何被钉住的页框,就好像它们不存在一样。我们甚至可以对此行为进行概率建模以理解其性能影响。如果有一小部分()的页面被钉住,而未钉住的页面中有一小部分()最近被引用过,那么时钟指针平均必须检查个页框才能找到一个牺牲品。这个简单的公式揭示了I/O压力和内存访问模式如何共同决定页面置换的开销,将一个复杂的系统交互转化为一个可处理的分析。
到目前为止,我们一直将系统视为一个整体。但操作系统是一个充满竞争进程的繁华都市。这正是简单化全局策略的阴暗面可能出现的地方。想象一个全局时钟算法,其中单个时钟指针扫过所有物理内存,淘汰页面时不考虑其所属进程。
现在,考虑两个进程:一个大型、消耗CPU的进程和一个小型、不常运行的进程。由于持续运行,它不断接触其页面,确保它们的引用位几乎总是。当最终需要一个新页面时,全局时钟指针开始扫描。它遇到一个又一个属于的页面,发现它们的R位是,尽职地将它们清零,然后继续前进。但在指针转完一整圈之前,再次运行,将其所有R位重新设置回!与此同时,属于那个小型的、休眠的进程的页面没有被接触过。它们在之前扫描中被清除的R位仍然是。它们成为大型进程缺页中断时完美、毫无防备的牺牲品。结果是典型的饥饿案例:不断地换页,持续丢失其页面,而则独占了内存。
解决办法和问题本身一样优雅:放弃全局策略。通过给每个进程自己的页框集和自己的局部时钟实例,我们建立了防火墙。中的缺页中断现在只触发对其自身页面的扫描。进程是安全的,其小工作集免受其咄咄逼人邻居的内存需求的影响。这阐明了一个深刻的系统设计原则:公平性通常需要明确的划分和隔离。
数字世界不是静止的;我们软件脚下的根基——硬件——在不断变化。时钟算法的韧性最好地体现在它适应这些新架构范式的能力上。
考虑非统一内存访问(NUMA)系统的挑战。在这些高性能机器中,内存分布在多个节点上。访问与CPU在同一节点上的本地内存速度快,而访问另一节点上的远程内存则明显更慢。单一的全局时钟将是灾难性的,仅为了检查引用位就会产生一场缓慢的跨节点流量风暴。
解决方案是什么?我们呼应了从进程公平性中学到的教训:划分和本地化。每个NUMA节点运行自己的时钟指针,首先扫描其本地内存。只有当本地内存压力极大且找不到本地牺牲品时,系统才不情愿地跨越互连去从远程节点“窃取”一个页面。我们甚至可以创建复杂的策略,例如要求远程页面在成为淘汰候选者之前必须在更长的时间内未被引用,从而进一步使系统偏向于局部性。时钟算法从一个简单的圆环转变为一个联邦式的多层环形结构,镜像了它所管理的机器的物理结构。
硬件革命不止于布局;内存介质本身也在改变。我们正在从磁性硬盘转向非易失性内存(NVM),如固态硬盘(SSD)。NVM提供了显著更快的写入速度,但代价是:每个内存单元都有有限的写入耐久性。这完全改变了页面置换的计算方式。
对于磁盘,淘汰一个脏页面主要是因为其高时间延迟而代价高昂。对于NVM,时间成本要低得多,但出现了一个新的“磨损”成本。这是否意味着我们应该放弃对干净页面的偏好?完全不是!这种偏好仍然有效,但原因已从最小化时间转变为最大化设备寿命。针对NVM的理想策略会减弱但不会消除对脏页的偏见。它承认写入更快但仍然不可取。它必须在即时性能增益与长期磨损成本之间取得平衡,保留引用位的主要重要性,同时注意硬件施加的新约束。
该算法的适应性甚至延伸到纯粹的抽象领域,在这些领域中,“内存”和“页面”的概念本身变得分层和复杂。
欢迎来到虚拟化的世界。在这里,一个“客户机”操作系统在虚拟机内部运行,认为它拥有自己的物理硬件。实际上,其下的一个“虚拟机监控程序”(hypervisor)层管理着真实的硬件。在现代硬件支持下,这涉及到嵌套分页:客户机操作系统将虚拟地址转换为“客户机物理”地址,然后虚拟机监控程序再将这些地址转换为真实的“主机物理”地址。
令人惊讶的是,客户机操作系统和虚拟机监控程序都可以同时运行它们自己的时钟算法!客户机扫描其页表来管理其“物理”内存,而虚拟机监控程序则扫描嵌套页表来管理实际的机器内存。这就创造了一个“钟中之钟”。这两个层次是独立的,但并非互不知晓。一个精明的虚拟机监控程序可以通过“偷看”客户机的引用位来提高性能。如果虚拟机监控程序看到客户机认为一个页面是“热”的(客户机),即使它自己的嵌套引用位碰巧是,它也可以在自己的算法中偏向于不淘汰该页面。这种合作策略,在不每次访问都捕获客户机的情况下成为可能,展示了简单的时钟机制如何可以分层组合,以管理极其复杂的虚拟化环境。
另一层抽象是内存中压缩。为了避免缓慢的磁盘I/O,操作系统可能会选择不淘汰一个页面,而是将其压缩并保存在一个特殊的RAM池中。一个被压缩的页面不能直接访问,也没有硬件引用位。时钟算法如何处理这个问题?它必须适应。“引用”事件不再是硬件位的翻转,而是一个触发解压缩的缺页中断。操作系统可以捕获此事件并设置一个软件引用位。通过这样做,它无缝地将这些压缩页面整合到其现有的基于近时性的淘汰逻辑中。一个刚刚被解压缩的页面被正确地视为最近使用过并给予第二次机会,这表明该算法即使在其核心硬件支持被移除时也能茁壮成长。
也许证明时钟算法强大功能的最有力证据是它在操作系统之外的出现。例如,数据库管理系统(DBMS)面临着几乎相同的问题:它们在内存中管理一个大型缓冲池,以缓存包含表和索引的磁盘页面。它们中的许多系统也使用类似时钟的算法来决定在缓冲区满时淘汰哪个页面。背景不同——事务而非进程,缓冲帧而非页框——但高效近似LRU这个基本问题是相同的,解决方案也是如此。
这段旅程甚至可以带我们到系统与安全的交叉点。人们可能会想象调整增强型时钟算法以增强安全性,例如,通过优先保留脏的可执行页面在内存中,将它们视为自修改代码的潜在迹象。然而,这是一个关于理解问题真正本质重要性的绝佳教训。虽然该策略确实改变了淘汰行为,但它对于阻止现代代码重用攻击(如ROP)几乎无能为力,因为这些攻击并不修改代码,而是链接现有的、未修改的代码片段。提议的安全变体是一个正在寻找正确问题的解决方案。这是一个强有力的提醒:即使是最优雅的算法,其优劣也取决于它所操作的世界模型。
从内核的核心职责到硬件的前沿和虚拟化的抽象层次,第二次机会算法一次又一次地证明了它的价值。它的故事是进化和适应的故事,证明了一个简单、优美思想的持久力量。它告诉我们,在计算世界中,如同在生活中一样,有时最有效的策略就是简单地给事物第二次机会。