背景

CoCo VM(TDX、SEV-SNP)的内存分 shared 与 private 两种属性,转换是运行期的常见动作:guest 启动时把内存转成 private,与宿主做 DMA 共享数据时需要 shared 页。此前 memslot 有两个后备,shared 页来自 userspace_addr 映射,private 页来自 guest_memfd。要改一个范围的属性,得先对这个范围 tear down,hole-punch 加 zap stage 2 页表,再按新状态重新 populate。普通 VM 重建页表只是性能损失,CoCo 不一样:zapping 是不可逆的破坏性操作,guest 每次 fault 之后还要重新 accept 页,安全语义决定它不会 accept 自己没有显式请求过 fault 的页。

旧转换要 tear down 再重建
┌────────────────────────────────┐
│ shared 与 private 分属两个后备 │
└───────────────┬────────────────┘
                ▼
┌────────────────────────────────┐
│ 转换先 hole-punch 再 zap 页表  │
└───────────────┬────────────────┘
                ▼
┌────────────────────────────────┐
│  重建后 CoCo 每页重新 accept   │
└────────────────────────────────┘

in-place 转换让同一批页原地换属性,不再搬移与重建。本系列把这条路径建在 guest_memfd 里,属性按 per-gmem 记录。

问题

  • shared 与 private 两个后备并存,转换要 tear down 再重建
  • stage 2 页表 zapping 不可逆,CoCo 每页还要重新 accept
  • 转换前的页可能仍被内核或 userspace 引用,直接转会有人写入 private 页
  • 已处于目标状态的范围也要挨 zap,整 DRAM 转换会被毁数据
  • 转换路径的新开销不应波及未启用转换的 VM

方案

一个后备:memslot 只认 guest_memfd

启用 in-place 转换的 memslot 打上 KVM_MEMSLOT_GMEM_ONLY,shared 与 private 的缺页都从同一个 guest_memfd 满足。userspace_addr 仍供 kvm_read_guest() 这类访问使用,指错了算用户错误,内核不做校验。单一后备让失效简单化:PUNCH_HOLE 保证只打到这个 guest_memfd 的内存,不会像 per-VM 属性时代那样,hole-punch 一个 private guest_memfd 时误 zap 掉 VMA 里毫不相干的 shared 映射。TDX 与 SEV 的初始化接口相应把源页参数改为可选,原地转换不再需要单独的源页。

增量转换与精确 zap

新 ioctl KVM_SET_MEMORY_ATTRIBUTES2 复用原编号但带写回语义:属性记录在 maple tree 里,失败时把失败偏移写进 error_offset 并返回 -EAGAIN,userspace 可以就地重试或处理,代替全有或全无的复杂回滚。两处省力设计:范围已经全是目标属性时直接返回(ARM CCA guest 开机要把整片 DRAM 转 private,若没有 shared 页就整个跳过;只要混着一个 shared 页,转换就得进行,但 zap 必须只落在 shared 映射上,否则连同 private 数据一起毁掉);失效 helper 改收映射类型 filter,shared 转 private 只 zap shared 映射,不再两边全打。

转换前确保页未被使用

转 private 之前必须确认没有别处能写这页,否则当前的写入者会落笔到 guest 的 private 内存。解除映射后逐 folio 检查 refcount:新分配的 folio 会挂在 per-CPU lru_add fbatch 里做批量统计,也可能躺在 mlock fbatch 里,先把这两个批处理缓存排干再数引用;排干后 refcount 仍高,才是真有外部引用。可能被 DMA-pinned 或仍被映射的 folio 提前判出,不再白排。扫范围时记住排干状态,避免逐 folio 重复排干。发现引用就停扫,经 error_offset 报告失败位置。

get_pfn 时清零页

in-place 之下,guest_memfd 页里已有的内容会经平台 ioctl populate 进 guest,不先清零就等于把宿主的未初始化内存递给 guest。清零挪进 __kvm_gmem_get_pfn(),用 uptodate 标志保证一页一生最多清一次;populate 失败不重清,因为 SNP firmware 拒绝 CPUID 页时会借页内容把期望值回传给 userspace。

未启用转换的 VM 零开销

kvm_arch_has_gmem_convert() 把转换相关的 arch 钩子调用 guard 起来,x86 CoCo 两家里 SEV 需要运行期钩子,TDX 的准备工作是纯开销;CONFIG_HAVE_KVM_ARCH_GMEM_CONVERT=n 时靠死代码消除把调用整体拿掉。

原地转换的处理流
┌───────────────────────────────────┐
│    已是目标属性的范围直接跳过     │
└─────────────────┬─────────────────┘
                  ▼
┌───────────────────────────────────┐
│ 排干 lru 与 mlock fbatch 再查引用 │
└─────────────────┬─────────────────┘
                  ▼
┌───────────────────────────────────┐
│    按映射类型只 zap 该 zap 的     │
└─────────────────┬─────────────────┘
                  ▼
┌───────────────────────────────────┐
│  失败写回 error_offset 增量重试   │
└───────────────────────────────────┘
单后备与两道配套
┌───────────────────────────────────┐
│ memslot 缺页只由 guest_memfd 满足 │
└─────────────────┬─────────────────┘
                  ▼
┌───────────────────────────────────┐
│ get_pfn 时清零页,uptodate 记一次 │
└─────────────────┬─────────────────┘
                  ▼
┌───────────────────────────────────┐
│   未启用的 VM 死代码消除零开销    │
└───────────────────────────────────┘

收益

作者未提供性能数据,从代码逻辑推断的预期收益:

  • 转换不再 tear down 与重建,stage 2 页表少拆少建,CoCo 每页重新 accept 的次数随之减少
  • 已是目标状态的范围与无关映射不再挨 zap,避免误毁数据与无谓的重新 accept
  • 失败经 error_offset 精确上报,增量重试替代全有或全无的回滚
  • 未启用 in-place 转换的 VM 不付转换路径的开销