
虚拟内存为每个进程创造了一个私有的、广阔的地址空间假象,该假象由操作系统(OS)和内存管理单元(MMU)通过基于页面的地址转换进行管理。
多级页表解决了大地址空间带来的扩展性问题,而转译后备缓冲器(TLB)和局部性原理则确保了系统的高性能。
内存管理对系统安全至关重要,它通过保护位和 IOMMU 等机制来强制实现进程间以及进程与外设间的隔离。
写时复制(COW)等先进技术使得创建进程的效率极高,而共享内存则允许无缝的进程间通信。
在现代计算中,每个应用程序运行时都仿佛独占了整个计算机的内存,拥有一个广阔而私有的空间。然而,物理现实是,内存是一种有限的共享资源,由操作系统在众多相互竞争的程序之间进行调度。本文旨在揭开软硬件之间复杂协作的神秘面纱,正是这种协作使得这一强大的假象成为可能。本文将探讨一个根本性问题:操作系统如何管理内存,以便为无数并发进程提供隔离、保护和效率?我们将首先探索其核心原理和机制,揭示构成虚拟内存基础的地址转换、页表和硬件辅助机制。随后,在“应用与跨学科联系”部分,我们将审视这些概念对从应用程序性能、系统安全到实时计算等各个方面产生的深远影响。我们的旅程将从剖析那些弥合程序虚拟世界与计算机物理现实之间鸿沟的基本原理开始。
在现代计算的核心,存在一个宏大而美妙的假象——一个如此成功的抽象,以至于我们几乎从不需要思考它。你运行的每一个程序,从网页浏览器到复杂的科学模拟,都相信自己独占了整个计算机的内存。它看到的是一个广阔、私有且纯净的地址景象,从零开始,一直延伸到极大的数值。这就是它的虚拟地址空间。
实际上,计算机的物理内存——即焊接在主板上的实际 RAM 芯片——是单一、共享且时常混乱的资源。它是一个有限的存储池,必须在操作系统、你的浏览器、音乐播放器以及数十个其他同时运行的进程之间进行分配。
内存管理的核心任务,就是弥合虚拟内存的整洁私有假象与物理内存的杂乱共享现实之间的鸿沟。操作系统是如何在硬件的帮助下,为每一个进程维持这种假象,使它们彼此隔离并受保护,同时又高效地共享有限的物理资源呢?这是一个关于巧妙的间接寻址、字典与缓存,以及软硬件之间优美协作的故事。
想象一个程序正在运行一个简单的循环,重复访问一条数据。我们可以在一个思想实验中,向系统连接两个探针。一个探针,我们称之为 Tracer Y,监控由 CPU 指令生成的内存地址。另一个探针 Tracer X,则监控实际发送到物理 RAM 芯片的地址。
最初,Tracer Y 可能会看到程序访问地址 ,而 Tracer X 可能会看到内存系统访问地址 。稍后,一件有趣的事情发生了:操作系统决定重新组织物理内存,这个过程称为内存紧缩(compaction)。它将我们程序的数据从一个位置移动到另一个位置,比如说,向上移动了 字节。
现在,当程序的循环重复时,我们的探针会看到什么?观察 CPU 的 Tracer Y 会看到与之前完全相同的情况:一次对地址 的访问。程序完全没有意识到这次移动;它的世界没有改变。但是,观察物理 RAM 的 Tracer X 现在报告了一次对地址 (即 )的访问。物理地址改变了,但虚拟地址没有改变。
这个简单的实验揭示了一个深刻的真理:存在一种机制,用于在 CPU 的逻辑地址(或虚拟地址)和内存的物理地址之间进行动态、实时的转换。这被称为执行时绑定,由一个名为内存管理单元(MMU)的硬件完成。正是这种持续的转换行为,使得操作系统能够像拼图一样在物理内存中移动进程,而程序却毫不知情。
MMU 是如何执行这种转换的?为一个拥有数 GB 地址空间的系统中的每一个字节都建立映射关系,其效率是极其低下的。用于这种转换的“字典”本身将比内存还要大!
诀窍在于将虚拟内存和物理内存都划分成固定大小的块。我们将虚拟内存的一个块称为页(page),物理内存的一个块称为页框(page frame)。转换于是以页为单位进行。如今,一个典型的页大小是 千字节( 字节)。
根据这个思想,一个虚拟地址不再是单个数字,而是被分为两部分。对于一个 字节的页,地址的低 12 位代表页内偏移(page offset)——即一个字节在其页内的位置。地址的高位则构成虚拟页号(Virtual Page Number, VPN)。转换的魔力现在归结为一个任务:将一个 VPN 转换为一个物理页框号(Physical Frame Number, PFN)。偏移量则保持不变;如果你在寻找一个虚拟页中的第 100 个字节,你会在相应的物理页框的第 100 个字节处找到它。
操作系统为每个进程维护一个称为页表的“字典”。在其最简单的形式中,这是一个由页表项(Page Table Entries, PTEs)组成的数组,数组的索引就是 VPN。MMU 使用虚拟地址中的 VPN 来在表中找到正确的 PTE。
一个 PTE 必须包含哪些信息?其最关键的组成部分当然是 PFN——数据所在的物理页框号。但这还不是全部。PTE 是操作系统为 MMU 硬件留下关键信息的地方。一个简单的 PTE 必须包含几个控制位:
要表示一个拥有 个物理页框(约一百万个页框,或使用 4KB 页面的 4GB RAM)的系统,PTE 中的 PFN 字段需要 位。加上大约 6 个常见的控制位,PTE 的最小尺寸为 26 位。在实践中,PTE 通常被填充到像 32 位或 64 位这样的 2 的幂次大小,以简化处理它们的硬件。
我们已经建立了一个简单而优雅的系统。但快速计算一下就会发现一个灾难性的缺陷。一台现代 64 位计算机的虚拟地址空间为 字节。若页面大小为 KB( 字节),这意味着一个进程理论上可以拥有 个虚拟页面。如此规模的地址空间将需要一个包含 个条目的页表。如果每个页表项(PTE)为 8 字节,那么单个进程的页表就会消耗 字节的内存——高达数 PB!这完全是不可能的;这张“地图”会比它所描述的“领土”大得不成比例。
解决方案是避免创建一个庞大、扁平的页表。取而代之的是,我们引入层级结构,借鉴了我们组织文件夹和子文件夹中文件的方式。这被称为多级(或分层)分页。
虚拟地址的高位不再是单个 VPN,而是被分解成几个部分。例如,在一个两级方案中,我们会有一个页目录索引和一个页表索引。页表基址寄存器(PTBR)现在指向一个页目录。第一个索引引导 MMU 到达该目录中的一个条目。这个条目并不指向一个物理页框,而是指向一个二级页表。然后,第二个索引被用来在那个二级表中找到真正的 PTE。
这个方案的精妙之处在于,如果虚拟地址空间中一个大的、连续的区域未被使用,我们只需将顶级页目录中相应的条目留空(或设为 null)。我们根本不需要为那整个区域创建任何二级页表。高层表中的一个空指针可以有效地剪除地址空间树的一个巨大分支,从而节省大量的内存。顶级页表中的单个条目可以负责映射虚拟地址空间的巨大区域,其总覆盖范围取决于层级结构的深度和各级表的大小。
这种层级结构解决了空间问题,但似乎又带来了速度问题。一次内存访问现在可能需要两次、三次甚至四次额外的内存访问,仅仅是为了遍历页表树。这会严重影响性能。
硬件通过另一个专用缓存解决了这个问题:转译后备缓冲器(Translation Lookaside Buffer, TLB)。TLB 是一个小型、极快、由硬件管理的缓存,存储了少量最近使用的 VPN 到 PTE 的转换关系。在每次内存访问时,MMU 首先检查 TLB。如果找到了转换关系(称为 TLB 命中),页表遍历过程就被完全跳过,转换在一个时钟周期内完成。如果转换关系不在那里(称为 TLB 未命中),硬件会执行缓慢的页表遍历,然后将新找到的转换关系存入 TLB,期望它很快会再次被需要。
为什么这个方法如此有效?为什么一个只有 64 个条目的微小 TLB 能够满足一个拥有数千个页面的程序的转换需求?答案在于局部性原理。程序访问内存不是随机的。它们表现出:
考虑一个在紧密循环中访问内存的地址轨迹:几条指令、几项数据,都位于相同的一两个页面内。在最初的一两次 TLB 未命中之后,这些页面的转换关系将被加载到 TLB 中。循环中所有后续的访问都将是闪电般的命中。这样的轨迹即使在一个只有 2 个条目的微小 TLB 上,也能达到超过 87% 的命中率。
现在考虑一个病态的轨迹,它在数百个不同页面之间随机跳转。TLB 在这里毫无用处。每次访问都是一个新的页面,其转换关系没有被缓存,导致未命中。命中率骤降至零。虚拟内存的全部性能都取决于一个经验事实:真实程序表现出强烈的局部性。
既然我们已经构建了虚拟内存的机制,让我们来欣赏它所提供的强大功能。
虚拟地址空间为进程之间提供了固有的隔离。你的浏览器不能意外地(或恶意地)读取你的密码管理器中的数据,因为它们存在于相互独立、不重叠的虚拟世界中。但系统还在单个地址空间内提供了细粒度的保护,主要用于保护操作系统免受用户程序的侵害。
操作系统为每个进程的虚拟地址空间保留一部分供自己使用(例如,所有高于某个高水位线的地址)。这些内核页面的 PTEs 会将其用户/内核位设置为“仅内核”()。当一个用户程序运行时,CPU 处于“用户模式”。如果它试图访问内核空间的地址,MMU 会检查 PTE,发现 位为 0,并立即触发一个保护错误。操作系统接管控制,发现非法访问,并终止这个违规的程序。这个由硬件强制执行的边界是系统稳定性的基石。
如果操作系统能将一个进程的虚拟页面映射到物理页框,那么有什么能阻止它将两个不同进程的虚拟页面映射到同一个物理页框呢?没有任何东西!这就是共享内存背后的优雅机制。
想象两个需要通信的进程 P1 和 P2。操作系统可以创建一个共享内存区域,并将其映射到它们各自的地址空间中。P1 可能通过虚拟地址 访问它,而 P2 则通过一个完全不同的虚拟地址 访问它。但是操作系统设置它们的页表,使得 和 都转换到同一个物理页框 。当 P1 向此页内的地址写入数据时,硬件的缓存一致性协议确保当 P2 从其相应地址读取时,这个变化是可见的。缓存是物理标记的,意味着它们操作的是物理地址,所以硬件看到两个进程都在与同一个物理位置通信,并自动保持数据一致。
虚拟内存系统还可以强制执行契约。如果 P2 只应该读取数据,操作系统只需清除 P2 对该共享页面的 PTE 中的写权限位。如果 P2 试图写入,它会得到一个保护错误。此外,如果操作系统需要动态更改这些权限,它将面临缓存中陈旧数据的挑战——不仅仅是数据缓存,还有 TLB。为了安全地将 P2 的访问权限升级为读写,操作系统必须执行一次 TLB 刷下(shootdown),向所有其他 CPU 核心发送中断,强制它们使可能缓存了该页面旧的、只读的转换关系失效。
虚拟内存所带来的最强大的优化之一是写时复制(Copy-on-Write, COW)。在类 Unix 系统上使用 [fork()](/sciencepedia/feynman/keyword/fork()|lang=zh-CN|style=Feynman) 创建一个新进程,看起来应该是一个极其昂贵的操作,需要操作系统为新的子进程复制父进程的整个内存空间。
有了 COW,操作系统采取了一种欺骗手段。它不复制任何数据,而是简单地为子进程复制父进程的页表。然后,它遍历父进程所有私有数据页的 PTE,并在父子进程的页表中都将它们标记为只读。最初,父进程和子进程共享每一个物理页框。
系统像什么都没发生一样继续运行,直到其中一个进程——比如说子进程——试图写入这些共享页面之一。MMU 看到对只读页面的写入尝试,并触发一个页错误。这个“只读”状态是一个暂时的谎言,而操作系统的错误处理程序是这个秘密的知情者。它看到写入尝试,为子进程分配一个全新的物理页框,将原始页面的内容复制到新页框中,最后更新子进程的 PTE,使其指向这个新的、具有写权限的私有副本。父进程的 PTE 保持不变(或者其引用计数被递减)。从那一刻起,父子进程就拥有了该页面的各自独立的副本。
这项技术效率惊人。如果子进程立即调用 exec() 来启动一个新程序,那么就根本没有数据被复制。所有这些工作都被避免了。COW 的具体实现是微妙的,它区分了匿名内存(如栈和堆)和文件支持的内存,以及文件映射是私有的(MAP_PRIVATE,使用 COW)还是共享的(MAP_SHARED,写入操作意在共享,因此不使用 COW)。
虚拟内存系统功能强大,但并非无懈可击。其性能依赖于一种微妙的平衡。
仅在需要时才从磁盘加载页面的做法(按需分页)之所以有效,是因为局部性原理。一个进程在任何给定时间通常只需要其页面的一个小小子集,即其工作集。只要物理内存足够大,能够容纳所有活动进程的工作集,系统就能平稳运行。
但是,如果总工作集大小超过了可用的物理内存会发生什么?系统开始颠簸(thrash)。想象一个进程需要页面 A、B、C 和 D 才能继续执行,但操作系统只能给它三个物理页框。它加载了 A、B 和 C。然后它需要 D。为了给 D 腾出空间,它必须换出其中一个——比如说 A。于是它换出 A,换入 D。紧接着的下一条指令又需要页面 A!于是它必须换出另一个页面(比如 B)来把 A 换回来。系统把 100% 的时间都花在疯狂地在 RAM 和磁盘之间交换页面上,而 CPU 却处于空闲状态,没有任何有效进展。对于一个局部性差的工作负载,在内存不足的情况下运行会导致页错误率飙升至 1.0,意味着每一次内存访问都需要一次缓慢的磁盘操作。这是一个性能悬崖,如果不减轻内存压力,就无法恢复。
最后,我们必须问一个看似矛盾的问题。如果操作系统可以将任何页面换出到磁盘,那么包含操作系统自身代码的页面呢?页表本身呢?
考虑一下 TLB 未命中时会发生什么。硬件必须从物理内存中读取进程的页表。但是,如果页表本身所在的页面已经被换出到磁盘了呢?这将触发一个页错误。为了处理这个页错误,操作系统必须执行其页错误处理程序代码。但是,如果处理程序的代码也在一个被换出的页面上呢?尝试获取处理程序的第一个指令将导致另一个页错误。这是一个无法解决的无限回归;系统将会崩溃。
为了防止这种情况,操作系统必须建立一个基本的不变性原则。一小部分关键的内核代码和数据结构必须被固定(pinned)在物理内存中。它们被标记为不可分页,并保证始终驻留在内存中。这至少包括:页错误处理程序及其调用的内存管理代码、内核自身的页表,以及任何正在运行进程的至少顶级页表。这些固定的区域构成了整个虚拟内存宏伟假象的基石,确保当错误发生时,操作系统总有坚实的立足点来解决它。
在我们遍历了虚拟内存的基本原理——页表、地址转换以及时刻警惕的内存管理单元(MMU)这些精妙的机制之后——我们可能会倾向于将其视为一个已经完成的、自成一体的工程作品。但这就像研究了万有引力定律却从不观察行星的轨道一样。这些概念真正的美和力量,只有在我们看到它们在实际中运作,塑造我们周围的数字世界时,才会显现出来。内存管理不是一个静态的背景;它是一个动态的、活生生的系统,支撑着从最简单的应用程序到整个系统安全的一切。它是性能的无形建筑师,我们数据的沉默守护者,也是一个软件、硬件乃至抽象数学在此交汇的迷人枢纽。
在最直接的层面上,操作系统的内存管理器为程序员提供了一套工具,一个用于雕琢进程私有宇宙——其地址空间——的接口。这个工具箱中最卓越的工具是 mmap 系统调用,它是一把名副其实的内存操纵瑞士军刀。程序员使用 mmap 来请求一个新的虚拟地址空间区域,无论是一片用于数据结构的空白匿名内存,还是将文件直接映射到内存中,使文件 I/O 看起来像从数组中读取一样简单。
然而,这个接口是一份精确的合同。当应用程序请求内存时,它可以建议一个起始地址,但除非它使用特殊标志强制要求,否则内核可以自由选择一个不同的、更合适的位置。内核是地址空间的终极城市规划师;它尊重请求,但必须确保它们符合页面边界的底层网格。任何对特定字节数的请求都将被向上取整到最接近的整页,因为系统只能以页大小的包裹分发土地。这种协商——其中提示可能被忽略,长度会被调整——是我们遇到的基于页面的内存管理的第一个实际后果。
拥有这种权力也伴随着责任。当一个程序映射了一块内存区域,但由于一个 bug 丢失了指向它的指针时,会发生什么?程序再也无法使用或释放那块内存了。这是一种资源泄漏。从操作系统的角度来看,进程的虚拟内存大小(Virtual Memory Size, VSZ)——其总预留地址空间——已经增长,但其常驻集大小(Resident Set Size, RSS)——在物理 RAM 中的部分——可能只因实际接触过的少数页面而增加,这要归功于按需分页。映射的其余部分仍然是一个预留,一个内存的空头承诺。预留的虚拟空间和已提交的物理内存之间的这种区别是根本性的。在这里,我们看到了操作系统最深刻的角色之一:当这个泄漏内存的进程最终终止时,操作系统扮演着终极垃圾回收器的角色,有条不紊地拆除整个地址空间,并回收每一个映射。这确保了一个行为不当的程序不会永久性地消耗系统资源,这是多任务世界中稳定性的基石。
虚拟内存提供了一个美妙的抽象,但这并非没有代价。从虚拟地址到物理地址的转换,如果错过了快速的转译后备缓冲器(TLB)缓存,就会迫使硬件开始一次“页表遍历”。对于一个多级页表,这意味着处理器必须进行几次依赖的内存访问,仅仅是为了找出实际数据所在的位置。如果一个程序以毫无局部性的方式访问内存,例如以正好一个页面的步长跨越一个大数组,它可能会为每一次访问都触发一次完整的页表遍历。在这种最坏的情况下,每一次预期的内存访问都被放大成多次,这是一个隐藏在明处的性能瓶颈。
这就是为什么性能是一项协作努力。操作系统无法总是猜到应用程序的意图,但应用程序通常知道自己的未来。通过 madvise 系统调用,应用程序可以与内核进行对话。例如,它可以提示说,它将不再需要某个内存范围中的数据。通过像 MADV_DONTNEED 这样的提示,它告诉操作系统:“我用完这些页面的内容了;你可以丢弃它们而无需写入交换区。” 在随后的访问中,应用程序会得到一个全新的、填满零的页面。或者,通过像 MADV_PAGEOUT 这样的提示,它可以说:“我暂时不需要这个了,但数据很重要。请把它写到交换文件里以释放 RAM,但为我保留它。” 这使得应用程序能够主动帮助操作系统管理内存压力,通过牺牲其“冷”页面来保护其更重要的“热”页面。
在像 Java 或 Go 这样的托管语言世界里,这种微妙的协作变得更加错综复杂。在这里,一个垃圾回收器(Garbage Collector, GC)在进程内部运行,寻找未使用的对象。一个简单的“停止-全世界”(stop-the-world)GC 可能会暂停应用程序并扫描整个堆。如果堆很大,GC 可能会接触到数千个应用程序本身最近没有使用过的页面。从操作系统的角度来看,进程的工作集——它最近使用过的页面集合——突然爆炸式增长。如果这个膨胀的工作集超过了分配给该进程的物理内存,操作系统就会开始疯狂地进行分页。更糟糕的是,属于应用程序实际热集的页面,在 GC 暂停期间没有被接触过,现在在操作系统的页面替换算法看来就变“老”了。它们被换出。当应用程序恢复时,它会立即在其所有必要数据上发生页错误,导致称为“颠簸”的性能灾难。这揭示了一个引人入胜的跨学科挑战:GC 内存管理器和操作系统内存管理器必须被设计成相互协作。现代 GC 通常是“增量式”和“分代式”的,它们小心翼翼地管理自己的内存访问模式,以避免激怒它们所栖身的操作系统这个庞然大物。
在许多应用程序中,平均速度就是一切。但在像视频游戏或数字音频工作站这样的实时系统中,一致性才是王道。体验的流畅度取决于最长的那一帧的耗时。一个使用 mmap 从磁盘流式传输资产的游戏可能在数百帧内都运行得非常漂亮,但随后对一个非驻留页面的单次访问触发了一个页错误。CPU 停顿下来,等待操作系统从可能很慢的磁盘上获取数据。如果这个延迟超过了紧张的帧预算(对于一个 60 FPS 的游戏来说,可能只有 毫秒),结果就是一次可见的“卡顿”。这些事件的概率性——什么时候会发生错误,以及处理它需要多长时间?——使得内存管理成为实时图形学中的一个核心挑战。预测和最小化这些长延迟事件的概率是一个将操作系统性能与用户感知体验质量联系起来的深层问题。
为了实现绝对最高的性能,尤其是在网络方面,系统设计者努力实现“零拷贝”I/O。操作系统不是让 CPU 将数据从网卡的缓冲区复制到内核,然后再复制到应用程序,而是给予网卡直接内存访问(Direct Memory Access, DMA)应用程序缓冲区的权限。为了安全地做到这一点,操作系统必须将应用程序的页面固定在物理内存中,向硬件承诺它们的物理地址不会改变。然而,这在现代多核处理器中会产生一些微妙但深刻的后果。在 RAM 中固定一个页面并不会在 TLB 中固定它的转换关系;该转换关系仍然受制于正常的缓存和驱逐策略。此外,如果操作系统需要取消映射这个缓冲区,它必须执行一次“TLB 刷下”——这是一个昂贵的操作,它向所有可能缓存了该转换关系的其他核心发送处理器间中断,通知它们使其失效。这是跨越芯片的一次呐喊,一个代价高昂但为维持内存一致性所必需的行动,揭示了操作系统策略与硬件现实之间的深层耦合。
内存管理不仅仅是关于组织数据和提升性能;它也是系统安全的第一道防线。启用虚拟内存的硬件——MMU——同样也强制执行进程间的隔离。但如何将系统与强大的外设隔离呢?
一个流氓或有缺陷的、具备 DMA 能力的网卡,原则上可以写入任何物理地址,从而破坏内核本身。这就是输入输出内存管理单元(Input-Output Memory Management Unit, IOMMU)发挥作用的地方。它充当设备的守门人,为外设提供一种“虚拟内存”抽象。在设置零拷贝 I/O 时,操作系统配置 IOMMU,只允许网卡访问其缓冲区的特定、固定的页面。设备任何试图访问这个小的、受制裁集合之外的内存的尝试都会被 IOMMU 硬件阻止。这将设备的攻击面从整个物理内存急剧缩小到仅仅几个页面。这些权限的设置和拆除必须完美无瑕。例如,内核必须在取消固定物理页面之前移除 IOMMU 映射。搞错顺序会创造一个微小的时间窗口——一个检查时-使用时(TOCTOU)漏洞——在这个窗口中,页面可能在流氓设备写入之前被另一个进程重用,这是一个微妙但致命的缺陷。
内存的物理特性还隐藏着其他秘密。当一个程序从内存中“释放”一个加密密钥时,虚拟映射消失了,但代表密钥位的电荷可能会在 DRAM 单元中停留数秒甚至数分钟——这种现象称为数据残留(data remanence)。一个能够快速重启机器并读取 RAM 原始内容(“冷启动攻击”)的攻击者可以恢复这个“被擦除”的密钥。这揭示了一个令人不寒而栗的真相:free() 并不等同于 erase()。为了真正销毁一个秘密,应用程序必须用零显式地覆盖缓冲区。然而即便如此也还不够!由于写回缓存的存在,覆盖操作可能只存在于 CPU 缓存中。程序必须使用特殊的指令来刷新缓存行,确保这些零被物理地写入 DRAM 芯片。这段从逻辑覆盖到物理覆盖的旅程,鲜明地提醒我们,我们依赖于多层抽象,而当这些抽象未被完全理解时,安全风险就会出现。
最后,内存管理的精妙机制使我们的系统成为能够活生生、不断演进的实体。考虑在一个正在运行的 Linux 系统上更新一个共享库——这是每天都在发生的事情。如何在不停止每个使用它的程序的情况下完成此操作?答案在于文件的路径名与其底层身份(inode)之间的区别。当一个进程用 MAP_SHARED 映射一个库时,这个映射是绑定到 inode 上的。如果一个包管理器就地覆盖了文件,这个变化会通过统一的页缓存立即对所有正在运行的进程可见。一种更健壮的方法是将新版本写入一个临时文件,然后使用一个原子性的 rename 操作。这将路径名指向一个新的 inode。现有的进程,其映射绑定到旧的 inode,继续使用旧版本不受干扰地运行。新打开该库的进程将获得新版本。这是一种无缝的、微观层面的外科手术,让系统在不停止音乐的情况下得以演进。
从程序员的 API 到游戏性能的概率性,从 IOMMU 的硬件堡垒到 RAM 中数据的鬼魅残留,内存管理的原理是一条贯穿始终的主线。它们证明了数十年来为驯服现代计算机的狂野复杂性所付出的智慧,将其转变为我们今天所知的强大、安全和动态的机器。