@bellicosiX:这篇读起来真是令人愉悦。写得充满爱意。我很感激。@abhi9u

X AI KOLs Timeline 论文

摘要

一篇全面、详尽的博客文章(堪比书籍),涵盖 Linux 中的虚拟内存概念,包括页表、TLB、按需分页、写时复制、内存映射 I/O 以及性能影响,通过进程与内核之间的对话呈现。

这篇读起来真是令人愉悦。写得充满爱意。我很感激。@abhi9u https://t.co/KYVqGGgEHW
查看原文
查看缓存全文

缓存时间: 2026/06/28 03:59

这篇读起来真是赏心悦目。字里行间都透着满满的用心。我由衷感激。@abhi9u https://t.co/KYVqGGgEHW —

虚拟内存:页表、TLB 与 Linux 内核探秘

原文:https://blog.codingconfessions.com/p/virtual-memory?r=3w4e6&utm_medium=ios&triedRedirect=true

这篇文章几乎是一本书级别的虚拟内存深度剖析。我花了几个月时间打磨它。因为篇幅接近一本书,我还准备了一份排版精美的 60 页 PDF/EPUB 版本,供想离线阅读、做标注或留作参考的读者使用。购买电子书也是直接支持这份工作的方式。我想要 PDF/EPUB(https://codingconfessions.gumroad.com/l/iuqsqm)

感谢你一直读到这儿。现在,让我们进入虚拟内存的世界。

虚拟内存是现代计算的基础组件,掌握它对构建和调试高性能、数据密集型系统至关重要。通常,我们认为虚拟内存是一个在内存层面为进程提供隔离的系统——这意味着操作系统能够并发运行多个进程,而不会让它们互相干扰或破坏彼此的数据。但虚拟内存能做的远不止于此,例如:

  • 通过请求分页实现内存的惰性分配
  • 进程间共享内存的写时复制,以及通过 fork 实现快速进程创建
  • 使用 mmap 避免页缓存到用户缓冲区拷贝的文件 I/O
  • 页面回收、交换与页面缓存
  • 访问模式、大页、TLB 击穿和 NUMA 放置带来的性能影响

本文将以宽广且实用的视角,介绍虚拟内存是什么、如何工作,以及它如何影响数据密集型系统的性能。读完本文,你将形成一个心智模型,理解以下关键概念:

  • 为什么需要虚拟内存:进程隔离、内存保护,以及大容量内存的假象。
  • 虚拟地址空间:进程的内存如何被组织成段(代码段、数据段、堆、栈和内存映射区域)。
  • 地址转换:使用分层页表将虚拟地址转换为物理地址的过程,以及为何分层页表能避免内存浪费。
  • 硬件的作用:MMU 和 TLB 如何加速地址转换,以及为什么 TLB 命中率对性能至关重要。
  • 请求分页:内核如何延迟物理内存分配,直到页面真正被访问,以及缺页异常如何驱动这种惰性分配。
  • 内存类型与回收:匿名页、文件后备页、共享页和 tmpfs 后备页有何不同,以及内核为何以不同方式回收它们。
  • 写时复制:进程如何高效共享内存,以及 fork 如何几乎瞬间创建新进程。
  • 内存映射 I/Ommap 如何将文件数据映射到进程地址空间,避免额外的用户缓冲区拷贝,并实现进程间共享内存。
  • 性能影响:页面大小、TLB 覆盖范围以及内存访问模式如何影响数据密集型工作负载的性能。
  • 可观测性:如何在 Linux 上查看 VMA、RSS/PSS、缺页异常、TLB 行为和 NUMA 放置。

本文采用了一种不同的方式讲解虚拟内存。我们不是罗列事实和定义,而是通过一个叙事来阐述概念:一系列发生在名为 Alloca 的新创建进程与 内核 之间的对话。Alloca 在执行代码时遇到挑战,内核则根据她的提问解释内在机制。这种基于对话的格式让我们可以逐步建立理解,随着自然问题的出现,慢慢引入复杂度。

结构:每个部分都遵循相同的模式:一段深入探讨概念的对话,后跟一个 关键收获 框,提供正式的总结、定义和技术细节。如果你更喜欢快速概览,可以只读关键收获部分。如果你想深入理解,请通读完整对话。

长度与节奏:本文内容全面,约 25,000 字,涵盖从基本地址转换到请求分页、页面回收、写时复制、可观测性和性能影响的方方面面。不必强求一口气读完。虚拟内存是一个复杂的主题,有许多相互关联的部分。慢慢来,分几次阅读,让概念沉淀。每一节都建立在前面内容的基础上,所以设计上是按顺序阅读的。另外,如果你已经修过操作系统课程,文章的前面部分可能对你来说有点基础。我鼓励你直接跳到你感兴趣的部分,后面也有不少高级内容。

实现细节:虚拟内存的概念在不同操作系统上基本通用,但当讨论特定的实现细节时,例如大页、TLB 行为或缺页异常处理,这些细节基于 Linux 内核和 x86-64 架构。另外,整篇文章我们将谈论大多数内核中仍普遍使用的 4 级页表。虽然最新的 Linux 内核也支持 5 级页表,但如果你掌握了 4 级页表的工作原理,理解 5 级页表应该是轻而易举的事。

旁注:文章大部分采用 Alloca 与内核之间的对话叙事风格,但我也穿插了一些旁注形式呈现的额外细节。

现在,让我们认识 Alloca,跟随她踏上虚拟内存系统的旅程。

当 Alloca 开始执行代码时,她遇到了第一个挑战。她需要从内存中读取一些数据。指令中包含数据的地址,Alloca 心想,“嗯,这应该不难。我只需要去到那个地址,读取值就行了”。但她将迎来一个巨大的意外。

当她走到那个地址时,发现那里空无一物。一切都只是幌子。她困惑地站在那里,不知道现在该怎么办。这时,她看到一个高大的身影从阴影中向她走来。

Alloca:“你是谁?” 内核:“我是内核。我掌控着整个世界,确保所有进程都能顺利工作。你在这儿做什么?这个地址什么都没有!” Alloca:“我想我迷路了。我应该从这个地址读取数据,但它看起来全是假的,我现在不知道该怎么办。” 内核(微笑着):“我能理解你的困惑。你拿到的地址并不是真正的地址,它是一个虚拟地址。” Alloca:“虚拟地址?那是什么意思?” 内核:“嗯,你所认为的内存并不是真正的物理内存,它是虚拟内存。而你手持的地址是一个虚拟地址。你需要的是物理地址,才能从物理内存中获取数据。” Alloca:“什么是虚拟内存?为什么不直接让我访问物理内存?” 内核:“让我们从基本原理来想想。我不仅负责你,还负责其他几百个进程的并发执行。你可能没有注意到,但此刻有许多其他进程正在和你一起执行。如果每个进程都能直接访问物理内存,你们如何协调谁该访问内存中的哪些地址呢?” Alloca:“那会很困难,因为我甚至不知道还有谁在运行,而且我想进程来来去去,所以这根本不可能实现。” 内核:“是的,这是第一个问题。即使你可以和其他进程通信,也会让系统变得极其缓慢,因为每次内存访问你都得问遍所有进程哪些地址可用。而且,这也是一个安全噩梦。某个进程的一个微小 bug 就可能破坏另一个进程的数据。” Alloca:“我明白问题了。那你如何解决这个问题呢?” 内核:“通过虚拟内存!基本上,我们需要解决两个问题。第一,每个进程都应该能够访问内存,而无需担心某个地址是否被其他进程使用。第二,内存访问应该安全,同时不能牺牲性能。” Alloca:“那么,虚拟内存是如何解决这些问题的呢?” 内核:“虚拟内存是一个软件构造体,它看起来和感觉上都像真正的内存,由你可以读写的一组地址组成。我给每个进程分配自己私有的虚拟内存空间,让它自由地导航和操作,而不用担心其他进程在使用这块内存。这就解决了第一个问题,它为每个进程隔离了内存。” Alloca:“但如果这些地址不是真的,那么读写操作到哪里去了?而且,安全性如何保证?” 内核:“这部分需要深入虚拟内存的工作细节,我现在简化一下。因为虚拟内存是一种抽象,它可以被我控制。我将进程使用的虚拟地址集合映射到对应的物理地址集合。而且,由于我知道其他进程正在使用哪些物理内存区域,我可以确保不会有两个进程最终共享相同的物理地址。”

关键收获

虚拟内存存在的根本原因是为了给进程提供内存级别的隔离。在一个多任务系统中,多个进程可以并行或时分运行,它们互相读取或写入对方的数据是不被允许的。通过给每个进程自己私有的虚拟内存,内核确保了这件事永远不会发生。每个进程都相信自己拥有整个物理内存的完全访问权,但实际上,那只是虚拟内存。在幕后,虚拟内存被映射到物理内存,并且每个进程的映射都不同。下一部分我们将了解这个映射是如何工作的。

关于叙事准确性的说明
在上面的场景中,Alloca 有意识地走到一个地址并注意到它是假的。这并不是进程体验内存的真实方式。在现实中,内存访问是由专用硬件(MMU)透明地拦截的,内核——进程从来不会注意到这些。但要准确解释这一点,需要先理解 MMU、页表以及内核如何处理内存事件,而我们还没有覆盖这些内容。一开始就讲那些,就像是用一个词去定义它自己。这就是为什么我们从简化的模型开始。随着各部分内容的推进,我们会逐步让心智模型变得更精确、更准确。

现在 Alloca 理解了虚拟内存为何存在,但她仍然不明白它是如何工作的以及它看起来像什么。她继续向内核提问。

Alloca:“如果我所看到的内存是虚拟的,那是说它是无限的吗?” 内核:“不是无限的,但非常大。告诉我,你知道 CPU 是如何表示地址的吗?” Alloca:“嗯,我知道在 x86-64 系统中,地址存储在 64 位寄存器中。所以我想那意味着我可以寻址 2⁶⁴ 字节?” 内核:“你可能会这么想,对吧?但有一点:虽然你的地址确实存储在 64 位寄存器中,但并非所有位都用于寻址。只有 48 位参与地址转换。” Alloca:“为什么只用 48 位?” 内核:“这是出于务实的考虑。想想看:48 位给你 2⁴⁸ 字节的可寻址空间,也就是 256 TiB。这太巨大了!任何应用程序现在都不需要接近这么大的空间。硬件设计者认为在可预见的未来这已经足够了,所以他们通过使用 48 位而不是完整的 64 位,使地址转换逻辑更简单。他们保留了未来扩展到 52 位或 56 位的余地,如果需要的话。” Alloca:“所以我有 256 TiB 的虚拟地址空间?那太庞大了!我能全部使用吗?” 内核:“啊,并非如此。你只能使用一半,也就是 128 TiB。我用地址空间的上半部分(128 TiB)来将我自己的代码和数据映射到每个进程的内存中。” Alloca:“你在我的地址空间里?” 内核:“我必须如此!当你进行系统调用或发生中断时,执行会切换到内核模式并开始运行我的代码。如果我的代码没有被映射到你的地址空间中,CPU 就不知道跳转到哪里。所以是的,我住在每个进程地址空间的上半部分。你不能直接访问我的内存,但它就在那里,随时准备从执行进入内核模式时使用。” Alloca:“好吧,但如此巨大的虚拟地址空间是怎么工作的?大多数机器只安装了很小的内存,比如 16 或 32 GB?” 内核:“这就是虚拟内存的精妙之处。你的虚拟地址空间完全独立于安装了多大的物理 RAM。即使这台机器只有 16 GB RAM,你的虚拟地址空间仍然跨越 256 TiB。从虚拟到物理的映射是两个世界连接的地方,这由我来管理。我精心确保这些映射保持在已安装物理内存的限制之内。”

关键收获

由于虚拟内存地址空间的虚拟特性,其大小远远大于已安装的 RAM。在常见的 48 位 x86-64 虚拟地址模式下,规范虚拟地址范围跨越 256 TiB。Linux 通常将其分成下半部分的用户空间和上半部分的内核空间。较低的 128 TiB 可供用户进程使用,而上半部分保留给进入内核模式时使用的内核映射。物理地址容量与虚拟地址容量不同,取决于 CPU 和平台。

Alloca:“你提到你把你的代码和数据映射到我地址空间的上半部分。那么我那一半地址空间中映射了什么呢?” 内核:“你那一半地址空间映射了你的代码和数据。” Alloca:“它看起来是怎样的?有特定的结构吗?” 内核:“是的,你的地址空间有一个特定的布局。它以段的形式组织,每一段都指定用于映射某种类型的数据。让我给你看看它的样子。”

内核做了个手势,Alloca 突然可以看到她虚拟内存的一张垂直地图

图 1:x86-64 Linux 上的规范虚拟地址空间布局。文本段、数据段和 BSS 段的大小在编译时确定。堆从数据区向上增长;栈从用户空间顶部附近向下增长。在它们之间,共享库和文件映射漂浮在巨大的中间区域。内核占据完整规范范围的上半部分(未按比例显示)。(https://substackcdn.com/image/fetch/$s_!InhV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2833505-2450-46a4-b342-a7431eb29083_1360x2240.png)

图 1:x86-64 Linux 上的规范虚拟地址空间布局。文本段、数据段和 BSS 段的大小在编译时确定。堆从数据区向上增长;栈从用户空间顶部附近向下增长。在它们之间,共享库和文件映射漂浮在巨大的中间区域。内核占据完整规范范围的上半部分(未按比例显示)。

内核:“最底部,在低地址处,是你的代码。这些是你要执行的指令。这个区域在我创建你时就被加载了。我们称之为文本段。” Alloca:“有道理。它上面是数据段,我猜它映射了所有其他数据?” 内核:“不是所有数据,而是特定类型的数据。代码中任何被初始化为非零值的全局和静态变量都加载到这里。例如,如果你——”

相似文章

Itanium C++ ABI中虚表的工作原理

Lobsters Hottest

一篇详细的博客文章,解释了Itanium C++ ABI中虚表(vtable)的实现方式,涵盖虚表结构、修饰名称和虚函数调度。

交换表、闪存友好的交换、swap_ops 等

Hacker News Top

本文介绍了 Linux 内核交换子系统的最新改进和未来计划,包括减少每页开销、基于 folio 的辅助函数,以及使交换更适配固态存储的努力。相关内容在 2026 年 Linux 存储、文件系统、内存管理与 BPF 峰会上进行了讨论。

@mem0ai: https://x.com/mem0ai/status/2054580022049198513

X AI KOLs Timeline

这篇文章解释了Codex CLI(OpenAI的开源编码代理)中的记忆机制。它描述了基于markdown文件的记忆架构、包含分阶段提取和整合的写入路径,以及使用关键词搜索的读取路径,所有设计都是为了可预测性和低检索成本。