这篇发表于SOSP '25的论文提出了一种革命性的内存管理系统CortenMM,旨在同时解决现代操作系统内存管理中的性能瓶颈 和并发正确性两大核心挑战。以下是对该论文的详细解读。
一、问题背景与核心洞察
1.1 传统内存管理系统的根本问题
现代操作系统(如Linux)的内存管理系统采用双层抽象设计:
- 软件层抽象:如Linux中的VMA(Virtual Memory Area)树,用于表示进程地址空间
- 硬件层抽象:即页表(Page Table),由MMU直接使用
这种设计源于1986年的SunOS,初衷是:
- 提高可移植性:适配不同MMU硬件架构(如段式、哈希页表等)
- 支持高级语义:如按需分页(on-demand paging)、写时复制(COW)等
然而,在现代计算环境中,这种设计已成为性能和正确性的主要瓶颈:
性能问题(Scalability Bottleneck)
- Linux内存管理在多核环境下存在严重的可扩展性问题
- 即使经过多次优化(如引入per-VMA锁),仍无法满足现代多核应用需求
- 实际影响:Android应用启动慢、Google Fibers线程创建性能差、TCP零拷贝效率低
正确性问题(Concurrency Bugs)
- 双层抽象需要在两个复杂数据结构间进行同步,极易引入并发竞态条件
- 论文统计:仅2023-2025年间,Linux因内存管理并发bug就产生了10个CVE
- 9个可导致系统崩溃
- 5个可导致信息泄露或权限提升等严重安全漏洞
1.2 核心洞察:软件层抽象已过时
论文提出一个关键洞察:现代操作系统不再需要软件层抽象,原因如下:
-
硬件趋同 :主流ISA(x86、ARM、RISC-V)都采用多级基数树(radix tree)页表
- 硬件差异极小,可通过语言特性(如C宏、Rust trait)处理
- 不再需要通用的软件抽象层来屏蔽硬件差异
-
高级语义可直接实现:支持高级内存语义所需的额外状态信息
- 可直接关联到页表项(PTE)
- 无需维护独立的软件层数据结构
根本结论 :双层抽象设计在现代环境下已成为不必要的复杂性来源,既影响性能又增加bug风险。
二、CortenMM的设计创新
2.1 单层抽象架构(Single-Level Abstraction)
CortenMM彻底摒弃了传统的VMA等软件层抽象,采用纯页表为中心的设计:
数据结构设计
- 页描述符(Page Descriptor) :每个页表页(PT page)关联一个描述符
- 包含一个锁(用于并发控制)
- 包含每PTE元数据数组(per-PTE metadata array)
- 元数据数组 :存储高级语义所需的状态信息
- 虚拟页状态(无效、已映射、匿名分配、文件映射、已交换等)
- 额外状态(访问权限、文件描述符、设备ID、偏移量等)
这种设计的优势:
- 简化架构:消除VMA与页表间的同步复杂性
- 减少内存开销:避免维护两套数据结构
- 提高性能:减少不必要的同步开销
2.2 事务型接口(Transactional Interface)
CortenMM引入了一个事务型编程接口,作为操作MMU的唯一方式:
接口设计
// 获取指定虚拟内存区域的游标
pub fn lock(&self, r: Range<Vaddr>) -> RCursor;
// RCursor支持的基本操作
pub fn query(&mut self, addr: Vaddr) -> Status; // 查询页状态
pub fn map(&mut self, addr: Vaddr, page: PhysPage); // 映射物理页
pub fn mark(&mut self, range: Range<Vaddr>, status: Status); // 标记状态
pub fn unmap(&mut self, range: Range<Vaddr>); // 取消映射
并发控制语义
- 原子性:事务内所有操作原子执行
- 隔离性:仅当事务操作的内存区域重叠时才需要串行化
- 自动资源管理:RCursor析构时自动释放锁
这种设计将并发控制与业务逻辑解耦,大幅简化了复杂操作的实现。
2.3 可扩展的锁协议
CortenMM提供了两种锁协议实现:
CortenMMrw(基于读写锁)
- 遍历阶段:获取读锁,向下遍历页表
- 操作阶段:在覆盖目标区域的最低层页表页上获取写锁
- 简单高效:适用于大多数场景
CortenMMadv(基于RCU + 自旋锁)
- 遍历阶段:无锁遍历(利用RCU读临界区)
- 锁定阶段:锁定覆盖页表页及其所有后代
- 处理PT页释放 :
- 使用RCU延迟释放机制
- 标记stale状态,避免use-after-free
- 检测到stale状态时自动重试
两种协议都实现了两阶段锁定,确保事务的原子性,同时最大化并发性能。
2.4 高级内存语义支持
尽管移除了软件层抽象,CortenMM仍完整支持Linux的关键特性:
按需分页(On-demand Paging)
mmap时仅在元数据中标记虚拟页为"已分配"- 页错误时才实际分配物理页并更新页表
写时复制(Copy-on-Write)
- 在元数据中维护两个额外位:
- shared bit:标记页面是否被多个进程共享
- writable bit:标记页面是否实际可写
fork时设置shared bit,页表设为只读- 写页错误时检查两个位,决定是否需要复制
其他特性
- 页面交换(swap)
- 反向映射(reverse mapping)
- 大页支持(4KB、2MB、1GB)
- 文件映射
2.5 可移植性设计
CortenMM通过Rust trait抽象ISA差异:
pub trait PageTableEntryTrait {
fn is_present(&self) -> bool;
}
// x86-64实现
impl PageTableEntryTrait for x86::PageTableEntry {
fn is_present(&self) -> bool {
self.0 & x86::PteFlags::PRESENT.bits() != 0
|| self.0 & x86::PteFlags::HUGE.bits() != 0
}
}
// RISC-V实现
impl PageTableEntryTrait for riscv::PageTableEntry {
fn is_present(&self) -> bool {
self.0 & riscv::PteFlags::V.bits() != 0
}
}
这种设计使得移植到新架构只需实现trait,无需修改核心逻辑。
三、形式化验证与正确性保证
3.1 验证策略
CortenMM使用Verus(基于Rust的SMT验证器)对核心代码进行形式化验证:
验证目标
- 互斥性(Mutual Exclusion):锁协议正确串行化重叠区域的操作
- 功能正确性:基本操作(map/unmap/query/mark)的正确性
- 页表良构性(Well-formedness):页表始终保持有效树结构
验证方法
- 状态机精化(State Machine Refinement) :
- 顶层规范:Atomic Spec(抽象互斥性)
- 中层规范:Atomic Tree Spec(具体页表状态)
- 底层实现:实际代码
- 模块化验证 :
- AddrSpace模块:锁协议
- RCursor模块:页表操作
3.2 验证成果
- 验证范围:1769行核心实现代码
- 验证工作量:8人月,证明代码4279行(证明/代码比5.2:1)
- 验证时间:<20秒
- 验证保证 :
- 锁协议的互斥性
- 基本操作的功能正确性
- 页表的良构性不变量
3.3 安全Rust的辅助作用
- Safe Rust强制 :
#![deny(unsafe_code)] - 双重保障 :
- 防止数据竞争和内存安全问题
- 强制所有MMU访问必须通过验证的事务接口
四、性能评估结果
4.1 微基准测试
单线程性能
- CortenMMadv在4/5个微基准中优于Linux(+7.8%到+46.8%)
- 仅在纯
mmap操作中略慢(-3.1%),因为需要初始化页表
多线程可扩展性(384核)
- 低竞争场景 :
unmap-virt:比Linux快2270×PF(页错误):比Linux快33×
- 高竞争场景 :
unmap:比Linux快1489×PF:比Linux快3×
4.2 真实应用性能
JVM线程创建
- 384核下比Linux快32%
- 解决了Android应用启动的性能瓶颈
metis(MapReduce)
- 384核下比Linux快26×
- 比RadixVM快1.24×(RadixVM在128核后崩溃)
dedup和psearchy
- 使用默认分配器(ptmalloc):比Linux快2-2.7×
- 使用tcmalloc:仍优于Linux,但内存开销增加2×
4.3 其他指标
内存开销
- 与Linux相当(<2%额外开销)
- 远低于RadixVM(页表复制导致高内存开销)
可移植性
- x86到RISC-V移植:252行代码(Linux需要699行)
- 支持Intel MPK:82行代码(Linux需要273行)
五、局限性与未来工作
5.1 当前局限性
- 架构限制 :仅适用于多级基数树页表的ISA
- 不支持段式、哈希页表等老式MMU
- NUMA策略缺失:当前未优化NUMA内存放置
- fork性能:比Linux慢17.7%(需要遍历页表而非VMA)
5.2 未来改进方向
- NUMA支持:在per-PTE元数据中存储NUMA策略
- ARM支持:处理ARM特有的MMU特性(如contiguous bit、break-before-make)
- 现有OS集成:探索将单层抽象思想应用到Linux等成熟系统
六、总结与意义
6.1 技术贡献
- 根本性洞察:揭示双层抽象是现代内存管理性能和正确性问题的根源
- 架构创新:提出单层抽象设计,简化系统复杂性
- 工程实现:构建完整的、高性能的内存管理系统
- 验证方法:展示如何对复杂并发系统进行实用的形式化验证
6.2 实际意义
- 性能提升:在真实应用中获得1.2×-26×的性能提升
- 安全性增强:通过形式化验证消除并发bug风险
- 工程价值:为未来操作系统设计提供新范式
6.3 学术影响
CortenMM证明了简化架构 + 形式化验证 + 现代语言特性可以构建出既高性能又高可靠的系统软件。这种方法论不仅适用于内存管理,也可推广到操作系统的其他组件,为构建可信计算基础设施提供了重要参考。
该工作代表了操作系统设计从"复杂但经过充分测试"向"简洁且经过数学证明"的重要转变,是系统软件工程领域的一个里程碑式成果。
论文中说VMA可以删除,为什么?
论文中说可以删除VMA(Virtual Memory Area,虚拟内存区域)抽象层,这看起来很激进,因为VMA在Linux等传统操作系统中承担着重要功能。让我详细解释为什么现在可以删除VMA,以及CortenMM是如何替代VMA的功能的。
一、VMA的传统作用
在Linux等传统系统中,VMA主要承担以下功能:
1. 地址空间表示
- VMA树表示进程的整个虚拟地址空间
- 每个VMA代表一个连续的虚拟内存区域,具有相同的属性(权限、映射类型等)
2. 高级内存语义支持
- 按需分页 :
mmap时只创建VMA,实际页表项在页错误时才创建 - 写时复制 :
fork时复制VMA但共享物理页,页表设为只读 - 文件映射:记录文件描述符、偏移量等信息
- 页面交换:记录交换设备和块号信息
3. 硬件抽象
- 屏蔽不同MMU架构的差异(段式、页式、哈希页表等)
二、为什么现在可以删除VMA?
2.1 硬件环境发生了根本变化
关键洞察 :现代主流ISA(x86、ARM、RISC-V)都采用几乎相同的多级基数树页表结构。
- 过去 (1980s-1990s):MMU架构多样
- Intel x86:段式 + 页式
- PowerPC:哈希页表
- Some architectures:反向页表、软件TLB等
- 现在 :统一的4级/5级页表结构
- x86-64:PML4 → PDP → PD → PT
- ARM64:PGD → PUD → PMD → PTE
- RISC-V:类似结构
这意味着硬件抽象的需求大大降低,差异可以通过简单的语言特性(如C宏、Rust trait)处理,不需要复杂的VMA层。
2.2 VMA带来的问题超过了其价值
性能问题
- 额外同步开销:每次页表操作都需要同时锁定VMA和页表
- 可扩展性瓶颈:即使有per-VMA锁,VMA树本身的遍历和修改仍有竞争
正确性问题
- 状态不一致风险:VMA和页表必须保持同步,否则会导致严重bug
- 复杂锁协议:需要维护两套数据结构的一致性,锁逻辑极其复杂
论文中提到的10个CVE就是这种复杂性的直接后果。
三、CortenMM如何替代VMA的功能?
3.1 地址空间表示 → 直接使用页表
传统方式:
VMA Tree: [0x1000-0x2000] → [0x3000-0x5000] → ... Page Table: 需要与VMA保持同步
CortenMM方式:
Page Table + Per-PTE Metadata Array
页表本身就天然表示了地址空间的布局,不需要额外的VMA树来重复这个信息。
3.2 高级语义支持 → Per-PTE元数据数组
这是CortenMM最关键的创新!每个页表页(PT page)关联一个per-PTE元数据数组:
// 元数据存储虚拟页的各种状态
pub enum Status {
Invalid, // 无效页
Mapped(PhysPage, Perm), // 已映射到物理页
PrivateAnon(Perm), // 私有匿名页(未分配物理页)
PrivateFileMapped(File, Offset, Perm), // 文件映射页
Swapped(BlockDev, BlockNum, Perm), // 已交换到磁盘的页
// ... 其他状态
}
具体功能实现:
按需分页:
mmap时:调用rcursor.mark(range, Status::PrivateAnon(perm))- 页错误时:检查状态为
PrivateAnon,分配物理页并调用rcursor.map()
写时复制:
- 在元数据中维护额外的标志位(shared bit, writable bit)
- 不需要VMA来记录这些信息
文件映射:
- 元数据直接存储
File、Offset等信息 - 页错误时根据这些信息从文件读取数据
页面交换:
- 元数据存储
BlockDev、BlockNum等交换信息
3.3 硬件抽象 → Rust Trait
如论文图9所示,通过Rust trait抽象不同架构的差异:
pub trait PageTableEntryTrait { fn is_present(&self) -> bool; } // x86实现 impl PageTableEntryTrait for x86::PageTableEntry { ... } // RISC-V实现 impl PageTableEntryTrait for riscv::PageTableEntry { ... }
这种方式比VMA更轻量、更直接。
四、删除VMA带来的好处
4.1 性能提升
- 减少内存访问:不需要遍历VMA树
- 减少锁竞争:只需要锁定页表相关部分,不需要VMA锁
- 更好的缓存局部性:页表和元数据在内存中更紧凑
4.2 正确性保证
- 消除状态不一致:只有一个数据源(页表+元数据),不存在同步问题
- 简化并发控制:锁协议只针对页表层次结构,逻辑清晰
- 便于形式化验证:单一数据结构比两个需要同步的数据结构更容易验证
4.3 代码简化
- 减少代码量:不需要维护VMA相关的数据结构和算法
- 降低复杂度:内存管理逻辑更直观,更容易理解和维护
五、什么情况下仍然需要类似VMA的抽象?
论文也承认了CortenMM的局限性:
-
非基数树MMU架构:如果目标架构使用段式、哈希页表等,可能仍需要某种抽象层
-
需要快速地址空间遍历的操作:
- 如
fork()操作需要复制整个地址空间 - CortenMM需要遍历整个页表,而VMA可以快速枚举所有区域
- 这也是为什么论文中CortenMM的
fork性能比Linux慢17.7%
- 如
-
复杂的内存策略:某些高级NUMA策略或内存压缩可能需要额外的元数据组织方式
总结
删除VMA之所以可行,是因为:
- 硬件趋同:现代CPU都使用相似的页表结构
- 功能替代:per-PTE元数据数组可以承载VMA的所有语义信息
- 收益大于成本:消除VMA带来的性能提升和正确性保证远超过失去的便利性
这体现了系统设计的一个重要原则:当底层硬件环境发生变化时,上层抽象也应该相应演进,避免维护过时的复杂性。CortenMM正是这一原则的完美体现。
删除 vma 后,在mmap时,怎么才能知道哪些虚拟地址是空闲的?
删除VMA后,确实失去了一个高效的虚拟地址空间管理机制。让我详细解释CortenMM是如何解决虚拟地址分配问题的。
传统VMA方式的问题
在Linux中,VMA树不仅记录已分配的区域,还隐式表示了空闲区域。通过遍历VMA树,可以快速找到合适的空闲地址范围。
但正如论文指出的,这种方式有严重问题:
- VMA树操作需要复杂的锁协议
- 在多核环境下扩展性差
- 容易产生并发bug
CortenMM的解决方案
1. Per-Core虚拟地址分配器(核心优化)
论文在4.5节明确提到了这个关键优化:
"Optimization: per-core virtual address allocator. Following prior work [36], to maximize scalability, CortenMM makes the virtual address allocator per core, and each core owns a private share of the address space. This avoids the contention on concurrent allocation and freeing."
具体实现方式:
- 每个CPU核心拥有自己私有的虚拟地址空间范围
- 核心内的地址分配不需要与其他核心同步
- 大大减少了并发竞争
这种设计借鉴了之前的研究工作(如MOSBENCH中的发现),是现代高性能系统常用的技术。
2. 基于页表的空闲地址发现
虽然没有VMA,但CortenMM仍然可以通过页表结构本身来发现空闲地址:
方法一:页表项状态检查
- 遍历页表层次结构
- 检查PTE的状态(通过per-PTE元数据)
- 如果PTE为
Invalid状态,说明对应的虚拟地址未使用
方法二:稀疏页表优化
- 现代页表本身就是稀疏的
- 如果某一级页表指针为空,说明整个子树对应的地址范围都是空闲的
- 可以快速跳过大片空闲区域
3. 地址分配策略
对于mmap(addr, len)系统调用:
指定地址的情况(addr != 0)
rust
fn do_syscall_mmap(offset: Vaddr, size: usize, perm: Perm) { let range = offset..offset + size; let mut rcursor = this_addr_space!().lock(range)?; if rcursor.query(range) { /* 检查是否已存在 */ } rcursor.mark(range, Status::PrivateAnon(perm)); }
- 直接锁定指定范围
- 查询该范围的状态
- 如果是空闲的(Invalid状态),就标记为已分配
未指定地址的情况(addr == 0)
- 使用per-core地址分配器
- 从核心私有的地址池中分配
- 或者遍历页表寻找足够大的连续空闲区域
4. 性能考虑和权衡
论文在6.2节提到了一个重要的性能数据:
- CortenMM在纯
mmap操作上比Linux慢3.1% - 但在
mmap-PF(mmap后立即访问)场景下比Linux快7.8%
这说明:
- 地址查找确实有开销,但这是可接受的权衡
- 整体性能更重要:避免了VMA带来的更大开销和复杂性
5. 实际实现细节
虽然论文没有给出完整的地址分配算法,但我们可以推断其实现可能包含:
分层地址管理
- 维护一些轻量级的数据结构来跟踪大块空闲区域
- 例如:位图(bitmap)或区间树,但比VMA简单得多
- 这些结构只用于地址分配,不参与页表操作的同步
缓存友好设计
- 利用程序的局部性原理
- 优先分配最近使用的地址附近的区域
- 减少页表遍历的深度
延迟初始化
- 不需要预先填充所有页表层级
- 只在真正需要时才创建中间页表页
- 这本身就提供了空闲区域的信息
为什么这种方法可行?
1. 硬件特性支持
现代CPU的页表遍历硬件已经非常高效,软件遍历的开销相对较小。
2. 工作负载特征
- 大多数应用不会频繁进行大范围的虚拟地址分配
- 即使需要,per-core分配器也能很好地处理并发场景
3. 简化带来的收益
- 虽然地址查找稍慢,但消除了VMA同步的巨大开销
- 在多核环境下,整体性能反而更好(见论文的多线程测试结果)
总结
CortenMM通过以下方式解决删除VMA后的地址分配问题:
- Per-core虚拟地址分配器:主要手段,避免并发竞争
- 页表状态查询:通过事务接口查询地址状态
- 稀疏页表遍历:利用页表本身的稀疏性快速发现空闲区域
- 轻量级辅助数据结构:可能使用简单的位图或区间记录大块空闲区域
这种方法体现了CortenMM的设计哲学:用更简单、更可验证的机制替代复杂的传统方案,即使在某些特定操作上略有性能损失,但在整体系统性能和正确性上获得更大收益。