Linux 虚拟内存(VM)子系统是操作系统中最为复杂且关键的组件之一。数十年来,随着硬件规模的扩大和工作负载的增长,用于管理物理内存和虚拟内存的机制经历了巨大的架构变革。
本文将深入探讨 Linux 内存管理中的核心挑战、历史上“逆向映射”(rmap)的引入,以及在 2026 年 Linux 存储、文件系统、内存管理和 BPF(LSFMM+BPF)峰会上提出的一项前沿提案——旨在解决长期存在的匿名内存追踪架构包袱。
第一部分:核心问题与逆向映射(rmap)的诞生
要理解现代 Linux 内存管理的发展,我们必须回到 2000 年代初期的 Linux 2.4 内核系列。
Linux 是一个虚拟内存系统。进程生成的每个虚拟地址都必须由硬件在页表(page table)中进行查找。页表项(PTE, Page Table Entry)要么将该虚拟地址映射到物理内存地址上,要么记录该页当前不存在(即触发缺页异常)。
传统虚拟内存的局限性
核心问题在于,页表是一种单向映射。已知一个进程上下文中的虚拟地址,可以轻松找到对应的物理地址。然而,在早期的原生内核中,没有简单的方法可以反向查出“究竟是哪个进程上下文中的哪一个页表项指向了某个特定的物理页”。
由于内存共享,情况变得更加复杂。例如,如果一个物理页存放的是共享 C 标准库的代码,那么可能会有数百个进程的页表项同时指向这同一个物理页。
在没有反向路径的情况下,管理物理内存的效率极低:
-
释放内存页: 在内核释放物理页之前,它必须找到并使指向该页的每一个页表项失效。唯一的办法就是扫描每一个进程的页表,递减引用计数,直到归零。
-
区域内存回收(Zone Reclaiming): 如果内核急需释放物理内存某个特定区域(Zone,物理内存的子结构)中的页,扫描虚拟内存是毫无用处的。内核在找到目标区域内的单个物理页之前,不得不筛查海量的无关页表项。
rmap 解决方案
2002 年,内核黑客 Rik van Riel 引入了“逆向映射”(rmap)补丁集。这个概念非常直接:创建一种数据结构,给定一个物理页,它能够返回指向该页的页表项(PTE)列表。
存放该信息最合理的地方是 struct page(系统内存映射表数组,系统中的每个物理页都对应一个这样的结构体)。Rik 的补丁在 page 结构体中增加了一个 pte_chain 成员,它指向一个由 PTE 指针组成的简单链表。
引入 rmap 后,内存管理变得极具响应性:
-
直接修改: 释放一个页变得轻而易举,因为所有相关的页表项都可以被同时找到并修改。
-
精准追踪页活跃度: 虚拟内存系统可以通过页的
pte_chain链表轻松检查其“已引用(referenced)”位,以确定该页真正的活跃程度(age),从而确保系统在内存紧张时精确淘汰正确的页。
优化内存开销
天下没有免费的午餐,rmap 当然也是有代价的:内存开销。在系统中的每个页上增加一个 12 字节的指针,会导致明显的内核膨胀。
为了减轻这种影响,rmap 补丁从 struct page 中移除了专用的等待队列(wait_queue_head_t,同样占用 12 字节)。因为进程真正需要排他性(独占)等待某个页的情况并不频繁,rmap 将这一职责转交给了规模小得多的等待队列哈希表。尽管偶尔会发生哈希冲突(导致进程被稍微提前唤醒),但其带来的巨大内存节省完全弥补了这一微小的性能折损。
第二部分:文件映射内存与匿名内存——架构的分水岭
随着 rmap 的演进,Andrea Arcangeli 在 2004 年对其进行了大幅重构,内核根据被追踪内存的类型,将逆向映射分成了两种完全不同的机制:文件映射内存(File-Backed Memory)和匿名内存(Anonymous Memory)。
理解这一区别,就能明白为什么 rmap 的其中一侧设计得依然优雅,而另一侧却演变成了极度复杂的抽象层。
| 特征 | 文件映射内存的逆向映射 | 匿名内存的逆向映射 |
| 定义 | 内存直接映射到磁盘上的实际文件(如二进制文件、共享库、mmap 文件)。 | 没有磁盘文件作为支撑的内存(如进程的堆、栈、MAP_ANONYMOUS 分配)。 |
| 核心关联结构 | struct address_space(直接绑定到文件的 inode)。 | struct anon_vma(与虚拟内存区域 VMA 绑定)。 |
| 查找路线 | Page $\rightarrow$ address_space $\rightarrow$ 区间树(Interval Tree) $\rightarrow$ VMA $\rightarrow$ PTE | Page $\rightarrow$ anon_vma 链表/树 $\rightarrow$ VMA $\rightarrow$ PTE |
| 架构稳定性 | 高。 文件在磁盘上的偏移量结构是静态的。即使有 100 个进程映射同一个文件,它们与文件数据的关系也是统一的。 | 低。 深度受到 fork() 和**写时复制(COW)**的影响,导致父子进程之间产生复杂的共享拓扑关系。 |
为什么匿名内存本质上更难处理
因为文件映射内存有一个明确且永久的锚点(磁盘上的文件),内核可以使用高效的区间树(Interval Tree)来精确查找哪些虚拟内存区域(VMA)映射了文件的特定部分。
而匿名内存没有文件,它仅存在于 RAM 中。当进程调用 fork() 时,子进程会复制父进程的页表,并且所有的匿名页都被标记为只读,以实现写时复制(COW)。如果其中任何一个进程试图写入该页,内核就会为修改者分配一个新的物理页。
为了在多代父子进程之间追踪这个动态分裂、共享和分化的复杂物理页网络,内核不得不依赖一个被称为 anon_vma 的复杂追踪结构。经过二十年对各种边缘情况的修修补补,这个追踪系统变得极其晦涩难懂。
第三部分:2026 年 LSFMM+BPF 峰会与“COW 上下文”提案
在 2026 年的 LSFMM+BPF 峰会上,开发者 Lorenzo Stoakes 针对这一结构性技术债务发难,直言不讳地指出,由于其极高的复杂性、内存开销以及严重的性能瓶颈,内核现有的匿名页逆向映射实现是一个“极其差劲的抽象”。
VMA 级追踪的问题
目前,匿名逆向映射是在虚拟内存区域(VMA)级别追踪关系的。由于一个现代进程(尤其是在大型多线程运行时或内存映射数据库中)很容易拥有数万个 VMA,系统需要管理的内核追踪对象数量大得惊人。
此外,现有的 rmap 代码必须在整个 fork 操作期间都持有锁。这引入了极高的锁竞争,严重损害了多核处理器上多线程应用的可扩展性(scalability)。
引入“COW 上下文”
Stoakes 展示了一个处于初级形态的激进替代方案:将匿名内存的追踪从 VMA 级别提升到 mm_struct(内存描述符)级别。
mm_struct 是描述整个进程地址空间的核心数据结构。由于每个进程只有一个 mm_struct,将匿名映射追踪移至此处可以瞬间削减系统需要维护的对象数量。Stoakes 的设计引入了一个独立的结构,称为 COW 上下文(COW Context)。
为什么不直接嵌入到 mm_struct 中?
COW 上下文不能简单地作为 mm_struct 内部的一个字段,因为COW 上下文的生命周期必须能够比其原始进程更长。
如果一个父进程 fork 了几个子进程然后退出,父进程的 mm_struct 会被销毁。然而,子进程们会继续通过 COW 共享原始的匿名页。父进程的 COW 上下文必须继续保存在内存中,以维持这一系列 fork 操作的谱系树模型,从而允许内核准确映射出哪些残存的子进程仍在共享这些页。
提议的查找机制如何工作
-
每个内存页(严格来说是每个
folio)都会获得一个直接指针,指向最先映射它的进程的 COW 上下文。 -
当内核需要对该 folio 进行逆向映射时,它会顺着指针找到位于该进程层级结构最顶部的 COW 上下文。
-
随后,内核只需清晰地向下遍历 COW 上下文树,即可锁定每一个可能仍持有有效映射的进程。
为了保持查找的极速响应,补丁集实现了一种优化:动态地将 folio 链接到当前活跃层级结构中尽可能低的 COW 上下文上。
此外,这种新架构允许在读写锁保护(RCU, Read-Copy-Update)下进行查找,而不是依赖沉重的自旋锁或互斥锁。虽然转向 RCU 消除了锁竞争,但也带来了数据竞争的挑战,这意味着 Stoakes 未来要么需要引入一种具有映射粒度的锁,要么通过将释放页表的时间推迟到 RCU 宽限期(grace period)结束,来强迫系统在一定程度上容忍临时的并发冲突。
结语:前行之路
从 Rik van Riel 具有里程碑意义的 2.4 rmap 补丁,到 Lorenzo Stoakes 在 2026 年提出的宏大 COW 上下文提案,Linux 内存管理的哲学始终如一:用局部的内存开销或软件设计复杂性,来换取系统整体性能、可扩展性以及可预测性的巨大飞跃。
Stoakes 在 LSFMM+BPF 峰会上坦言,COW 上下文补丁集目前还是一个实验性的研究项目,还有很多粗糙的地方。然而,无论这一特定实现最终是并入主线内核,还是为某种混合设计铺平道路,它都代表了让 Linux 在面对下一代硬件基础设施时继续高效扩展所必经的、长期的架构重构之路。

653

被折叠的 条评论
为什么被折叠?



