1.2 QEMU与KVM架构介绍

1.2.1 QEMU与KVM历史

QEMU和KVM经常被人们放在一起讨论,其实两者的关系完全可以解耦合。QEMU最开始是由法国程序员Fabrice Bellard开发的一个模拟器。QEMU能够完成用户程序模拟和系统虚拟化模拟。用户程序模拟指的是QEMU能够将为一个平台编译的二进制文件运行在另一个不同的平台,如一个ARM指令集的二进制程序,通过QEMU的TCG(Tiny Code Generator)引擎的处理之后,ARM指令被转换成TCG中间代码,然后再转换成目的平台的代码。系统虚拟化模拟指的是QEMU能够模拟一个完整的系统虚拟机,该虚拟机有自己的虚拟CPU、芯片组、虚拟内存以及各种虚拟外部设备,能够为虚拟机中运行的操作系统和应用软件呈现出与物理计算机完全一致的硬件视图。QEMU能够模拟的平台很多,包括x86、ARM、MIPS、PPC等,早期的QEMU都是通过TCG来完成各种硬件平台的模拟,所有的虚拟机指令需要经过QEMU的转换。

系统虚拟机天生适用于云计算。云计算提供了一种按需服务的模式,让用户能够很方便地根据自己的需求使用各种计算、网络、存储资源。以计算资源中的虚拟机为例,用户可以指定不同CPU模型和内存规格的虚拟机。云计算平台可以通过系统虚拟化技术很方便地满足用户的需求。如果用户删除资源,云计算平台可以直接删除其对应的虚拟机。早期的QEMU都是软件模拟的,很明显其在性能上是不能满足要求的。所以早期的云计算平台通常使用Xen作为其底层虚拟化平台。前面提到过,Xen早期是在x86架构上直接完成的虚拟化,这需要修改虚拟机内部的操作系统,也使得Xen的整个VMM非常复杂,缺陷比较多。

Intel和AMD在2005年左右开始在CPU层面提供对系统虚拟化的支持,叫作硬件虚拟化,Intel在x86指令集的基础上增加了一套VMX扩展指令VT-x,为CPU增加了新的运行模式,完成了x86虚拟化漏洞的修补。通过新的硬件虚拟化指令,可以非常方便地构造VMM,并且x86虚拟机中的代码能够原生地运行在物理CPU上。

以色列初创公司Qumranet基于新的虚拟化指令集实现了KVM,并推广到Linux内核社区。KVM本身是一个内核模块,导出了一系列的接口到用户空间,用户空间可以使用这些接口创建虚拟机。最开始KVM只负责最核心的CPU虚拟化和内存虚拟化部分,使用QEMU作为其用户态组件,负责完成大量外设的模拟,当时的方案被称为QEMU-KVM。KVM的具体设计与实现可以参考Avi Kivity等人在2007年发表的论文“KVM:The Linux Virtual Machine Monitor”。由于KVM的设计架构精简,能够跟现有的Linux内核无缝吻合,因此在社区获得了极大的关注与支持。特别是随着Red Hat投入大量的人力去完善QEMU和KVM,QEMU社区得到了飞速发展。直到现在,QEMU社区依然非常活跃,但是其主要用途已经不是作为一个模拟器了,而是作为以QEMU-KVM为基础的为云计算服务的系统虚拟化软件。当然,不仅仅是KVM将QEMU作为应用层组件,Xen后来支持的硬件虚拟机也使用QEMU作为其用户态组件来完成虚拟机的设备模拟。

1.2.2 QEMU与KVM架构

QEMU与KVM的完整架构如图1-5所示。该图来自QEMU官网,比较完整地展现了QEMU与KVM虚拟化的各个方面,包括QEMU的运行机制,KVM的组成,QEMU与KVM的关系,虚拟机CPU、内存、外设等的虚拟化,下面对其进行简要介绍。

QEMU与KVM架构整体上分为3个部分,对应图中的3个部分。左边上半部分是所谓的VMX root模式的应用层,下面是VMX root模式的内核层。所谓VMX root,其实是相对于VMX non-root模式而言的。VMX root和VMX non-root都是CPU引入了支持硬件虚拟化的指令集VT-x之后出现的概念。VT-x的概念会在第4章CPU虚拟化中进行详细介绍,现在可以将VMX root理解成宿主机模式,将VMX non-root理解成虚拟机模式。右边上半部分表示的是虚拟机的运行,虚拟机运行在VMX non-root模式下。VMX root模式与未引入VT-x之前是一样的,CPU在运行包括QEMU在内的普通进程和宿主机的操作系统内核时,CPU处在该模式。CPU在运行虚拟机中的用户程序和操作系统代码的时候处于VMX non-root模式。需要注意的是,CPU的运行模式与CPU运行时的特权等级是相互正交的,虚拟机在VMX root模式和VMX non-root模式下都有ring 0到ring 3四个特权级别。

图1-5左边上半部分列出了QEMU的主要任务,QEMU在初始化的时候会创建模拟的芯片组,创建CPU线程来表示虚拟机的CPU执行流,在QEMU的虚拟地址空间中分配空间作为虚拟机的物理地址,QEMU还需要根据用户在命令行指定的设备为虚拟机创建对应的虚拟设备。在虚拟机运行期间,QEMU会在主线程中监听多种事件,这些事件包括虚拟机对设备的I/O访问、用户对虚拟机管理界面、虚拟设备对应的宿主机上的一些I/O事件(比如虚拟机网络数据的接收)等。QEMU应用层接收到这些事件之后会调用预先定义好的函数进行处理。

图1-5 QEMU与KVM整体架构图

图1-5右边上半部分表示的是虚拟机的运行。对虚拟机本身来讲,它也有自己的应用层和内核层,只不过是VMX non-root下的。QEMU和KVM对虚拟机中的操作系统来说是完全透明的,常用的操作系统可以不经修改就直接运行在虚拟机中。虚拟机的一个CPU对应为QEMU进程中的一个线程,通过QEMU和KVM的相互协作,这些线程会被宿主机操作系统正常调度,直接执行虚拟机中的代码。虚拟机中的物理内存对应为QEMU进程中的虚拟内存,虚拟机中的操作系统有自己的页表管理,完成虚拟机虚拟地址到虚拟机物理地址的转换,再经过KVM的页表完成虚拟机物理地址到宿主机物理地址的转换。虚拟机中的设备是通过QEMU呈现给它的,操作系统在启动的时候进行设备枚举,加载对应的驱动。在运行过程中,虚拟机操作系统通过设备的I/O端口(Port IO、PIO)或者MMIO(Memory Mapped I/O)进行交互,KVM会截获这个请求,大多数时候KVM会将请求分发到用户空间的QEMU进程中,由QEMU处理这些I/O请求。

图1-5下半部分表示的是位于Linux内核中的KVM驱动。KVM驱动以杂项(misc)设备驱动的方式存在于内核中。一方面,KVM通过“/dev/kvm”设备导出了一系列的接口,QEMU等用户态程序可以通过这些接口来控制虚拟机的各个方面,比如CPU个数、内存布局、运行等。另一方面,KVM需要截获虚拟机产生的虚拟机退出(VM Exit)事件并进行处理。

QEMU和KVM联合起来共同完成虚拟机各个组件的虚拟化,这里对几个重要组件的虚拟化进行简单介绍。

首先介绍CPU虚拟化。QEMU创建虚拟机CPU线程,在初始化的时候会设置好相应的虚拟CPU寄存器的值,然后调用KVM的接口,将虚拟机运行起来,在物理CPU上执行虚拟机的代码。当虚拟机运行起来之后,KVM需要截获虚拟机中的敏感指令,当虚拟机中的代码是敏感指令或者说满足了一定的退出条件时,CPU会从VMX non-root模式退出到KVM,这叫作VM Exit,这就像在用户态执行指令陷入内核一样。虚拟机的退出首先陷入到KVM中进行处理,如果KVM无法处理,比如说虚拟机写了设备的寄存器地址,那么KVM会将这个写操作分派到QEMU中进行处理,当KVM或者QEMU处理好了退出事件之后,又可以将CPU置于VMX non-root模式运行虚拟机代码,这叫作VM Entry。虚拟机就这样不停地进行VM Exit和VM Entry,CPU会加载对应的宿主机状态或者虚拟机状态,如图1-6所示。KVM使用一个结构来保存虚拟机VM Exit和VM Entry的状态,叫作VMCS。

图1-6 CPU虚拟化原理

其次介绍内存虚拟化。如同物理机运行需要内存一样,虚拟机的运行同样离不开内存,QEMU在初始化的时候需要调用KVM的接口向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,扩展页表)之前,虚拟机通过影子页表实现从虚拟机虚拟地址到宿主机物理地址的转换,是一种软件实现。当CPU支持EPT之后,CPU会自动完成虚拟机物理地址到宿主机物理地址的转换。虚拟机在第一次访问内存的时候就会陷入到KVM,KVM会逐渐建立起所谓的EPT页面。这样虚拟机的虚拟CPU在后面访问虚拟机虚拟内存地址的时候,首先会被转换为虚拟机物理地址,接着会查找EPT页表,然后得到宿主机物理地址,其内存寻址过程如图1-7所示,整个过程全部由硬件完成,效率很高。由于现在EPT都是标配,本书将只关注EPT存在的情况。

图1-7 EPT原理

再次介绍外设虚拟化。一个计算机系统离不开大量的外部设备,网卡、磁盘等通常都是计算机系统必不可少的组成部分。虚拟化的一个烦琐任务就是为虚拟机提供大量的设备支持,如同Linux内核中最多的代码是设备驱动,QEMU最多的代码是设备模拟。设备模拟的本质是要为虚拟机提供一个与物理设备接口完全一致的虚拟接口。虚拟机中的操作系统与设备进行的数据交互或者由QEMU和(或)KVM完成,或者由宿主机上对应的后端设备完成。QEMU在初始化过程中会创建好模拟芯片组和必要的模拟设备,包括南北桥芯片、PCI根总线、ISA根总线等总线系统,以及各种PCI设备、ISA设备等。QEMU的命令行可以指定可选的设备以及设备配置项。大部分情况下,用户对虚拟机的需求都体现在对虚拟设备的需求上,比如常见的网络、存储资源对应QEMU的网卡模拟和硬盘模拟。这些需求也导致了QEMU虚拟设备的快速发展。设备模拟经历了非常大的发展,最开始的QEMU只有纯软件模拟,虚拟机内核不用做任何修改,每一次对设备的寄存器读写都会陷入到KVM,进而到QEMU,QEMU再对这些请求进行处理并模拟硬件行为,纯软件模拟设备如图1-8a所示。显然,软件模拟会导致非常多的QEMU/KVM介入,效率不高。为了提高虚拟设备的性能,社区提出了virtio设备方案。virtio设备是一类特殊的设备,并没有对应的物理设备,所以需要虚拟机内部操作系统安装特殊的virtio驱动,virtio设备模拟如图1-8b所示。virtio设备将QEMU变成了半虚拟化方案,因为其本质上修改了虚拟机操作系统内核,与之相对的完全不用修改虚拟机操作系统的方案叫作完全虚拟化。virtio仍然不能完全满足一些高性能的场景,于是又有了设备直通方案,也就是将物理硬件设备直接挂到虚拟机上,虚拟机直接与物理设备交互,尽可能在I/O路径上减少QEMU/KVM的参与,直通设备原理如图1-8c所示。与设备直通经常一起使用的有设备的硬件虚拟化支持技术SRIOV(Single Root I/O Virtualization,单根输入/输出虚拟化),SRIOV能够将单个的物理硬件高效地虚拟出多个虚拟硬件。通过将SRIOV虚拟出来的硬件直通到虚拟机中,虚拟机能够非常高效地使用这些设备。

图1-8 QEMU的各种设备模拟方式

a) 纯软件的设备模拟 b) virtio设备模拟 c) 直通设备

最后介绍中断虚拟化。中断系统是一个计算系统必不可少的组成部分。操作系统通过写设备的I/O端口或者MMIO地址来与设备交互,设备通过发送中断来通知虚操作系统事件,图1-9显示了模拟设备向虚拟机注入中断的状态。QEMU在初始化主板芯片的时候初始化中断控制器。QEMU支持单CPU的Intel 8259中断控制器以及SMP的I/O APIC(I/O Advanced Programmable Interrupt Controller)和LAPIC(Local Advanced Programmable Interrupt Controller)中断控制器。传统上,如果虚拟外设通过QEMU向虚拟机注入中断,需要先陷入到KVM,然后由KVM向虚拟机注入中断,这是一个非常费时的操作,为了提高虚拟机的效率,KVM自己也实现了中断控制器Intel 8259、I/O APIC以及LAPIC。用户可以有选择地让QEMU或者KVM模拟全部中断控制器,也可以让QEMU模拟Intel 8259中断控制器和I/O APIC,让KVM模拟LAPIC。QEMU/KVM一方面需要完成这项中断设备的模拟,另一方面需要模拟中断的请求。中断请求的形式大体上包括传统ISA设备连接Intel 8259中断控制器产生的中断请求,PCI设备的INTx中断请求以及MSI和MSIX中断请求。

图1-9 虚拟设备的中断注入