KVM 架构

本页目录

guest 退出到 KVM,不一定让 KVM_RUN 返回用户空间。沿执行循环要分清硬件退出、内核处理和 VMM 处理⁠。KVM 提供执行接口,QEMU 是常见使用者而非唯一规定。

硬件例子以 x86 VMX/SVM 为主,其他架构使用不同执行级和机制。

查阅目标阅读位置
职责与退出层次前两节
接口探测与宿主调度文件描述符、vCPU
执行与通知边界运行循环、中断注入

内核负责执行边界,VMM 组织整台机器

KVM 使用 Linux 的调度、内存管理等基础设施,并通过架构实现控制硬件虚拟化。用户空间 VMM 通常负责机器配置、固件、设备模型和迁移协调;部分设备数据路径可以进入内核或独立后端。

x86 上的 kvm 模块也包含相应架构代码,kvm_intel/kvm_amd 再对接厂商机制,不能把整个 kvm.ko 简写成纯架构无关核心。目录 virt/kvm 与 arch/x86/kvm 可以帮助区分源码责任。

两种“退出”需要分开

硬件 VM exit 不一定返回用户空间 用户空间 VMM 调用 KVM_RUN KVM 内核 准备并进入 guest guest 执行 直到需退出或被打断 KVM 处理硬件退出 可处理:再次进入 guest 需要用户空间处理时才返回 KVM_RUN 也可能因信号等返回错误;必须先检查 ioctl 返回值。

VMX 的 root/non-root 与 guest 内部 CPL0/CPL3 是不同维度:guest 内核仍可在自己的特权级执行。普通指令通常直接由 CPU 执行,硬件控制指定哪些事件或操作需要退出。

硬件 VM exit 首先回到 KVM。若 KVM 能处理,例如某些页映射或中断状态更新,可以直接再次进入 guest;只有需要用户空间处理等情况,KVM_RUN 才返回。一个 KVM_RUN 期间可能发生多次硬件进入与退出。

因此统计 ioctl 次数不能代替统计硬件 VM exit。exit 也不全由“敏感指令”触发,外部事件和嵌套分页问题同样可能触发。

独立选择一次 guest 运算、CPUID、MMIO 或宿主信号,分别统计硬件退出与 KVM_RUN 返回。两条边界可以只跨一条,不能用同一个“exit 次数”混合计数。

正在呈现知识画面
一次硬件退出,会不会让 KVM_RUN 返回?

硬件退出先进入 KVM;内核能处理时可直接重入 guest。KVM_RUN 成功返回才分派 exit_reason,错误返回先处理 errno。

文件描述符分层

句柄获得方式主要用途
KVM system fdopen /dev/kvm查询 API、能力,创建 VM
VM fdKVM_CREATE_VM注册内存、创建 vCPU 与设备
vCPU fdKVM_CREATE_VCPU配置 CPU 状态,KVM_RUN
设备 fd相应设备创建接口配置具体的内核虚拟设备

不同 ioctl 必须用于正确类别的 fd。接口能力通过 KVM_CHECK_EXTENSION 查询,不能只看内核版本号;部分扩展还需要显式启用。VM 生命周期与其引用关联,关闭一个重复 fd 不必然立刻销毁整台 VM。KVM API

下面只探测接口,不创建 VM:

#include <fcntl.h>
#include <linux/kvm.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>

int main(void) {
    int fd = open("/dev/kvm", O_RDWR | O_CLOEXEC);
    if (fd == -1) { perror("open /dev/kvm"); return 1; }
    int version = ioctl(fd, KVM_GET_API_VERSION, 0);
    if (version < 0) { perror("KVM_GET_API_VERSION"); close(fd); return 1; }
    if (version != KVM_API_VERSION) {
        fprintf(stderr, "unsupported KVM API: %d\n", version);
        close(fd); return 1;
    }
    int memory = ioctl(fd, KVM_CHECK_EXTENSION, KVM_CAP_USER_MEMORY);
    if (memory < 0) { perror("KVM_CHECK_EXTENSION"); close(fd); return 1; }
    int bytes = ioctl(fd, KVM_GET_VCPU_MMAP_SIZE, 0);
    if (bytes < 0) { perror("KVM_GET_VCPU_MMAP_SIZE"); close(fd); return 1; }
    printf("API=%d user_memory=%d vcpu_mmap=%d\n", version, memory, bytes);
    close(fd);
    return 0;
}

保存为 kvm_probe.c,用 cc -D_GNU_SOURCE -Wall -Wextra kvm_probe.c -o kvm_probe 编译。/dev/kvm 不存在或无权限与接口能力不支持是不同问题,诊断时应保留具体错误。

vCPU 是被宿主调度的执行上下文

QEMU 的常见 KVM 模式为每个 vCPU 配一个宿主线程。vCPU 数量不是物理核数量,线程可能被抢占、迁移到不同 CPU,或等待 I/O/中断;CPU pinning 和 NUMA 放置会影响结果。

KVM 使用调度通知等机制在宿主切换时维护架构执行状态。steal time、虚拟时钟、TSC offset/scaling 各有具体接口,不能把每次线程切换描述成“重新计算全部时间偏移”。

HLT 表示 guest 暂时没有工作等待唤醒,是否阻塞在内核或返回用户空间取决于配置;它不等于用来限制宿主功耗的 idle injection。

运行循环要处理错误与完成协议

KVM_RUN 返回值首先处理什么
-1按 errno 处理信号中断等错误
0再按本次 exit_reason 分派

错误返回时,不能盲读可能还属于上次退出的共享 exit_reason。

返回 0 后,用户空间按 KVM_EXIT_IO、KVM_EXIT_MMIO 等退出原因处理。部分 I/O 操作需要再次进入 KVM_RUN 才在内核侧完成;准备保存状态或迁移时,必须考虑这些尚未完成的操作。

一个输入指令,需要跨过哪些完成边界

假设 guest 执行“从端口读取一个字节,再把结果加 1”。设备模型在用户空间,第一次 KVM_RUN 因 KVM_EXIT_IO 返回后,VMM 找到该端口的虚拟设备,把值 7 填入共享运行区。但“缓冲区已填入 7”与“guest 输入指令已经提交结果”仍是两个状态。

阶段VMM 已完成什么还不能据此断言什么
收到 I/O 退出知道方向、端口、宽度、数量guest 已经取得设备返回值
填好输入缓冲区设备模型给出了值 7寄存器和指令进度已形成可迁移的完整状态
再次调用 KVM_RUN内核完成挂起的操作后续 guest 指令一定已经执行
允许继续执行guest 可使用结果完成加法仅凭这个 vCPU 的状态就能迁移整台机器
填好设备结果之后,还要让 KVM 完成挂起指令 I/O 退出 guest 等待输入 VMM 填入 7 共享缓冲区就绪 重入并提前返回 输入完成,尚未加 1 再次允许执行 guest 计算得到 8 可见寄存器、设备缓冲区和内核挂起状态必须一起考虑 提前返回示意使用 immediate_exit;必须查询相应能力并处理 ioctl 返回值。

KVM API 明确要求这类操作再次进入内核完成。需要停在完成之后、下一条 guest 指令之前时,可按能力支持使用 immediate_exit 或待处理的未屏蔽信号;完成挂起操作发生在后续信号检查之前。immediate_exit 应通过 KVM_CAP_IMMEDIATE_EXIT 查询支持。迁移还需要协调其他 vCPU、设备与内存,这里只处理一项容易遗漏的局部边界。KVM 运行区与 I/O 完成约定

下面是三个抽象指令 IN → ADD → HALT 的状态机。pc 是指令序号,不是 x86 字节地址;它用于展示完成顺序,不模拟真正的指令解码、硬件退出或 KVM ioctl。

import errno

class GuestModel:
    def __init__(self):
        self.pc = 0
        self.acc = 0
        self.waiting_input = False
        self.input_byte = None
        self.exit_reason = None

    def run(self, immediate_exit=False):
        # 前一次退出的输入操作先完成,随后才处理本轮提前返回。
        if self.waiting_input:
            if self.input_byte is None or not 0 <= self.input_byte <= 255:
                raise ValueError("必须先提供一个输入字节")
            self.acc = self.input_byte
            self.pc += 1
            self.waiting_input = False
            self.input_byte = None
        if immediate_exit:
            return -1, errno.EINTR  # exit_reason 不代表本次的新退出
        while True:
            if self.pc == 0:
                self.waiting_input = True
                self.exit_reason = "IO_IN"
                return 0, 0
            if self.pc == 1:
                self.acc = (self.acc + 1) & 255
                self.pc += 1
            else:
                self.exit_reason = "HALT"
                return 0, 0

for value in (0, 7, 255):
    guest = GuestModel()
    assert guest.run() == (0, 0) and guest.exit_reason == "IO_IN"
    guest.input_byte = value
    assert (guest.pc, guest.acc, guest.waiting_input) == (0, 0, True)
    assert guest.run(immediate_exit=True) == (-1, errno.EINTR)
    assert (guest.pc, guest.acc, guest.waiting_input) == (1, value, False)
    assert guest.exit_reason == "IO_IN"  # 旧字段仍在,不能再次处理成一条新 I/O
    assert guest.run() == (0, 0) and guest.exit_reason == "HALT"
    assert (guest.pc, guest.acc) == (2, (value + 1) & 255)

值 7 的轨迹在完成输入后停于 pc=1, acc=7,继续运行才得到 8。若在缓冲区写完、尚未再次进入内核时就保存寄存器,挂起操作的隐藏状态没有被普通可见状态完整表达,恢复端可能无法继续同一个操作。

模型中的第二次调用还说明一个细节:EINTR 不表示此前的一切工作都没有发生。这一轮可能已经完成了前次挂起操作;但它也没有提供一个新的成功退出,不能再根据遗留的 IO_IN 字段调用设备模型一次。普通运行循环应先检查 ioctl 返回值,再决定读取哪些退出字段。

I/O 数据按偏移和数量解释

KVM_EXIT_IO 的 data_offset 相对于整个 struct kvm_run 映射起点,数据是 count 个、每个 size 字节的紧凑数组。例如 size=2、count=3 意味着 6 个字节,不是只处理第一个 16 位值。OUT 时 VMM 读取这段数据,IN 时 VMM 把设备返回值填回同一段区域。

偏移不是 guest 虚拟地址,也不是 guest 物理地址;直接把它拿去查询 guest 页表会走错地址空间。MMIO 退出使用另一组字段,包括物理地址、长度和 data,不能与端口 I/O 的布局混用。处理共享区域期间也应保证同一 vCPU 的运行与设备处理按预定线程协议串行化,避免一边重入 KVM_RUN、一边改写上一轮的数据。

共享运行区不是完整寄存器镜像

KVM_GET_VCPU_MMAP_SIZE 给出 vCPU 共享映射大小,不能自己假定一页。struct kvm_run 用于执行控制和退出数据,不是通用的完整寄存器镜像;寄存器通常通过相应 ioctl,或在明确支持并启用的同步机制下处理。

中断注入取决于 guest 是否可接收

设备完成、虚拟定时器和 vCPU 间 IPI 都可产生虚拟中断。设置待处理状态不保证下一条 guest 指令前立即交付,还需符合 guest 中断屏蔽、优先级与硬件注入条件。kick 用于促使 vCPU 处理新工作,不等于无条件立即运行其处理程序。

内核 irqchip、irqfd 与硬件中断虚拟化可减少部分用户空间往返,但路径与架构能力有关。地址转换见 内存与设备虚拟化,线程与迁移协作见 QEMU 与 KVM。