6.1810 / 6.1810 · 第 10 课 · rtm

应用虚拟内存

操作系统内核使用虚拟内存实现按需分页、共享内存等机制。Appel & Li (1991) 的核心论点:用户级应用同样能从虚拟内存原语中获益——并发 GC、检查点、分布式共享内存等都可通过缺页异常驱动。

授课教师:rtm

日期:2025 年春季

难度:中等

阅读:VM Primitives for User Programs (Appel & Li, 1991)

应用级虚拟内存原语

论文提出的应用级 VM 原语:

原语作用
Trap在用户态处理缺页异常
Prot1降低一个页面的可访问性
ProtN降低 N 个页面的可访问性
Unprot提升一个页面的可访问性
Dirty返回自上次调用以来被修改的页面列表
Map2将同一物理页映射到两个不同 VA,使用不同保护级别

xv6 不支持任何这些原本。好的操作系统应该支持它们。今天的 Unix 呢?

Unix 今天的 VM 接口

mmap() —— 映射内存到地址空间

// 映射文件
mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, offset);

// 匿名内存(优于 sbrk)
mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

mprotect() —— 改变映射的保护级别

mprotect(addr, len, PROT_READ);     // 只读
mprotect(addr, len, PROT_NONE);    // 不可访问,访问将触发异常

munmap() —— 移除映射

munmap(addr, len);

sigaction() —— 注册信号处理器

act.sa_sigaction = handle_sigsegv;
act.sa_flags = SA_SIGINFO;
sigemptyset(&act.sa_mask);
sigaction(SIGSEGV, &act, NULL);

Linux 额外 VM 调用

madvise(), mincore(), mremap(), msync(), mlock(), mbind(), shmat() 等。

用 Unix 接口实现论文原语

原语Unix 实现是否完全支持
Trapsigaction + SIGSEGV是
Prot1mprotect()是
ProtNmprotect()是
Unprotmprotect()是
Dirty无直接支持,但有 workaround部分
Map2不直接,但 shm_open+mmap 可模拟部分

VM 实现基础

VMA(虚拟内存区域)

地址空间由 VMA 和页表组成。每个 VMA 是一段连续的虚拟地址范围,具有相同的权限,由同一对象(文件或匿名内存)支持。VMA 帮助内核决定如何处理缺页。

Trap/sigaction 实现流程

  1. PTE(或 TLB 条目)标记为"受保护"
  2. CPU 保存用户态,跳入内核
  3. 内核询问 VM 系统如何处理(从磁盘换入?核心转储?)
  4. VM 系统查看 VMA
  5. 生成信号——upcall 进入用户进程
  6. 运行用户处理器(必须调用 UNPROT 避免重复缺页)
  7. 用户处理器返回内核
  8. 内核返回用户程序

演示:单页支持的大平方根表

思路

应用代码认为有一个预计算的大表 sqrts[n]。表大于物理内存,但用户级 VM 原语只让它占用一个物理页。每次访问新项时触发缺页,处理器计算并解保护该页。

实测(2024):一次缺页约 10233μs / 1000 = ~10μs;一次加法约 0.417ns,即 ~24000 次加法等价于一次缺页。


用例 1:并发 GC(Baker 实时算法)

概述

传统实现需要语言/编译器支持(插桩 load/store),而用户级 VM 可以避免插桩,更快。

Baker 算法(see baker.c)

堆分为 from-space 和 to-space。to-space 进一步分为 scanned、unscanned 和 new:

  1. GC 开始:所有对象在 from-space
  2. 将根(寄存器和栈)复制到 to-space,from-space 中留下转发指针
  3. 每次分配或解引用 from-space 指针时,复制对象到 to-space
  4. 扫描完成后,from-space 全部空闲

为什么吸引人?

代价

解决方案:让应用使用 VM

复制根之后,取消映射 to-space 的 unscanned 部分。当线程访问 unscanned 区域时触发缺页:

易于并发化

回收器线程可与应用线程并发运行:回收器扫描完一页后 UNPROT 即可。唯一需要的同步是"哪个线程在扫描哪一页"。

现状:很少有系统使用 Baker 算法(它复制所有存活对象),但 VM 思想很酷。


用例 2:虚拟机/容器迁移

将进程从机器 M1 迁移到 M2(如升级 M1)。朴素方案:检查点 P → 复制到 M2 → 重启 P。问题是复制内存可能很慢。

用户级 VM 方案

  1. 停止 M1 上的 P
  2. 检查点 P(除内存映像外)
  3. 在 M2 启动检查点,但取消映射其内存映像
  4. 后台开始从 M1 获取内存映像
  5. 重启 P 后,访问未获取的内存时触发缺页 → 从 M1 获取该页

更快的原因

可以重叠发送内存与运行 P 的时间。需要 Trap 和 ProtN 原语。

变体:热迁移

P 继续在 M1 运行,同时复制检查点(含内存)到 M2。跟踪 P 修改的页面,取消映射内存,缺页时记录该页需发送。


1991 到 2022 的变化


测验

Q1:Appel & Li 论文提出的 6 个用户级 VM 原语中,哪一项 Unix 无法直接支持?

Q2:Baker 实时 GC 算法使用 VM 的主要目的是什么?

Q3:在 Baker 算法的 VM 实现中,当应用线程访问 unscanned 区域时会发生什么?

Q4:使用用户级 VM 进行虚拟机迁移的主要好处是什么?

Q5:为什么论文时代的缺页陷阱(~1200μs)并非 GC 的障碍?

要点总结

VM 原语的力量

Trap、Prot1/ProtN、Unprot、Dirty、Map2 让应用利用缺页驱动计算。

并发 GC 的 VM 方案

避免插桩 load/store,用缺页驱动扫描,易于并发化。

VM 迁移

取消映射内存,缺页时按需获取,可重叠传输与执行。

持续演进

1991 至今,VM API 持续扩展,应用对 VM 的控制力不断增强。

延伸阅读