
对于大多数用户而言,计算机内存是数据的静态存储库。然而,这种稳定性是一种精心管理的幻象。在表象之下,操作系统通过一个称为页迁移的过程不断地重新排列数据,以优化性能、增强弹性并实现高级抽象。本文揭开了这支隐藏之舞的帷幕,旨在弥合人们对内存简单性的认知与内存管理复杂现实之间的差距。读者将首先探索核心的“原理与机制”,理解内存规整和 NUMA 优化等基本驱动力。随后,“应用与跨学科联系”一章将揭示这一技术如何驱动从云计算的实时迁移到 CPU 与 GPU 之间无缝协作等关键技术。
在计算机用户看来,内存如同一片平静、稳定的广阔空间——一个巨大的图书馆,数据静静地躺在书架上,等待被读取。但这是一种巧妙营造的幻象。在幕后,操作系统(OS)是一位不知疲倦的园丁,不断地照料着物理内存的景观。它移动、重排、迁移数据,并非出于心血来潮,而是为了不懈地优化系统性能。这种将数据从一个物理位置移动到另一个的动态过程,便是页迁移。
从本质上讲,页迁移由两个基本需求驱动,我们将探讨这两大原则。其一是与混乱的斗争:需要为不可避免的内存碎片化带来秩序,这个过程称为内存规整。其二是与现代硬件中距离的暴政作斗争:需要将数据放置在靠近使用它的处理器附近,这一目标称为 NUMA 优化。让我们层层剥茧,探寻支配这支隐藏之舞的美妙逻辑。
想象一条有许多停车位的长街。随着时间的推移,各种大小的汽车来来去去,留下一片零散的空位。你可能总共有足够的空余空间停放一辆大巴士,但如果没有任何一个单独的空位足够长,这辆巴士就无处可停。这就是外部碎片,也是操作系统长期以来的一个头疼问题。内存变成了一块由已分配“页”和空闲“帧”组成的补丁拼布,即使总的空闲内存很充裕,一个请求大块连续内存的申请也可能失败。
页迁移是操作系统的解决方案。通过玩一场复杂的俄罗斯方块游戏,它可以将已分配的可移动页移到一起,从而将零散的小空闲帧整合成一个大的、可用的块。这个过程称为规整。
但如果有些车被“焊”在了地面上呢?在真实系统中,一些内存页是不可移动的或被钉住的。这可能由多种原因造成,但常见的一种是,某个硬件(如网卡或存储控制器)被配置为直接访问那个特定的物理地址——这种技术被称为直接内存访问(DMA)。内核本身也有复杂的数据结构,例如由 slab 分配器管理的数据结构,它们可能在设计上就不是用来移动的。
这些不可移动的页就像我们内存景观中不可撼动的巨石,将内存分割成更小的区域,从根本上限制了规整的能力。考虑这样一个场景:操作系统需要创建一个 5 个空闲页的连续块。它总共有 6 个空闲页,所以看起来是可能的。然而,如果来自内核 slab 分配器的不可移动页充当了屏障,那么操作系统能形成的最大连续空闲块可能小于所需的 5 页。规整只能在这些屏障所定义的段内进行。如果最大的段只有 4 页,那么请求 5 页就会失败,这是由被钉住的页导致碎片化问题无法克服的直接后果。就这样,一个内核分配器的内部工作方式可能对系统服务大内存请求的能力产生深远的外部影响。只有当这些对象可以被安全迁移时,才有可能将所有 6 个空闲页整合成一个单独的块。
这揭示了一个关键的权衡。规整功能强大,但并非没有代价。移动页会消耗 CPU 时间和内存带宽。操作系统必须足够智能,决定何时进行规整是值得的,以及移动哪些页。理想的选择是移动那些对运行中程序干扰最小的页。
但操作系统如何量化“干扰”呢?一个巧妙的方法是利用概率对问题进行建模。想象一下,操作系统需要为一个大页清理一个 4 帧的区域。它有几个候选区域,每个区域都被几个页占据。为了最小化干扰,它应该选择其驻留页在不久的将来最不可能被程序访问的区域。我们可以通过考察一个页被访问的频率,即它的“热度”,来对此建模。一个页的访问模式通常可以被建模为一个泊松过程,其中引用的平均到达率为 。由此,我们可以计算出在时长为 的短暂迁移窗口内,一个页被访问(从而成为“热”页)的概率。这个概率是 。
迁移单个页的预期干扰成本可以定义为一个固定拷贝时间加上一个惩罚项的组合,如果该页是热页,惩罚项会大得多。通过计算候选区域中每个需要移动的页的预期成本并求和,操作系统可以做出一个有依据的、量化的决策。它将选择清理总预期干扰最低的区域,从而优雅地在对连续内存的需求与迁移本身的性能成本之间取得平衡。
在一台简单的计算机中,所有内存与处理器的距离都是相等的。但现代高性能服务器更像一个拥有多个厨师工位(处理器插槽)的大型专业厨房。每个工位都有自己的本地冰箱(本地内存节点),但厨房另一侧也有冰箱(远程内存节点)。从本地冰箱取食材很快,比如 纳秒。穿过厨房去远程冰箱则慢得多,可能要 纳秒。这种架构被称为非一致性内存访问(NUMA)。
为了获得最佳性能,在一个插槽中的核心上运行的线程,其数据应该位于该插槽的本地内存中。但如果数据是由另一个插槽上的线程创建的呢?操作系统现在面临着“NUMA 不平衡”:一个线程不断地进行缓慢、昂贵的跨厨房之旅。页迁移就是答案:操作系统可以将页从远程内存节点物理移动到本地节点。
这就提出了一个关键问题:操作系统如何知道一个线程在远程访问上浪费时间?它必须扮演侦探的角色。现代处理器为此提供了一个强大的工具:性能监控单元(PMU)。PMU 是硬件计数器,可以跟踪极其具体的事件,例如一次内存访问是由本地还是远程 DRAM 满足的。
一个用于检测 NUMA 不平衡的稳健系统是精心设计的典范。首先,为了获得清晰的信号,操作系统必须通过将线程钉在一个插槽的核心上来确保其位置固定。然后,在一个小的时间窗口内,它使用 PMU 统计本地内存访问次数()和远程访问次数()。迁移的决策并非基于简单的一次性检查。为了避免对噪声或瞬时行为做出反应,系统采用了一个多部分规则:
只有当所有这些条件都满足时,操作系统才会触发页迁移,确信它正在解决一个真实且持续的性能问题。
当然,跨越插槽间互连“高速公路”的旅程是有成本的。这个成本不仅是延迟,还有带宽。迁移流量与应用程序自身的数据流量争夺这一有限的资源。迁移单个页的总流量不仅仅是页的数据。它包括协议开销,以及至关重要的缓存一致性消息。如果页中的某一行存在于源插槽的缓存中,它必须被作废,这会产生额外的流量。通过对所有这些组件进行建模,我们可以看到,高频率的页迁移可能会消耗互连容量的很大一部分,从而可能减慢它本想帮助的应用程序。操作系统再一次必须进行微妙的平衡。
处理器的缓存架构使决策进一步复杂化。例如,在写回策略下,处理器可以本地修改一个缓存行而无需立即写入内存。这会产生“脏”行。当迁移一个页时,这些脏行会产生额外的转发惩罚,因为最新的版本必须从缓存中检索,而不是主存。相反,写通缓存会保持内存的最新状态,所以从内存的角度看,所有行都是“干净”的,这简化了迁移。然而,在迁移之前,写通策略下的每一次写操作都必须穿过缓慢的互连。通过对每种策略下迁移成本与未来远程访问成本进行建模,操作系统可以确定一个活动阈值(例如,未来的写操作次数),超过该阈值,迁移就变得有利。这显示了页迁移与硬件基本工作原理的深度交织。
一旦操作系统决定迁移一个页,它面临另一个选择:如何执行移动?这引出了两种主要策略,它们之间存在着有趣的权衡。
主动迁移 (Eager Migration): 这是最直接的方法。操作系统暂停任务,将其所有必要的页复制到新位置,然后恢复任务。优点是简单。缺点是可能带来一次漫长、干扰性的前期停顿,并且可能会浪费时间移动任务再也不会使用的页。
懒惰迁移 (Lazy Migration): 这是“按需复制”的方法。操作系统移动核心任务状态,并立即在新核心上恢复它。内存页被留在原地。当任务第一次尝试访问一个页时,会触发一个页错误。操作系统随后截获这个错误,将所需的页复制过来,然后恢复任务。这避免了大的前期停顿,并确保只移动需要的页。然而,其代价是对每个尚未移动的页的首次访问都会产生一个小的软件开销 。
哪种更好?答案在于一个优美的代数关系。如果因不移动未使用页面而节省的成本大于因访问已使用页面而产生的故障累积开销,那么懒惰策略就更好。这导出了一个对最大可容忍开销的条件:如果 ,懒惰迁移就更优越,其中 是复制单个页面的时间。如果应用程序的内存访问是稀疏的( 很小),懒惰迁移通常是明显的赢家。
最后,所管理页面的大小本身也引入了另一个关键的权衡,尤其随着大页(例如,2 MiB 而不是标准的 4 KiB)的兴起。一方面,大页对性能非常有益。它们极大地扩展了转译后备缓冲区(TLB)的内存覆盖范围——TLB 是一个用于地址翻译的关键硬件缓存。使用 4 KiB 页,一个 64 项的 TLB 可能只能覆盖 256 KiB 的内存,而使用 2 MiB 页,一个 32 项的 TLB 就能覆盖巨大的 64 MiB,几乎消除了许多应用的地址翻译开销。
另一方面,对于页迁移而言,这种粗粒度可能是一把双刃剑。当操作系统检测到大页的一部分正在被远程节点频繁访问时,它唯一的选择可能是迁移整个 2 MiB 的页。如果实际上只有一小部分 4 KiB 是“热”的,这可能会造成浪费,迫使系统将大量“冷”数据一并移动。这增加了带宽消耗和“乒乓效应”的风险,即如果访问模式发生变化,大页会反复地来回移动。
这给操作系统带来了另一个艰难的决定:是应该迁移整个大页,还是应该先将大页分解成更小的 4 KiB 页,然后只迁移那些真正热的少数几个?通过对两种情况的总时间——包括初始迁移成本和后续访问成本——进行建模,操作系统可以计算出一个盈亏平衡点。例如,它可能会发现,如果一个大页内的 512 个小页中有超过 个是热的,那么将整个大页作为一个单元进行迁移实际上更快。
从对抗碎片化到驯服现代硬件的物理特性,页迁移证明了操作系统的隐藏智能。它是一个持续的、动态的优化过程,通过对概率、性能和硬件现实的优雅建模来平衡成本和收益。它确保了向应用程序呈现的简单、稳定的内存抽象,是由一个不懈、自适应的运动基础所支撑的。
在理解了页迁移的原理和机制之后,我们现在可以踏上一段旅程,看看这项非凡的能力将我们带向何方。如果说上一章是学习页迁移的语法,那么这一章就是阅读它的诗篇。我们将看到,这个单一而优雅的机制不仅仅是一个技术工具,而是现代计算的基石,支撑着从高性能科学模拟到庞大、无形的云基础设施的一切。正是操作系统,以其首席后勤官的角色,持续而静默地重新排列着机器的结构,以实现速度、弹性和令人惊叹的新形式的抽象。
想象一个有两个独立厂房(或“节点”)的大型工厂综合体。对于一个厂房里的工人来说,从本地仓库取零件,比等待从另一个厂房仓库运送过来要快得多。现代高性能计算机通常就是这样构建的,这种设计被称为非一致性内存访问(NUMA)。每个处理器(或“插槽”)都有自己的本地内存,可以非常快速地访问。访问连接到另一个处理器的内存是可能的,但速度明显更慢。要让程序快速运行,至关重要的是它的线程——它的工人们——与他们需要处理的数据在同一个厂房里。
但如果初始设置很笨拙怎么办?考虑这样一种情景:一个孤独的工人负责为一个庞大项目开箱和整理所有原材料。这种在许多操作系统中常见的“首次接触”策略,意味着所有数据最终都物理上位于第一个工人所在节点的内存中。现在,当全部劳动力到达,其中一半工人被分配到另一个节点时,他们发现自己处境非常糟糕。他们需要的每一个零件都需要一次缓慢的跨节点请求。整个项目的速度现在都受制于这些远程内存访问。
操作系统看到这种低效,有两种选择,每一种都深刻地体现了在移动数据和移动计算之间的权衡。
移动数据: 操作系统可以使用页迁移将一半的物料——内存页——移动到另一个节点,以便每个工人团队都能在本地拥有其数据。这会产生一次性、前期的巨大搬迁成本。但对于一个长时间运行的任务来说,这个成本很快就会被随之而来的本地访问巨大加速所摊销。操作系统必须足够聪明,权衡迁移的成本与远程访问的惩罚,以决定这次移动是否值得。
移动工人: 或者,操作系统可以将第二个节点的工人移动到第一个节点,那里存放着所有的数据。这不是页迁移,而是*线程迁移*。操作系统现在面临一个根本性的两难选择:是把数据移到计算这边更便宜,还是把计算移到数据那边更便宜?答案取决于相对成本:需要移动的内存大小,与重新调度线程并在新位置预热其缓存的开销之间的比较。
当我们意识到操作系统还有其他职责时,这场舞蹈变得更加错综复杂。调度器可能会看到一个节点过载,并出于负载均衡的原因决定移动一个线程,这是一种“推送迁移”。但这样做,它可能会把一个线程从其宝贵的本地数据旁移开,无意中造成了 NUMA 性能问题。一个真正智能的系统必须协调这些决策,也许可以先迁移线程,然后观察其行为。如果线程似乎稳定下来并正遭受远程访问的困扰,系统就可以触发页迁移,将其数据也带过来。这避免了为移动任务及其数据付出“双重惩罚”的代价,特别是如果该任务很快又要被再次移动的话。
页迁移不仅关乎速度,也关乎稳健性。物理内存和任何物理设备一样,可能会开始出现故障。高端系统使用纠错码(ECC)内存,它可以自动修复微小的、单位的错误。虽然纠错防止了立即的崩溃,但操作系统会收到一个通知。这种“软错误”是一个警示信号,就像地震前的小震动,表明该物理内存帧未来发生不可纠正故障的风险更高。
操作系统不必坐等灾难发生,而是可以主动采取行动。它可以触发一次页迁移,将数据从可疑的物理帧撤离到一个新的、健康的帧上。这是透明完成的,运行中的应用程序永远不会知道它的数据刚刚从一块可能有故障的硅片上被拯救出来。这是一个软件在硬件之上提供一层弹性的优美范例,它使用页迁移作为其应急响应工具。
这种灵活性的主题延伸到了机器的根本结构。如果你可以像插拔 U 盘一样,在服务器运行时添加或移除内存条呢?这种能力,被称为内存热插拔,对于需要进行维护而又不能关闭服务的大型数据中心至关重要。页迁移是实现这一点的魔法。为了安全地移除一个内存条,操作系统必须首先有条不紊地将位于该物理范围内的每一个活动页都迁移到系统的其他部分。这是一次细致的撤离,确保在物理区域断电并脱机之前,没有数据被遗漏。
也许页迁移最壮观的应用是在虚拟化世界中。它实现了一些听起来像科幻小说的事情:将一台完整运行的计算机——一台虚拟机(VM)——从一台物理服务器传送到另一台,可能相隔数千英里,而感知的停机时间仅有几百毫秒。这就是“实时迁移”,这项技术让云服务提供商能够在不中断客户应用的情况下平衡负载、进行硬件维护和提供容错能力。
这个过程的核心是迁移虚拟机的内存。但是,当虚拟机仍在运行并主动更改内存时,你如何通过网络复制数 GB 的 RAM 呢?最常见的方法,“预拷贝”,就像你一边住在房子里一边搬家。搬家工人(迁移过程)复制每个房间(内存页)的内容。但当他们这样做时,你还在继续制造混乱(脏页)。搬家工人必须在后续几轮中回来重新复制那些又变乱的房间。如果你制造混乱的速度比搬家工人清理和运输的速度还快,这个过程就永远无法收敛。这对于写密集型应用来说是一个真实的问题,其页面变脏的速率可能超过网络带宽。
为了解决这个问题,现代系统使用巧妙的混合策略。它们可能会进行几轮预拷贝,以将大部分“冷”(不变的)内存迁移过去。然后,当收敛变得不可能时,它们会切换到“后拷贝”模型。这就像把自己传送到新的、空无一物的房子里。虚拟机被暂停一瞬间,其 CPU 状态被转移,然后在新的服务器上恢复运行。起初,它没有任何内存;每次它试图访问一个页时,都会产生缺页中断,然后该页会按需从旧服务器获取。通过结合这些技术,系统即使对于要求最苛刻的工作负载,也能满足严格的停机时间和流量预算要求。
当虚拟机不仅仅是一个脱离实体的软件,而是直接使用物理硬件设备,比如高性能网卡(这种做法被称为 SR-IOV 或设备直通)时,情况变得更加复杂。虚拟机的驱动程序使用“被钉住的”内存缓冲区与设备通信——操作系统被禁止移动它们,因为硬件已经被告知了它们确切的物理地址。要实时迁移这样的虚拟机,虚拟机监控程序不能简单地移动这些页。它必须首先与客户机操作系统进行一次合作式的、半虚拟化的握手,请求其驱动程序安全地静默设备并释放对这些页的占用。只有这样,这些页才能被迁移,这展示了为实现如此强大的功能,软件层之间需要何等复杂的协调。
在追求更高计算能力的道路上,系统越来越依赖于像图形处理单元(GPU)这样的专用加速器。从历史上看,为 GPU 编程意味着手动在 CPU 主存和 GPU 专用内存之间来回复制数据。这既繁琐又容易出错。
现代系统提供了一个优美的抽象,称为统一虚拟内存(UVM)。CPU 和 GPU 共享一个单一的、统一的虚拟地址空间,使其看起来好像共享一个巨大的内存池。程序员可以分配一个数组,并使用相同的指针从 CPU 或 GPU 访问它。在底层,这种幻象是由页迁移驱动的。当 GPU 试图访问一个物理上位于 CPU 内存中的地址时,会触发一个页错误。UVM 驱动程序捕获这个错误,并发起一次迁移,通过高速互连(如 PCIe)将该页传输到 GPU 的本地内存中。
这种自动迁移非常神奇,但并非没有风险。如果一个 GPU 内核的工作集——它一次需要的数据量——大于 GPU 的物理内存,系统将开始“颠簸”,无休止地迁入和迁出页面,性能会陷入停顿。为了防止这种情况,权力被交还给程序员。通过明确的提示,程序员可以向系统建议未来的访问模式。通过为计算的下一阶段预取数据,并告知驱动程序哪个处理器将是某些数组的主要使用者,程序员可以引导迁移过程,将潜在的混乱转变为一场精心调校的数据芭蕾,并防止系统淹没在自身的迁移开销中。
页迁移的影响延伸到系统性能的最精细层面。移动一个页不仅改变了它的 NUMA 局部性,还改变了它的物理地址。这反过来又可能改变其内容如何映射到 CPU 的缓存中。一个复杂的操作系统可以使用这种“页着色”技术来仔细地在缓存中分布内存分配,从而最小化冲突并最大化性能。在具有不同缓存架构的 NUMA 节点之间迁移页面,需要对这些颜色进行更仔细的映射以保持局部性。
迁移的概念甚至超越了物理位置。应用程序的内存分配器可能会维护不同层次的内存——例如,一个使用对转译后备缓冲区(TLB)友好的小页的“热”层,和一个使用对批量存储更高效的大页的“冷”层。随着一个对象的访问模式发生变化,运行时可以在这些层之间迁移它,这是一种并非发生在物理芯片之间,而是在不同逻辑管理结构之间的页迁移形式,其目的全是为了在微秒级别上优化性能。
从规避硬件故障到传送虚拟世界,从协调 CPU 和 GPU 到优化缓存行为,页迁移展现了自己作为操作系统武库中最通用、最强大的工具之一。这是在我们计算机内部每秒发生数十亿次的无形之舞。一个看似简单的机制——将一个数据块从一个物理位置移动到另一个——实际上是定义现代计算的性能、可靠性和抽象的深刻推动者。它证明了系统设计之美,即一个单一、精心打造的原语可以为解决整个宇宙的复杂而奇妙的问题提供基础。