KVM Caretaker:让虚拟机在宿主内核重启期间继续执行
KVM Caretaker:让虚拟机在宿主内核重启期间继续执行
Orphaned VMs 是 Linux/KVM 在 2026 年的一项实验性工作,目标是在宿主机
kexec热升级的整个过程中,让虚拟机的 vCPU 持续执行 guest 指令,而不是被挂起。材料出处
- 架构提案:
[RFC] proposal: KVM: Orphaned VMs: The Caretaker approach for Live Update,Pasha Tatashin,2026-04-28,LKML · 原文- 评审意见:Paolo Bonzini,2026-04-30 · 原文;Alexander Graf,2026-09-21 · 原文
- 源码:
github.com/soleen/linux分支ovm/rfc/v1,HEADf466a76d7,基线edaf38e8c,46 个 patch / 95 个文件 · 仓库演示出处
LPC 2026 KVM Microconf:
In-Kernel Caretaker for Orphaned VMs,Pasha Tatashin,2026-10-06 12:00,Prague Congress Centre(Club E)
一、当前场景:kexec 方案与问题
云上宿主机的内核维护通常采用 kexec:跳过 BIOS 与固件初始化,直接把新内核加载到内存并跳转执行,停机时间从分钟级压缩到数秒。谷歌已有的 Live Update 方案(LUO + KHO)解决了"状态延续"问题:vmfd、vcpufd、guest_memfd、二级页表都可以原地保留,新内核启动后领回。
但它仍然有执行暂停的问题,vCPU 在这段窗口内必须挂起。对于业务来说,这是一次可观测的中断。问题就在于宿主机的内核更新时,没有执行主体可以运行虚拟机VM。
二、方案综述
Orphaned VMs 的核心是把两件事拆开:
| 归属 | 跨 kexec | |
|---|---|---|
| 资源所有权 | LUO / KHO | vmfd、vcpufd、guest 内存、EPT/NPT 页表 |
| 活跃执行 | 被保留的物理 CPU + Caretaker | vCPU 持续执行 |
在这样的状态下,虚拟机成为"孤儿"(orphaned VM):脱离宿主 OS 与用户态 VMM,但仍在物理 CPU 上执行 guest 指令。承担执行职责的组件叫 Caretaker。它是驻留在被保留物理核上的一段内核代码,独立于宿主内核存在。
整体分为四层:
| 组件 | 职责 |
|---|---|
| KHO + LUO | 跨 kexec 保留 RAM、页表、vCPU 状态 |
| cpu_preserve | 预留出物理cpu核心,不受主内核调度,跨 kexec 保持独立 |
| oncore | 在少数预留的物理cpu核心上实现调度 |
| Caretaker | 运行 guest 指令,处理中断 |
三、实现
3.1 状态与空间保留
通过 KHO (kexec handover)保留需要跨内核保留的状态。
vCPU 架构状态:arch_state 指向一块 KHO 保留区,detach 时硬件状态写入其中,新内核从这里读回。
Caretaker 页:每个 vCPU 一块 4 KB 运行时页,包含通用寄存器、GDT、TSS、私有栈:
struct caretaker_x86_page {
struct kvm_caretaker_arch_ser abi; /* offset 0,跨 kexec ABI */
struct kvm_caretaker_vcpu vcpu;
struct kvm_vcpu_arch_ser *arch_state;
u64 rax, rbx, rcx, rdx, rsi, rdi, rbp;
u64 r8, r9, r10, r11, r12, r13, r14, r15;
...
u8 stack[2048];
};
地址映射:所有被保留对象的指针都必须在新旧内核、以及隔离页表里保持同一虚拟地址。为此 oncore_session_map_buffer() 以原 VA 重新映射:
* Convenience helper that resolves `virt_to_phys(@va)` and maps the buffer at
* its existing kernel virtual address `@va` with %PAGE_KERNEL permissions in
* the session's isolated page tables.
*/
int oncore_session_map_buffer(struct oncore_session *sess, void *va,
size_t size)
{
if (!va || !size)
return 0;
return oncore_session_map_range(sess, virt_to_phys(va),
(unsigned long)va, size, PAGE_KERNEL);
}
物理 CPU:cpu_preserve() 分配 KHO 保留栈,设置保留掩码,然后走标准热插拔下线:
if (cpu_online(cpu)) {
ret = remove_cpu(cpu);
...
}
set_cpu_present(cpu, false);
下线流程的最后一步走特殊分支:CPU 报告 dead 时不停机,而是转入自循环:
void cpuhp_ap_report_dead(void)
{
cpuhp_ap_update_sync_state(SYNC_STATE_DEAD);
cpu_preserved_report_dead();
}
set_cpu_present(cpu, false) 实现了物理cpu隔离:该核从此不在 cpu_present_mask 中,不受主内核调度,主内核更新与启动时会跳过他。
3.2 跳转到 Caretaker 执行
正常 KVM 在 VMCS 中把 VM Exit 的落点指向内核:
vmcs_writel(HOST_RIP, (unsigned long)vmx_vmexit);
Caretaker 把它替换为自身入口,同时改写栈、页表与描述符表:
vmx_vmwrite(HOST_CR3, host_cr3);
vmx_vmwrite(HOST_RSP, (unsigned long)&cvp->common.stack[CXP_STACK_SIZE]);
vmx_vmwrite(HOST_RIP, (unsigned long)&vmx_caretaker_exit_handler);
从而实现 VM 退出后不是进入原内核而是进入独立的裸机代码。
被保留的核在 park 循环中等候 workload:
for (;;) {
cpu_preserved_inval(ser);
switch (READ_ONCE(ser->workload)) {
case CPU_PRESERVED_WORKLOAD:
cpu_preserved_run_workload(ser, pcpu);
break;
default:
arch_cpu_preserved_park_wait();
break;
}
}
在这套系统中,虚拟机的vCPU被包装成oncore_job:
job = oncore_session_submit_job(session, kvm_arch_vcpu_caretaker_run, NULL);
...
vcpu->caretaker.job = job;
ser->flags |= KVM_VCPU_LUO_FLAG_CARETAKER;
从此以 job 为调度单位分配到预留的核心上运行,将 job 入队,发中断唤醒目标核心:
if (job->data)
oncore_session_map_buffer(sess, job->data, PAGE_SIZE);
oncore_sched_enqueue(&sess->rq, job);
if (cpu_is_preserved(job->assigned_cpu))
arch_cpu_preserved_kick(job->assigned_cpu);
3.3 Caretaker 执行循环
用简单的汇编代码让虚拟机进入 Caretaker 的处理
SYM_TYPED_FUNC_START(vmx_caretaker_enter)
ENDBR
CARETAKER_PUSH_HOST_REGS
movq %rsp, CXP_STACK_ORIG(%rdi)
leaq PAGE_SIZE(%rdi), %rsp /* 换到页内私有栈 */
vmptrld CXP_VMCS_PA(%rdi) /* 激活 VMCS */
CARETAKER_RESTORE_GPRS %rdi
CARETAKER_CLEAR_CPU_BUFFERS /* VERW,清微架构缓冲 */
vmresume
vmlaunch
主流程仍然是一个 while 循环,受到时间片的调度
while (!kvm_caretaker_should_exit(cvcpu)) {
if (arch_oncore_read_counter() >= deadline_ticks)
break;
if (kvm_caretaker_enter_guest(cvcpu)) {
reason = ONCORE_EXIT_ERROR;
break;
}
if (!kvm_caretaker_handle_exit(cvcpu, &reason))
break;
}
时间片的时间计数基于cpu的物理计数器
3.4 状态机处理
每个 vCPU 的控制块 16 字节,状态四个值:
struct kvm_caretaker_cb_ser {
u32 state; /* PAUSED / RUNNING / STOPPING / STOPPED */
u32 pcpu_id;
u32 vcpu_id;
u32 reserved;
DECLARE_KHOSER_PTR(telemetry, struct kvm_caretaker_telemetry_ser *);
} __packed;
PAUSED:状态已完全序列化到内存,可被宿主收回RUNNING:硬件寄存器是活的,内存中的arch_state是脏的STOPPING:宿主请求收回,等待序列化STOPPED:终止态,arch_state有效
转移全部通过 cmpxchg 完成,不依赖任何锁:
if (cmpxchg(&cxp->abi.cb.state, KVM_CARETAKER_PAUSED,
KVM_CARETAKER_RUNNING) != KVM_CARETAKER_PAUSED ||
kvm_caretaker_should_exit(&cxp->vcpu)) {
smp_store_release(&cxp->abi.cb.state, KVM_CARETAKER_STOPPED);
return ONCORE_EXIT_ATTACH_SIGNALED;
}
3.5 退出到主内核
新内核接管时,若 vCPU 处于 PAUSED,一次 cmpxchg 即可转为 STOPPED:
if (cmpxchg(&cb->state, KVM_CARETAKER_PAUSED,
KVM_CARETAKER_STOPPED) == KVM_CARETAKER_PAUSED) {
cpu_preserved_clean(cb);
return true;
}
若处于 RUNNING,则转 STOPPING 并周期性发送 IPI,等待目标核完成序列化:
cmpxchg(&cb->state, KVM_CARETAKER_RUNNING, KVM_CARETAKER_STOPPING);
cpu_preserved_clean(cb);
smp_mb();
for (i = 0; i < KVM_CARETAKER_ATTACH_TIMEOUT_US / KVM_CARETAKER_ATTACH_STEP_US; i++) {
if (i % KVM_CARETAKER_ATTACH_KICK_STEPS == 0)
arch_cpu_preserved_kick(pcpu);
if (kvm_caretaker_try_stop(cb))
return 0;
udelay(KVM_CARETAKER_ATTACH_STEP_US);
}
被保留核收到 IPI 后产生 VM Exit,检查到 STOPPING 便退出循环:
reason = kvm_caretaker_vcpu_run(&cxp->vcpu, deadline_ticks);
iret_to_self();
if (ops->detach_serialize && cxp->arch_state)
ops->detach_serialize(cxp, cxp->arch_state);
kvm_x86_caretaker_restore_host_state(&host_state, pcpu);
iret_to_self() :通过 iret 回到原上下文,即原本的栈等。之后才把实时状态写入 KHO:
state->regs.rax = cxp->rax;
state->regs.rbx = cxp->rbx;
...
if (cxp->last_exit_rip)
state->regs.rip = cxp->last_exit_rip;
序列化完成后发布 STOPPED。新内核侧读取 arch_state,恢复 struct kvm_vcpu,把目标核重新上线:
void kvm_caretaker_post_attach_vcpu(struct kvm_vcpu *vcpu)
{
smp_store_mb(vcpu->mode, EXITING_GUEST_MODE);
vcpu->cpu = -1;
if (vcpu->caretaker.cb) {
kvm_caretaker_telemetry_report(vcpu, vcpu->caretaker.cb);
kvm_caretaker_stop(vcpu->caretaker.cb);
vcpu->caretaker.cb = NULL;
}
}
3.6 执行边界
Caretaker 能处理的退出类型极少:
| 类型 | 处理 |
|---|---|
| HLT / WFI / PAUSE | 推进 RIP,让出 CPU |
| 时间片到期、INIT / SIPI | 正常退出 |
| 其他全部 | 暂停(ONCORE_EXIT_STALL) |
"暂停"的含义是 RIP 不推进:vCPU 停在触发退出的那条指令上,等新内核接管后由完整 KVM 处理。
代码注释对此有明确说明:
/*
* x86 has no cross-vCPU emulation. The decoders route
* VMCALL, APIC_ACCESS, APIC_WRITE, EOI_INDUCED and
* INTERRUPT_WINDOW here, and every one of them has a
* guest-visible effect the Caretaker cannot produce: a
* hypercall it cannot service, an APIC register write it
* cannot apply, an EOI it cannot retire, an IPI it cannot
* deliver to a vCPU parked on another core.
*/
return false;
该方案依赖硬件虚拟化中断绕开这些路径:
- 设备中断 → Intel Posted Interrupts / AMD AVIC / ARM GICv4
- vCPU 间 IPI → APICv / AVIC / GICv4.1 virtual SGI
- 缺页 → gap 期间不做 EPT/NPT 更新,直接停车
这些中断能够直接送入虚拟机的 guest os,从而不被影响
四、实测实验
2026 年 10 月 6 日,LPC 2026 KVM Microconf 演示。
环境:13 代 Intel 服务器,20 CPU,32 GB 内存;guest 内运行 Llama 文本生成,输出写入预先 fault-in 的缓冲区。
| 阶段 | 吞吐 |
|---|---|
| 升级前 | 48–56 tokens/s |
| 收缩到 4 个保留 CPU | 约 22 tokens/s |
| kexec 重启期间 | 未中断,从 3000 推进到 4000 tokens |
| 新内核接管 | 恢复 20 CPU 全速 |
五、社区意见
Paolo Bonzini(KVM 维护者)对提案的评价:
-EOVERENGINEERED. Just shove it into the kernel.
他认可方向,但要求先做内核内最小实现。
Alexander Graf(AWS)的关注点不同:caretaker 的入口/出口几乎没有复用现有 KVM 代码,而是另写一套,他建议把 KVM 代码拆为"可独立运行"与"依赖完整内核生态"两类并共享。
六、总结
Caretaker 为解决云服务中宿主机更新内核导致虚拟机需要停机的问题提供了新的思路,当没有内核时如何暂时地运行一个虚拟机。当前实现还有诸多限制:强依赖硬件、缺少用户态支持等。目前还处于讨论阶段,离生产落地还有很长的距离。

浙公网安备 33010602011771号