KVM-QEMU 的完整工作流程案例解析
阶段一:虚拟机启动准备
步骤 1:用户发起启动命令
# 用户执行类似命令
qemu-system-x86_64 -enable-kvm -m 2048 -hda vm_disk.img -vnc :0
步骤 2:QEMU 进程初始化
- QEMU 解析命令行参数
- 初始化模拟的设备树(虚拟主板、芯片组等)
- 加载虚拟 BIOS 或 UEFI 固件
- 准备虚拟设备(磁盘镜像、网络后端等)
步骤 3:KVM 初始化
// QEMU 内部执行类似操作
kvm_fd = open("/dev/kvm", O_RDWR); // 打开 KVM 设备
vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0); // 创建虚拟机上下文
步骤 4:内存分配与映射
- QEMU 通过
mmap()
分配一大块内存作为客户机物理内存 - 通过
ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &mem)
告知 KVM 内存布局 - 建立客户机物理地址(GPA)到主机虚拟地址(HVA)的映射
步骤 5:创建虚拟 CPU
// 对每个 vCPU
vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, vcpu_id);
vcpu_mmap_size = ioctl(kvm_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
vcpu_state = mmap(NULL, vcpu_mmap_size, PROT_READ|PROT_WRITE, MAP_SHARED, vcpu_fd, 0);
阶段二:虚拟机运行循环
步骤 6:启动 vCPU 线程
- 对每个虚拟 CPU,QEMU 创建一个独立的 POSIX 线程
- 线程进入主循环,不断执行:
while (1) {// 进入 KVM 运行状态ret = ioctl(vcpu_fd, KVM_RUN, 0);// 处理退出原因switch (vcpu_state->exit_reason) {case KVM_EXIT_IO: handle_io(vcpu_state);break;case KVM_EXIT_MMIO:handle_mmio(vcpu_state);break;case KVM_EXIT_HLT:handle_hlt(vcpu_state);break;// ... 其他退出原因}
}
阶段三:详细退出处理流程
场景 A:端口 I/O 退出(如磁盘写入)
步骤 7a:触发退出
虚拟机内程序 → 执行 OUT 指令(端口 0x1F0) → CPU 触发 VM-Exit
步骤 8a:KVM 捕获并分类
- KVM 检查退出原因:
KVM_EXIT_IO
- 读取退出信息:
- 方向:输出(OUT)
- 端口:0x1F0(IDE 数据端口)
- 数据大小:4 字节
- 数据内容:要写入的扇区数据
步骤 9a:QEMU 设备模拟
void handle_io(struct kvm_run *run) {if (run->io.port == 0x1F0 && run->io.direction == KVM_EXIT_IO_OUT) {// 这是 IDE 磁盘数据端口ide_write_data(run->io.data, run->io.size);}
}void ide_write_data(void *data, int size) {// 将数据写入主机上的磁盘镜像文件fwrite(data, size, 1, vm_disk_file);
}
步骤 10a:恢复执行
- QEMU 完成模拟后,循环回到
KVM_RUN
- KVM 通过 VM-Entry 重新进入客户机
- 虚拟机从 OUT 指令的下一条指令继续执行
场景 B:内存映射 I/O 退出(如 Virtio 设备)
步骤 7b:触发 MMIO 退出
虚拟机 → 访问 0xFE000000(GPA) → EPT 违规 → VM-Exit
步骤 8b:KVM 处理 MMIO
- 退出原因:
KVM_EXIT_MMIO
- 信息包含:
- 物理地址:0xFE000000
- 访问类型:读/写
- 数据长度:8 字节
- 数据值(如果是写操作)
步骤 9b:QEMU Virtio 模拟
void handle_mmio(struct kvm_run *run) {if (run->mmio.phys_addr >= VIRTIO_MMIO_BASE) {// 这是 Virtio 设备区域virtio_mmio_write(run->mmio.phys_addr, run->mmio.data, run->mmio.len);}
}void virtio_mmio_write(uint64_t addr, void *data, int len) {// 处理 Virtio 队列操作if (addr == VIRTIO_MMIO_QUEUE_NOTIFY) {// 通知 Virtio 设备有新的缓冲区process_virtio_queue();}
}
阶段四:中断注入
步骤 11:设备产生中断
- QEMU 的虚拟设备完成操作后可能需要中断虚拟机
- 例如:磁盘读取完成、网络包到达
步骤 12:中断注入流程
// QEMU 请求注入中断
struct kvm_interrupt intr = { .irq = 14 }; // IDE 中断号
ioctl(vcpu_fd, KVM_INTERRUPT, &intr);// 或者设置 LAPIC
ioctl(vcpu_fd, KVM_SET_LAPIC, &lapic_state);
步骤 13:KVM 处理中断
- 在下次 VM-Entry 时,KVM 将中断注入到客户机
- 客户机正常处理中断,就像在物理机上一样
阶段五:性能优化路径
Virtio 优化路径
传统模拟 vs Virtio:
传统 IDE 模拟:
虚拟机OUT指令 → VM-Exit → QEMU处理 → 文件写入 → 中断注入Virtio 优化:
虚拟机写入共享环 → 很少VM-Exit → QEMU异步处理 → 事件通知
vhost 进一步优化
vhost-net 工作流程:
- QEMU 将 Virtio 后端交给内核的 vhost-net 驱动
- 网络数据包直接在 KVM 和 vhost-net 之间传递
- 完全绕过 QEMU 进程,减少上下文切换
完整执行流程图
用户空间 (QEMU) 内核空间 (KVM) 硬件│ │ ││ 1. qemu-kvm命令 │ ││ ────────────────────> │ ││ │ ││ 2. open("/dev/kvm") │ ││ ────────────────────> │ ││ │ 3. 初始化VMX/SVM ││ │ ◀─────────────────── ││ │ ││ 4. 内存分配与映射 │ ││ ────────────────────> │ ││ │ ││ 5. KVM_RUN循环开始 │ ││ ────────────────────> │ 6. VM-Entry ││ │ ───────────────────> │ 客户机直接执行│ │ ││ │ 7. I/O触发VM-Exit ││ │ <─────────────────── ││ 8. 处理退出原因 │ ││ <──────────────────── │ ││ │ ││ 9. 设备模拟 │ ││ (文件操作/网络等) │ ││ │ ││ 10. 可能注入中断 │ ││ ────────────────────> │ ││ │ ││ 11. 回到步骤5 │ │└─────────────────────> └────────────────────┘
这个细化的工作流程展示了从虚拟机启动到正常运行的完整路径,包括各种退出场景的处理细节和性能优化机制。