1. QEMU与KVM 架构

qemu 和 kvm 架构整体上分为 3 部分,对应着上图的三个部分 (左上、右上和下),3 部分分别是 VMX root 的应用层,VMX no-root 和 VMX root 的内核层(分别对应着左上、右上和下)
VMX root: 宿主机根模式,CPU 在运行包括 QEMU 在内的普通进程和宿主机的操作系统内核时,CPU 处在该模式。
VMX no-root: 宿主机非根模式,CPU 在运行虚拟机中的用户程序和操作系统代码的时候处于 VMX no-root 模式
注:VMX root 和 VMX no-root 都包含 ring0 到 ring3 四个特权级别
1.1 QEMU 的主要任务(图中左上部分)
- 创建模拟芯片组
- 创建 CPU 线程来表示虚拟机的 CPI 执行流
- 在 QEMU 的虚拟地址空间中分配空间作为虚拟机的物理地址
- 根据用户在命令行指定的设备为虚拟机创建对应的虚拟设备
- 在主线程中监听多种事件,这些事件包括虚拟机对设备的 I/O 访问、用户对虚拟机管理界面、虚拟设备对应的宿主机上的一些 I/O 事件等(比如虚拟机网络数据的接收)等
对应图中的系统总线、PCI 总线、VGA 的概念如下:
- 系统总线:单独的电脑总线,是连接电脑系统的主要组件。这个技术的开发是用来降低成本和促进模块化。
系统总线结合数据总线的功能来搭载信息,地址总线来决定将信息送往何处,控制总线来决定如何动作。- VGA:VGA 采集卡是能直接采集 VGA 信号,把输入的 VGA 视频信号实时采集压缩,并能立即在一台显示器上同时显示另外一台甚至多台设备的 VGA 数据的设备。VGA 高清采集卡工作原理是由 RGB 模拟信号经过 A/D 采样后转换为数字信号,通过 FPGA 写入 SDRAM 作为缓存,再经 FPGA 从 SDRAM 中将采集压缩的数据读出通过 PCI 总线传输到上位机,由上位机对数据进行传输等处理。 对 VGA 信号实时进行采集支持声音的同步录制,显示画面可以任意拉伸或全屏,录制后格式为 avi,保证适应各类播放器播放,录制后期可以进行非线编,删除或添加相关内容,显示画面颜色和清晰度可调,录制帧率和码率可调。
- PCI 总线主要被分成三部分:
- PCI 设备。符合 PCI 总线标准的设备就被称为 PCI 设备,PCI 总线架构中可以包含多个 PCI 设备。图中的 Audio、LAN 都是一个 PCI 设备。PCI 设备同时也分为主设备和目标设备两种,主设备是一次访问操作的发起者,而目标设备则是被访问者。
- PCI 总线。PCI 总线在系统中可以有多条,类似于树状结构进行扩展,每条 PCI 总线都可以连接多个 PCI 设备 / 桥。上图中有两条 PCI 总线。
- PCI 桥。当一条 PCI 总线的承载量不够时,可以用新的 PCI 总线进行扩展,而 PCI 桥则是连接 PCI 总线之间的纽带。
1.2 虚拟机的运行:(图中右上部分)
- 虚拟机的 CPU:虚拟机的一个 CPU 对应宿主机的一个线程,通过 QEMU 和 KVM 的相互合作,这些线程会直接被宿主机的操作系统正常调度,直接执行虚拟机中的代码
- 虚拟机的内存:虚拟机的物理内存对应于 QEMU 中的虚拟内存,虚拟机的虚拟地址转换为宿主机的物理地址需要先将虚拟机的虚拟地址转换为虚拟机的物理地址,然后再将虚拟机的物理地址通过 KVM 的页表完成虚拟机物理地址到宿主机物理地址的转换
- 虚拟机的设备:虚拟机中的设备是通过 QEMU 呈现给它的,操作系统在启动时进行设备枚举,加载对应的驱动
- 虚拟机与宿主机的交互:虚拟机操作系统通过 I/O 端口 (Port IO、PIO) 或者 MMIO (Memory Mapped I/O) 进行交互,KVM 会截获这个请求,大多数时候 KVM 会将请求分发到用户空间的 QEMU 进程中,有 QEMU 处理这些 I/O 请求
1.3 KVM 驱动:(图中下半部分)
KVM 通过”/dev/kvm” 设备导出了一系列的接口,QEMU 等用户态程序可以通过这些接口来控制虚拟机的各个方面,比如 CPU 个数、内存布局、运行等。另一方面,KVM 需要截获虚拟机产生的虚拟机退出 (VM Exit) 事件并进行处理
1.4 CPU 虚拟化:
前面说过了虚拟机中的一个 CPU 对应宿主机的一个线程。QEMU 会先创建一个 CPU 线程,然后初始化通用寄存器和特殊寄存器 (CPU 寄存器) 的值,然后调用 KVM 接口运行虚拟机。在虚拟机运行的过程中,KVM 会捕获虚拟机中的敏感指令,当虚拟机中的代码时敏感指令或者说满足了一定的退出条件时,CPU 会从 VMX non-root 模式退出到 KVM,这叫做 VM Exit。 退出之后先回陷入到 KVM 中进行处理,如果 KVM 处理不了就转去让 QEMU 处理,当 KVM 或者 QEMU 处理好了 VM Exit 事件后,又可以将 CPU 置于 VMX non-root 模式运行虚拟机代码,这叫做 VM Entry。虚拟机会不停的进行 VM Exit 和 VM Entry,CPU 会加载对应的宿主机状态或者虚拟机状态,并且使用一个 VMCS 结构来保存虚拟机 VM Exit 和 VM Entry 的状态,如下图所示

1.5 内存虚拟化:
前面 KVM 驱动那里说了,QEMU 通过 KVM 来设置虚拟机的内存布局。其中 QEMU 会在初始化的时候通过 mmap 系统调用分配虚拟内存空间作为虚拟机的物理内存,QEMU 在不断更新内存布局的过程中会持续调用 KVM 接口通知内核 KVM 模块虚拟机的内存分布。
虚拟机虚拟地址转换为宿主机物理地址的过程如下 (前面也提到过):
- 虚拟机虚拟地址 (Guest Virtual Address,GVA) 转换为虚拟机物理地址 (Guest Physical Address,GPA )
- 虚拟机物理地址转换为宿主机虚拟地址 (Host Virtual Address,HVA)
- 宿主机虚拟地址转换为宿主机物理地址 (Host Physical Address,HPA)
在 CPU 没有支持 EPT (Extended Page Table,扩展页表) 的时候,虚拟机通过影子页表实现从虚拟机虚拟地址到宿主机物理地址的转换,是一种软件实现。
关于影子页表,它的具体细节就是会在初始化写入 cr3 寄存器的时候,把宿主机针对虚拟机生成的一张页表放进 cr3 寄存器中,然后把原本想要写入 cr3 寄存器的值保存起来,当虚拟机读 cr3 寄存器值的时候,就会把之前保存的 cr3 的值返回给虚拟机。这样做的目的是,在虚拟机内核态中虽然有一张页表,但是虚拟机在访问内存的时候,MMU 不会走这张页表,MMU 走的是以填入到 cr3 寄存器上的真实的值为基地址 (这个值是 vmm 写的宿主机的物理地址) 的影子页表,经过影子页表找到真实的物理地址,影子页表时刻与客户端的页表保持同步。
通过 EPT 表寻址的过程如下图所示:

1.6 设备虚拟化:
- 纯软件模拟设备 :虚拟机内核不用做任何修改,每一次对设备的寄存器读写都会陷入到 KVM,进而到 QEMU,QEMU 再对这些请求进行处理并模拟硬件行为,如下图所示:
- virtio 设备方案 :virtio 设备模拟如下图所示,virtio 设备将 QEMU 变成了半虚拟化方案,因为它修改了虚拟机操作系统的内核。这里先十分简要的说一下 virtio 后面会再详细的进行分析
- 设备直通方案 :将物理硬件设备直接挂到虚拟机上,虚拟机直接与物理设备交互,尽可能在 I/O 路径上减少 QEMU/KVM 的参与,直通设备原理如下图所示,与设备直通经常一起使用的硬件虚拟化支持技术 SRIOV,SIROV 能够将单个的物理硬件高效地虚拟出多个也许你硬件。通过 SIROV 虚拟出来的硬件直通到虚拟机中,虚拟机能够非常高效的使用这些设备



1.7 中断虚拟化:
操作系统通过写设备的 I/O 端口或者 MIMO 地址来与设备交互,设备通过发送中断来通知虚拟操作系统事件,下图显示了模拟设备向虚拟机注入中断的状态。QEMU 在初始化主板芯片的时候初始化中断控制器。QEMU 支持单 CPU 的 Intel8259 中断控制器以及 SMP 的 I/O APIC (I/O Advanced Programmable Interrupt Controller) 中断控制器。

2. KVM API 使用实例
下面是一个超级精简版的内核,功能就是向 I/O 端口写入数据"Hello": 文件 boot.s
start:
mov $0x48, %al # 'H'
outb %al, $0xf1
mov $0x65, %al # 'e'
outb %al, $0xf1
mov $0x6C, %al # 'l'
outb %al, $0xf1
mov $0x6C, %al # 'l'
outb %al, $0xf1
mov $0x6F, %al # 'o'
outb %al, $0xf1
mov $0x0A, %al # '\n'
outb %al, $0xf1
hlt精简版的 qemu(light-qemu): 文件 light-qemu.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/kvm.h>
int main()
{
int ret;
int kvmfd = open("/dev/kvm", O_RDWR); // 获取系统中KVM子系统的文件描述符kvmfd
ioctl(kvmfd, KVM_GET_API_VERSION, NULL); // 获取KVM版本号
int vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0); // 创建一个虚拟机
unsigned char *ram = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); // 为虚拟机分配内存,大小4K
int kfd = open("bootimg", O_RDONLY); // 打开第一个例子的程序
read(kfd, ram, 4096); // 把程序的读入到虚拟机中,这样等会虚拟机运行的时候就会先开始执行这个打开的程序了
struct kvm_userspace_memory_region mem = { // 设置虚拟机内存布局
.slot = 0,
.guest_phys_addr = 0,
.memory_size = 0x1000,
.userspace_addr = (unsigned long)ram,
};
ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &mem); // 分配虚拟机内存
int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, 0); // 创建VCPU
int mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, 0); // 获取VCPU对应的kvm_run结构的大小
struct kvm_run *run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0); // 给VCPU分配内存空间
struct kvm_sregs sregs;
ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs); // 获取特殊寄存器
sregs.cs.base = 0;
sregs.cs.selector = 0;
ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs); // 设置特殊寄存器的值
struct kvm_regs regs = {
.rip = 0,
};
ret = ioctl(vcpufd, KVM_SET_REGS, ®s); // 设置通用寄存器的值
while(1){
ret = ioctl(vcpufd, KVM_RUN, NULL); // 开始运行虚拟机
if(ret == -1){
printf("exit unknown\n");
return -1;
}
switch(run->exit_reason){ // 检测VM退出的原因
case KVM_EXIT_HLT:
puts("KVM_EXIT_HLT");
return 0;
case KVM_EXIT_IO:
putchar(*(((char *)run) + run->io.data_offset));
break;
case KVM_EXIT_FAIL_ENTRY:
puts("entry error");
return -1;
default:
puts("other error");
printf("exit_reason: %d\n",run->exit_reason);
return -1;
}
}
return 0;
}Makefile如下:
OBJ = bootimg light-qemu
all: $(OBJ)
light-qemu: light-qemu.c
gcc light-qemu.c -o light-qemu
bootimg: boot.s
as -32 boot.s -o boot.o
objcopy -O binary boot.o bootimg
clean:
rm -f *.o
dist-clean: clean
rm -f $(OBJ)编译、运行:
# make
# ./light-qemu
Hello
KVM_EXIT_HLT参考:
QEMU KVM Note ⅠQEMU/KVM源码解析与应用Linux虚拟化KVM-Qemu分析(四)之CPU虚拟化(2)
















