1 虚拟内存硬件解决什么
服务器上的程序通常用虚拟地址访问内存。虚拟地址是程序看到的地址,物理地址是处理器最终交给内存系统的地址。连接这 2 套地址的硬件机制称为地址翻译。它让多个进程都能使用看起来连续、彼此独立的地址空间,而实际数据可以分散在不同的 DRAM 位置。这里的“连续”首先是地址编号连续,并不保证内存条上的存放位置连续。
设想 1 台装有 64 GiB DRAM、1 块 NVMe SSD 和 1 张 25 Gb/s 网卡的服务器。数据库进程和图像处理进程都可能使用虚拟地址 0x400000,但它们对应的物理页可以不同。程序发出读写后,CPU 内的内存管理单元 MMU 依据当前地址空间的映射进行翻译,并检查访问权限。这样,普通进程不能仅凭猜测地址就读到另 1 个进程的数据,也不能直接改写内核维护的关键结构。
虚拟内存也使操作系统能够把地址空间的组织与物理内存的分配分开。进程预留 16 GiB 地址范围,不表示机器立刻消耗 16 GiB DRAM;某些页可以等到首次访问才分配。多个进程可以通过不同虚拟地址共享同 1 份只读代码页。文件内容也可以映射成内存地址,由系统在需要时准备对应页。不过这些能力需要操作系统参与,MMU 不会自己决定把哪个文件读进来。
不要把虚拟内存理解成“用 SSD 增加内存容量”的同义词。即使机器没有配置交换空间,地址翻译、隔离和按需分配仍然成立。SSD 上的数据必须先经过存储控制器、总线和内存系统进入 DRAM,CPU 才能按照通常的内存访问方式使用它。DRAM 与 NVMe 的延迟和带宽特征相差很大,扩大可承诺的地址范围,并不等于扩大同等速度的工作空间。
这套机制也存在于笔记本、工作站与许多嵌入式板卡中,但具体能力要看处理器和系统配置。没有 MMU 的微控制器可能不提供同样的进程地址隔离。带 MMU 的板卡还要考虑物理内存大小、内存控制器以及操作系统支持。选型时应先确认“是否支持所需的地址翻译与保护”,再讨论容量和性能;不要仅凭处理器是 64 位就推断全部地址位都已实现。

2 页表、TLB 与缺页路径
页是地址映射的基本单位。以 4 KiB 页为例,1 页包含 4096 B,页内偏移需要 12 位。虚拟地址可以拆成虚拟页号和页内偏移;页表记录虚拟页号到物理页框的映射,并附带有效性与权限信息。物理页框也按页大小对齐,因此翻译改变的是页号,页内偏移保持不变。页表本身通常存放在 DRAM 中,也会受到处理器缓存的影响。
如果每次读数据前都完整查询页表,翻译会很昂贵。CPU 因而设置 TLB,即转换后备缓冲,用来缓存最近使用过的地址翻译及相关属性。TLB 命中时,硬件获得物理页框并检查本次访问是否允许。TLB 的容量有限,而且指令访问和数据访问可能使用不同结构;本课只用“有限数量的翻译缓存项”理解它,不把某种具体结构当作所有 CPU 的共同设计。
TLB 未命中表示当前缓存里没有可用的翻译,不表示目标数据一定不在 DRAM。以常见的硬件遍历页表架构为例,硬件会按页表层级读取条目,找到有效映射后填充 TLB,再继续访问。层级页表让稀疏的地址空间不必预先占用巨大连续表格,但查找可能涉及多个依赖读取。部分架构采用不同的未命中处理方式,因此实际分析必须先确认处理器文档。
缺页异常是另一件事:翻译过程中发现映射状态或访问条件不能直接满足当前请求,处理器把控制权交给操作系统。若只是首次触碰匿名内存,系统可能分配并清零 1 个物理页;若内容已在文件页缓存中,可能只需建立映射;若需要从 SSD 读入文件页或换入内容,就要等待存储 I/O。不同路径的成本差异很大,不能用“发生了 1 次缺页”直接估算耗时。
还有无法恢复的访问。例如程序写入只读页,或访问没有合法映射的地址,系统通常向进程报告错误,而不是替它准备数据。合法缺页处理完成后,系统可安排原访问重新执行;非法访问则不会因为重试自然成功。观察问题时应把 TLB 未命中、可恢复缺页、权限故障和存储读取分开,分别寻找计数、地址范围和时间上的证据。

3 大页与地址翻译成本
TLB 能覆盖多少数据,粗略取决于缓存项数量乘以每项覆盖的页大小。假设某级数据 TLB 有 64 项,且全部用于 4 KiB 页,理想覆盖量是 256 KiB。若同样有 64 项可用于 2 MiB 页,理想覆盖量则是 128 MiB。这个乘法解释了大页的吸引力,但它只是容量直觉:真实处理器可能为不同页大小提供不同数量的缓存项,还会受到访问模式和映射冲突影响。
考虑 1 GiB 数据区。使用 4 KiB 页时包含 262144 页;使用 2 MiB 页时包含 512 页。若程序随机触碰这片区域,大页有机会显著减少需要记住的翻译数量。在支持相应大页的页表格式中,翻译也可能在较高层级就结束,从而减少部分遍历工作。然而数据本身仍然要经过缓存与内存系统,大页不会让 DRAM 芯片的读取延迟自动下降。
大页也有代价。映射粒度变粗后,只有少量数据被使用的区域可能浪费更多物理内存;某些分配方式还需要足够大的连续物理范围。系统运行较久后,空闲容量足够也未必立即满足这类分配,整理内存可能带来额外延迟。透明大页、显式大页和不同系统的回退策略各有差异,不能把“开启了配置”当成“所有目标数据都已使用大页”。
在有多路处理器或多个内存节点的服务器中,还要关注页被放到了哪里。更大的映射粒度可能影响细粒度放置和迁移的选择。如果访问跨越较远的内存路径,大页带来的翻译收益可能被数据访问成本掩盖。入门阶段不必研究复杂的节点调度算法,但应知道“翻译更省”和“数据更近”是 2 个不同目标,必须分别观察。
评估大页适合先提出条件:工作集较大、跨页访问频繁、映射较稳定,并且确有翻译开销证据。对小型控制程序、变化频繁的小块分配或内存紧张的板卡,收益可能有限。下面的滑块只展示页数量、理想覆盖量和简化成本如何变化,不能用来预测某款服务器的真实吞吐。实际选型应以目标 CPU、页大小配置和代表性负载的测量为准。
4 动手:跟踪一次地址翻译
先做可以手算的例子。假设页大小为 4 KiB,某次读取的虚拟地址是 0x12345ABC。低 12 位是偏移 0xABC,其余部分组成虚拟页号 0x12345。若页表把它映射到物理页框 0x6789A,物理地址就是 0x6789AABC。这里把地址写成较短形式只为演示拆分,不代表真实机器的完整地址宽度,也不表示可以随意选择物理页框。
接着写出 3 条路径。路径 A 是 TLB 已缓存此映射,读权限允许,硬件直接继续数据访问。路径 B 是 TLB 没有映射,但页表中存在有效且允许读取的条目,硬件完成遍历并缓存翻译。路径 C 是页表不能立即满足访问,进入异常处理。先分清路径,再讨论耗时,才能避免把所有“比预期慢”的访问都叫作缺页。
在 Linux 测试机上,可以用受控程序申请 64 MiB 匿名缓冲区,按 4096 B 步长写入,每页触碰 1 个字节。先记录申请后的状态,再记录首次写入完成后的状态,最后重复同样的写入。不要在生产服务上临时制造内存压力。首次写入常会触发物理页准备;后续访问通常减少这类首次触碰缺页,但这并不保证全部翻译一直保留在 TLB 中。
程序应输出每阶段的耗时和校验结果,避免编译器删除无效工作。可用 getconf PAGESIZE 查询基础页大小,用 getrusage 的阶段差值记录轻微缺页和重大缺页,也可读取进程映射统计了解实际页大小。若系统启用了透明大页,观察结果可能不同于纯 4 KiB 假设;应记录实际设置和目标区域的映射情况,而不是为了符合例子忽略这些差异。
进一步可在支持的 Linux 环境中尝试 perf stat -e page-faults,minor-faults,major-faults -- ./vm_touch,其中 vm_touch 是上述自行编译的测试程序。TLB 相关事件的名称和语义需要查询 perf list 与处理器文档,权限限制也可能使事件不可用。不要假定每台机器都有完全相同的通用事件,更不要把缺页总数当成 TLB 未命中数的替代品。
最后将步长从每页 1 次改为页内多次,保持总访问次数可解释,再对比重复访问的结果。按页随机访问还会同时改变数据缓存局部性,因此观察到变慢只能提出假设,不能立即归因于 TLB。实验记录应包含 CPU、内存容量、页大小、缓冲区大小、触碰顺序、线程数和是否包含首次触碰阶段;这些信息让另 1 台机器上的复现实验有明确边界。

5 权限位与保护
页表不仅回答“数据在哪里”,还回答“当前访问是否允许”。常见属性包括是否可读、是否可写、是否允许取指执行,以及普通用户态是否可以访问。具体编码因架构而异,有的权限由多个层级共同约束。MMU 会结合访问类型与处理器当前执行权限检查它们,因此即使地址已经能翻译成物理地址,写入或执行也可能被阻止。
例如进程的代码页可以允许读取和执行,同时禁止普通写入;数据缓冲区可以允许读写,同时禁止执行。程序把数据地址错误地当作函数入口时,禁止执行的属性可以使这类错误及早暴露。它不是所有攻击的完整解决方案,但把“能保存字节”和“能执行指令”分开,是硬件保护的重要基础。系统还可以按进程权限限制用户态对内核区域的访问。
只读页也可能参与正常的资源管理。创建子进程后,系统可暂时让父子进程共享部分物理页,并把相关映射设为只读。首次写入产生异常时,系统确认这是允许的写时复制请求,再分配新页、复制内容和更新映射。表面上同样是写入触发故障,合法写时复制与真正越权写入的结局却不同,关键在系统维护的映射语义,而不只是某个权限位。
页表更新后,处理器已经缓存的旧翻译也必须得到适当处理。否则软件认为页面已撤销写权限,某个 CPU 却可能继续使用旧属性。操作系统依照架构规则进行翻译失效,并在需要时协调其他处理器。应用通常不直接执行这些操作,但频繁创建、撤销或改变大量映射可能带来可见成本。具体广播和同步细节留到后续课程,本课只要求认识缓存翻译需要保持有效。
最后要区分 CPU 访问与设备直接访问内存。PCIe 网卡可通过 DMA 在主机内存与设备之间搬运数据,CPU 页表并不自动替所有设备完成隔离;相关系统可能使用 IOMMU 建立设备侧的地址转换和保护。设备、驱动与系统配置需要共同支持。排查网卡缓冲区损坏时,既要看进程映射,也要看驱动管理的 DMA 范围,不能仅凭用户态页权限正常就排除设备路径。
6 常见坑:把缺页当纯软件问题
缺页异常由硬件检测并进入系统处理,所以它跨越了软硬件边界。处理结果既可能只涉及页表和内存分配,也可能涉及 DRAM 清零、内存回收、文件系统、NVMe 控制器和 PCIe 链路。若故障路径需要等待 SSD,存储队列拥堵、设备温度导致的性能变化和其他任务的 I/O 都可能扩大延迟。仅查看异常处理函数的 CPU 耗时,可能漏掉真正占据时间的等待。
轻微缺页通常表示无需为该次缺页从后备存储读取内容,但它仍可能消耗分配、清零、复制和映射更新的时间。重大缺页通常涉及从存储读入所需页面,具体统计定义应以系统文档为准。它也不能简单等同于“交换空间被使用”:首次读取映射文件也可能产生需要 I/O 的缺页。诊断报告应写明映射类型以及观察到的 I/O,避免用 1 个计数器概括全部原因。
另 1 个坑是把首次运行与稳定运行混为 1 组数据。刚启动的服务可能需要准备匿名页和读取文件,运行后页缓存及映射状态改变,同样请求的路径就会不同。如果性能承诺覆盖启动后的首批请求,就必须测量这段过程;如果只承诺稳定吞吐,也要明确预热条件。不能把较快的阶段单独挑出来,却让部署方案承担没有测量的启动延迟。
内存容量压力同样会改变故障路径。64 GiB 服务器若同时运行多个大工作集,系统可能回收文件缓存或交换匿名页,也可能因限制或配置而让申请失败。容器的内存限额还可能早于主机总容量成为约束。增加 SSD 容量无法消除 DRAM 工作集超额带来的延迟;增加 DRAM 也不会自动修复非法地址或权限错误。先判断是容量、映射还是访问合法性问题,再决定硬件动作。
实际排查可以从发生时间和地址类别入手:启动阶段还是稳定阶段,匿名区域还是文件映射,是否伴随重大缺页和块设备读取。若缺页并未上升而吞吐下降,再看 TLB 遍历、数据缓存与内存带宽等证据。若进程直接收到访问错误,应先核对故障地址、读写类型和映射权限。不同分支对应不同证据,扩大交换空间或盲目开启大页都不能替代这次分类。

7 案例:TLB 抖动拖垮吞吐
考虑 1 台 16 核、128 GiB DRAM、25 Gb/s 网卡的索引服务器。它把 8 GiB 索引常驻内存,每次查询随机读取多个短记录,记录分散在许多 4 KiB 页上。负载增加后吞吐低于预期,但网卡未接近可持续带宽上限,SSD 读取很少,稳定阶段重大缺页接近 0。这里的数字描述排查场景,不是某款设备的实测规格或性能结论。
最初假设是“索引已经在内存里,剩下只是计算”。但在内存里只说明无需重新从 SSD 取回,并不表示翻译都在 TLB 中,也不表示数据都在 CPU 缓存中。若查询不断触碰远超 TLB 覆盖范围的页面,旧翻译会被反复替换;后续再次访问这些页时,又要执行页表遍历。这种频繁失去可复用翻译的状态,可称为 TLB 抖动。
团队先固定线程数、查询集合和索引布局,预热后重复测量,把缺页与 TLB 相关事件分开记录。若处理器提供适当事件,可观察数据 TLB 未命中后的遍历次数或活动,并同时记录缓存未命中、CPU 时间和吞吐。仅有某个 TLB 事件较高还不够:并行访问可能掩盖部分开销,事件也可能只覆盖某级结构,因此需要行为变化与性能变化相互支持。
接着做 2 类受控改动。第 1 类把经常共同访问的记录紧凑放置,减少每次查询跨越的页数。第 2 类在内存预算允许时,为适合的索引区域试用 2 MiB 大页,并核实实际映射成功。每次只改变需要验证的因素,同时保持查询结果一致。如果吞吐提高、翻译遍历指标下降且其他条件接近,才能加强“翻译压力是瓶颈组成部分”的判断。
假设某次内部测量从每秒 10 万次查询提高到 13 万次,可以报告该负载下吞吐约增加 30%,但不能据此承诺所有部署都增加 30%。较小索引、不同访问顺序、不同 CPU 或更高并发可能改变结果。若大页降低遍历却使尾延迟恶化,还要检查分配与回收阶段;若记录重排同时改善缓存局部性,就应承认收益包含多种因素,不能全部归给 TLB。
8 把翻译开销写进性能假设
性能方案应把地址翻译视为每次访存路径的一部分。可以先列出工作集大小、基础页大小、估计活跃页数以及访问顺序,再判断 TLB 覆盖是否值得关注。活跃页数比“总分配字节”更贴近翻译需求,因为 8 GiB 地址区可能只在短时间内访问其中一小部分。反过来,每页只读少量字节,也可能用很少有效数据制造大量翻译需求。
建立入门模型时,可用“每次访问平均附加翻译成本约等于未命中比例乘以平均遍历成本”作初步估算。假设未命中比例为 5%,每次遍历暂按 80 ns 估计,得到 4 ns 的平均附加量。这里的比例和时间必须来自明确假设或测量;现代处理器可能把部分工作重叠起来,故这个算式不能直接加到每条指令上,更不能把结果当作确定的请求延迟。
请求级估算还应区分串行依赖和可并行的访问。前 1 次读取决定后 1 次地址时,等待更容易暴露;多个独立请求同时执行时,部分等待可能被其他工作覆盖。与此同时,增加线程也可能增加活跃翻译和页表访问压力。因此不能从“核数翻倍”直接推断随机内存查询吞吐翻倍。方案可以提出扩展假设,但应安排不同线程数的验证点。
把故障路径单独列预算同样重要。稳定阶段的 TLB 未命中预算、启动阶段的首次触碰预算以及需要存储 I/O 的缺页预算,属于不同时间尺度。对于要控制尾延迟的服务,可以评估预先准备内存、约束工作集或减少关键路径上的映射变化;每个选择都应列出启动时间、DRAM 占用和实现复杂度的代价。不要用稳定阶段平均值掩盖偶发的慢路径。
验收条件应能被其他人复核,例如指定 CPU 型号、内存容量、系统与页配置,固定数据规模和并发度,分别报告预热前后吞吐与延迟分位值。硬件事件只在同一语义下比较,涉及平台差异时保留原始定义。最后给假设写上失效条件:工作集超预算、实际未采用大页、发生持续换入或访问模式改变时,原先的性能结论需要重新测量。
9 从硬件翻译走向同步与并发(S25)
地址翻译解决了地址空间的组织和访问保护,却没有替程序定义多个线程如何协作。同 1 进程内的线程通常共享地址空间,因而可以通过相同虚拟地址访问共享对象。不同进程则可能通过共享映射访问同 1 物理页。共享的对象有了落脚点,但何时写入、其他执行者何时可以使用结果,仍需要明确的同步规则。
共享映射还要求正确表达对象的位置。例如进程 A 将共享页映射到 0x400000,进程 B 将同 1 物理页映射到 0x800000,页内偏移为 0x100 的记录在两边分别位于 0x400100 和 0x800100。如果 A 把自己的虚拟地址直接写进共享页,B 按这个数值解引用,可能访问无关页面或触发缺页异常。常见做法是保存相对共享区域起点的偏移,由各进程加上自己的映射基址。MMU 只按当前地址空间翻译地址,不会识别共享数据中的指针并自动修正它;排查这类故障时,应同时核对双方映射的后备对象、区域偏移和本地基址。
例如线程 A 在共享缓冲区写入数据,再设置“准备完成”标记;线程 B 看到标记后读取缓冲区。仅确认这 2 个地址都映射成功、读写权限正确,不能证明这个协议可靠。编译器和处理器都可能按各自规则安排访问,语言层面的数据竞争还会使程序行为失去保证。下一课 S25 将讨论如何借助互斥量、原子操作等工具表达协作关系,本课不展开内存序细节。
也不要把修改页权限当成普通线程同步的替代办法。页保护适合表达较粗粒度的隔离、只读和执行限制,权限变更可能涉及系统调用及翻译失效。线程之间频繁传递少量状态时,需要的是适合该协议的同步机制。2 者都依赖硬件与系统协作,但解决的问题和成本不同;知道页表如何工作,有助于识别边界,而不是把全部协调问题都交给页表。
在设备参与时,边界更加明显。网卡 DMA 把数据写入可访问的缓冲区,并不单凭“物理地址正确”就完成了软件所有权交接。驱动还要遵守设备完成通知和平台规定的访问规则。入门阶段只需记住:地址可达、权限允许、内容准备完成、协作顺序正确,是需要分别确认的条件。后续学习同步与并发时,应把本课的地址路径作为基础,再补上时间与协作关系。
放进方案里
假设要为 1 台 16 核、128 GiB DRAM、25 Gb/s 网卡、NVMe SSD 的索引服务器写部署方案,先说明稳定工作集预算,例如索引与缓冲区合计不超过 80 GiB,并为系统、文件缓存及并发任务保留余量。这个预算应根据实际程序测量修订,同时写清容器限额与交换配置,避免只用主机安装容量推断可用空间。
地址翻译部分记录基础页大小、目标区域是否采用 2 MiB 大页、启用方式和实际映射的核实方法。大页收益以固定查询集的对照测量为依据,同时观察物理内存占用、启动时间与延迟分位值。若申请失败,写明是否允许退回基础页,以及退回后仍须满足的性能下限,让部署行为和验收条件对应起来。
性能假设分成启动与稳定运行。启动阶段测量文件载入、首次触碰和内存准备时间;稳定阶段报告吞吐、延迟、轻微与重大缺页,并在平台支持时保留语义明确的翻译遍历指标。对照实验固定线程数、索引版本和访问顺序,每次只调整目标因素。网卡和 SSD 的负载作为相关证据记录,不能仅凭设备标称带宽排除瓶颈。
运行边界也要具体:工作集超过预算、持续出现需要 I/O 的缺页、实际页大小偏离配置,或访问模式变得更分散时,触发重新评估。访问权限错误按故障地址与映射检查,吞吐下降按翻译、缓存、内存和存储路径分类。若大页带来额外内存压力或尾延迟,保留经过验证的基础页配置作为回退选择,并明确回退后的容量与性能条件。
共享数据的协作约束交给 S25 的同步设计补全。方案在此只确认哪些区域可共享、由谁建立映射、CPU 与设备各自使用什么访问路径;对“何时可以读写”的协议另列同步要求。这样,硬件容量、地址保护、性能假设和并发正确性都有可检查的责任位置。
10 小结:下一步同步与并发(S25)
虚拟地址经页表和 MMU 到达物理内存,TLB 缓存翻译以降低重复查询成本。TLB 未命中、需要系统处理的缺页与非法访问是不同路径;大页可以扩大翻译覆盖范围,也需要付出分配粒度和内存管理的代价。
自测时可以回答 3 个问题:数据已在 DRAM 中,为何仍可能因翻译变慢?首次写入匿名页与从 SSD 读取映射文件,为何不能合并估算?共享页已允许读写,为何还需要同步?能把地址、权限、数据位置和协作顺序分开说明,就具备进入 S25 的基础。