论文解读:CortenMM- Efficient Memory Management with Strong Correctness Guarantees

这篇发表于SOSP '25的论文提出了一种革命性的内存管理系统CortenMM,旨在同时解决现代操作系统内存管理中的性能瓶颈并发正确性两大核心挑战。以下是对该论文的详细解读。

一、问题背景与核心洞察

1.1 传统内存管理系统的根本问题

现代操作系统(如Linux)的内存管理系统采用双层抽象设计

  • 软件层抽象:如Linux中的VMA(Virtual Memory Area)树,用于表示进程地址空间
  • 硬件层抽象:即页表(Page Table),由MMU直接使用

这种设计源于1986年的SunOS,初衷是:

  1. 提高可移植性:适配不同MMU硬件架构(如段式、哈希页表等)
  2. 支持高级语义:如按需分页(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 核心洞察:软件层抽象已过时

论文提出一个关键洞察:现代操作系统不再需要软件层抽象,原因如下:

  1. 硬件趋同 :主流ISA(x86、ARM、RISC-V)都采用多级基数树(radix tree)页表

    • 硬件差异极小,可通过语言特性(如C宏、Rust trait)处理
    • 不再需要通用的软件抽象层来屏蔽硬件差异
  2. 高级语义可直接实现:支持高级内存语义所需的额外状态信息

    • 可直接关联到页表项(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>);       // 取消映射
并发控制语义
  1. 原子性:事务内所有操作原子执行
  2. 隔离性:仅当事务操作的内存区域重叠时才需要串行化
  3. 自动资源管理: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验证器)对核心代码进行形式化验证:

验证目标
  1. 互斥性(Mutual Exclusion):锁协议正确串行化重叠区域的操作
  2. 功能正确性:基本操作(map/unmap/query/mark)的正确性
  3. 页表良构性(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)]
  • 双重保障
    1. 防止数据竞争和内存安全问题
    2. 强制所有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快

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 当前局限性

  1. 架构限制 :仅适用于多级基数树页表的ISA
    • 不支持段式、哈希页表等老式MMU
  2. NUMA策略缺失:当前未优化NUMA内存放置
  3. fork性能:比Linux慢17.7%(需要遍历页表而非VMA)

5.2 未来改进方向

  1. NUMA支持:在per-PTE元数据中存储NUMA策略
  2. ARM支持:处理ARM特有的MMU特性(如contiguous bit、break-before-make)
  3. 现有OS集成:探索将单层抽象思想应用到Linux等成熟系统

六、总结与意义

6.1 技术贡献

  1. 根本性洞察:揭示双层抽象是现代内存管理性能和正确性问题的根源
  2. 架构创新:提出单层抽象设计,简化系统复杂性
  3. 工程实现:构建完整的、高性能的内存管理系统
  4. 验证方法:展示如何对复杂并发系统进行实用的形式化验证

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来记录这些信息

文件映射

  • 元数据直接存储FileOffset等信息
  • 页错误时根据这些信息从文件读取数据

页面交换

  • 元数据存储BlockDevBlockNum等交换信息

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的局限性:

  1. 非基数树MMU架构:如果目标架构使用段式、哈希页表等,可能仍需要某种抽象层

  2. 需要快速地址空间遍历的操作

    • fork()操作需要复制整个地址空间
    • CortenMM需要遍历整个页表,而VMA可以快速枚举所有区域
    • 这也是为什么论文中CortenMM的fork性能比Linux慢17.7%
  3. 复杂的内存策略:某些高级NUMA策略或内存压缩可能需要额外的元数据组织方式

总结

删除VMA之所以可行,是因为:

  1. 硬件趋同:现代CPU都使用相似的页表结构
  2. 功能替代:per-PTE元数据数组可以承载VMA的所有语义信息
  3. 收益大于成本:消除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后的地址分配问题:

  1. Per-core虚拟地址分配器:主要手段,避免并发竞争
  2. 页表状态查询:通过事务接口查询地址状态
  3. 稀疏页表遍历:利用页表本身的稀疏性快速发现空闲区域
  4. 轻量级辅助数据结构:可能使用简单的位图或区间记录大块空闲区域

这种方法体现了CortenMM的设计哲学:用更简单、更可验证的机制替代复杂的传统方案,即使在某些特定操作上略有性能损失,但在整体系统性能和正确性上获得更大收益

相关推荐
闲人编程32 分钟前
Django中间件开发:从请求到响应的完整处理链
python·中间件·性能优化·django·配置·codecapsule
❆VE❆32 分钟前
【技术分享】前端跨窗口/标签页面通信:掌握以下几种方法,提升用户体验(附带常用场景以及典例)
前端·javascript·性能优化·vue·跨标签通信
AWS官方合作商36 分钟前
AWS GPU实例深度解析:G系列与P系列如何选择?
云计算·aws
北辰浮光5 小时前
[Amplify]项目aws-amplfiy服务旧版本升级v6经验分享
云计算·aws
年小个大8 小时前
优化App启动时间?startup-coroutine是什么?
性能优化·架构·kotlin
ZouZou老师10 小时前
FFmpeg性能优化经典案例
性能优化·ffmpeg
七夜zippoe11 小时前
使用Ollama在消费级硬件上运行大模型:从环境配置到企业级实战
性能优化·大模型·模型量化·ollama
2401_8612775513 小时前
Web应用程序、服务器、数据库性能测试工具Jemeter使用方法与举例说明
性能优化·压力测试
6***S22215 小时前
SQL Server Management Studio的使用
数据库·oracle·性能优化