:第二章 KVM内部原理

在本章中,我们将讨论libvirt、QEMU和KVM的重要数据结构和内部实现。然后,我们将深入了解KVM下vCPU的执行流程。

在这一章,我们将讨论:

  • libvirt、QEMU和KVM的内部运作方式。
  • libvirt、QEMU和KVM的重要数据结构和代码路径。
  • vCPU的执行流程
  • 所有这些组件如何通信以提供虚拟化

熟悉libvirt及其实现

上一章中提到,libvirt作为额外的管理层可以跟各种hypervisors(例如KVM/QEMU,LXC,OpenVZ,UML)进行通信。libvirt API是开源的。与此同时,它是一个守护进程和管理工具,用于管理前面提到的不同hypervisor。libvirt被各种虚拟化程序和平台广泛采用,例如,图形用户界面工具GNOME boxes和virt-manager(http://virt manager.org/)。不要将libvirt与我们在第1章讨论的VMM/hypervisor混淆起来。

libvirt的cli命令行接口叫做virsh。libvirt也被其他高级管理工具采用,如oVirt(www.ovirt.org)。

<Mastering KVM Virtualization>:第二章 KVM内部原理

很多人以为libvirt只能在单个节点或者运行libvirt的本地节点上运行,这并不正确。libvirt库支持远程功能。因此,任何libvirt工具(例如virt-manager)都可以通过传递一个额外的--connect参数,远程连接到网络上的libvirt守护进程。Fedora,CentOS等绝大多数发行版都安装了libvirt的客户端virsh(由libvirt -client package提供)。

正如前面所讨论的,libvirt的目标是为管理在hypervisor上运行的VM提供一个通用的和稳定的抽象层。简而言之,作为管理层,它负责提供执行管理任务的API,如虚拟机置备、创建、修改、监视、控制、迁移等。在Linux中,您会注意到一些进程成为了守护进程。libvirt进程也有一个守护进程,叫做libvirtd。与其他守护进程一样,libvirtd为其client发起的请求提供服务。让我们试着了解libvirt client(如virsh或virt-manager)向libvirtd请求服务时,到底发生了什么。根据客户端传递的连接URI(将在接下来的部分中讨论),libvirtd建立起与hypervisor的连接。客户端程序virsh或virt-manager就是通过这样的方式使libvirtd与hypervisor建立通信的。在本书的范围内,我们的目标是KVM虚拟化技术。因此,最好是用QEMU/KVM hypervisor而不是其他hypervisor为场景来考虑与libvirtd的通信。你可能会对使用QEMU/KVM而不是QEMU或KVM作为底层hypervisor的名称有些困惑。但别担心,一切都会在适当的时候变得明了。下面会讨论QEMU和KVM之间的联系。目前你只需了解该hypervisor同时用到了QEMU和KVM技术。

让我们回到通过libvirt客户端virsh传递的连接URI。当我们将注意力集中在QEMU/KVM虚拟化时,从客户端传递的连接URI包含“QEMU”字符串,或者传递给libvirt的连接URI是以下形式的:

  • qemu://xxxx/system
  • qemu://xxxx/session

前一个(qemu://xxxx/system)请求以root身份连接到本地管理的QEMU和KVM域,或者VM的守护进程。后一个(qemu://xxxx/session)请求以普通用户身份连接到它自身的QEMU和KVM域。之前,我们提到libvirt也支持远程连接;幸运的是要实现这个功能,只需在连接URI上做一个小小的更改。也就是说,可以通过在连接URI中更改某些字符串来建立远程连接。例如,连接URI的通用格式如下:

driver[+transport]://[username@][hostname][:port]/[path][?extraparameters]

远程连接的virsh简单命令行示例如下:

$ virsh --connect qemu+ssh://root@remoteserver.yourdomain.com/system list --all

如virsh命令示例所示(qemu+ssh://root@remoteserver.yourdomain.com/system),远程URI是由本地URI,主机名和/或transport name组成:

<Mastering KVM Virtualization>:第二章 KVM内部原理

前面的图显示了远程连接是如何与其他系统上的libvirt进行通信的。稍后将介绍驱动程序API或驱动程序实现的细节。 When using a URI scheme of "remote",it will tell the remote libvirtd server to probe for the optimal hypervisor driver。以下内容将提供一些关于 “remote driver” 的详细信息。有关remote connection URI的更多选项,请参阅下面的URL以获取更多细节:

要了解libvirt的工作原理,我们来看看代码。本节包含一些面向开发者的细节,如果您一点都不想知道libvirt内部是如何工作的,您完全可以跳过这一部分。如果你有那么一点想知道,那就来完成它吧。

libvirt的内部运作机制

待补充

Time to think more about QEMU

Quick Emulator(QEMU)由Fabrice Bellard(FFmpeg的创作者)编写,属于*软件(free software),在GPL协议下许可开发。

QEMU是一个通用和开源的机器仿真和虚拟化软件。当作为机器仿真使用时,QEMU可以在一种机器上(比如你的PC)运行为另一种机器(比如ARM开发板)编写的操作系统和应用程序。通过使用动态转译技术,它的性能非常好(见www.QEMU.org)。

让我重新解释一下前面的段落,并给出一个更具体的解释。QEMU实际上是一个执行硬件虚拟化的Hypervisor/VMM。你是不是感觉有些困惑?如果是的,别担心。在本章末尾,您将获得一个更好的了解,特别是当您浏览完每个相关的组件并关联用于执行虚拟化的整个路径。QEMU既可以充当模拟器(Emulator)也可以用作虚拟机(Virtualizer):

  • Qemu as an Emulator:在第1章《理解Linux虚拟化》中,我们简单讨论了二进制转译。当QEMU作为一个仿真器运行时,它能够在一种机器上运行为另外类型机器设计的OS/应用程序。它是如何做到的?这其中就用到了二进制转译的方法。这种模式下,QEMU通过动态二进制转译模拟CPU,并提供一组设备模型。因此,可以在不同的架构下运行多种不同的未修改的Guest OS。将Guest code运行在Host CPU上就需要用到二进制转译技术。完成这项工作的二进制转译程序称为Tiny Code Generator(TCG);这是一个即时编译器。它将为给定处理器编写的二进制代码转换为运行在另一个处理器的二进制代码(例如:ARM运行在X86上):

TCG旨在消除依赖于特定版本GCC或其他编译器的这一缺点,而是将编译器(代码生成器)和运行时由QEMU执行的其他任务结合起来。整个转译任务由两部分组成:target code(TBs)被重写成TCG ops(一种独立于机器的中间代码),随后由TCG根据主机的架构编译此中间代码。并根据需要进行相关优化。

TCG需要编写专用代码来支持它运行的每个架构。(TCG info from Wikipedia https://en.wikipedia.org/wiki/QEMU#Tiny_Code_Generator)

<Mastering KVM Virtualization>:第二章 KVM内部原理

  • QEMU as virtualizer:这种模式QEMU直接在主机CPU上执行Guest code,从而达到了原生性能。例如,当在Xen/KVM hypervisor下工作时,QEMU可以在这种模式下运行。如果KVM是底层hypervisor,那么QEMU可以对嵌入式客户机进行虚拟化,如Power PC、S390、x86等。简而言之,QEMU可以在没有KVM的情况下,以前文提到的二进制转译的方式运行。与启用KVM的硬件辅助虚拟化相比,这种方式的执行速度会慢一些。无论在哪种模式下,QEMU不仅模拟处理器,还模拟了不同的外围设备,如磁盘、网络、VGA、PCI、串口和并行端口、USB等。除了I/O设备的模拟,在使用KVM时,QEMU-KVM创建并初始化虚拟机。它还为每个vCPU(引用下图)初始化不同的posix线程。此外,它还提供了一个框架,在QEMU-KVM用户模式地址空间中,模拟虚拟机的物理地址空间:

<Mastering KVM Virtualization>:第二章 KVM内部原理

为了在物理CPU中执行Guest code,QEMU使用了posix线程。也就是说,Guest虚拟CPU在主机内核中作为posix线程执行。这本身就带来了许多好处,因为从上层来看它们只是主机内核的一些进程。从另一个角度看,KVM hypervisor的用户空间部分由QEMU提供。QEMU通过KVM内核模块运行客户代码。在使用KVM时,QEMU也执行I/O仿真、I/O设备设置、实时迁移等。

QEMU访问KVM模块创建的设备文件(/dev/kvm),并执行ioctls()。请参阅KVM的下一节,了解这些ioctls()。最后,KVM利用QEMU成为一个完整的hypervisor,而KVM是利用CPU提供的硬件虚拟化扩展(VMX或SVM)的加速器或促成器,与CPU架构紧密耦合。间接地,这表明VM也必须使用相同的架构以使用硬件虚拟化扩展/功能。一旦启用KVM,它肯定会比其他技术(如二进制转译)提供更好的性能。

QEMU-KVM internals

在开始研究QEMU内部之前,让我们先git clone QEMU的仓库:

#git clone git://git.qemu-project.org/qemu.git

一旦git clone完成,您就可以看到repo内部的文件层次结构,如下面的截图所示:

<Mastering KVM Virtualization>:第二章 KVM内部原理

一些重要的数据结构和ioctls()构成了QEMU用户空间和KVM内核空间。一些重要的数据结构是KVMState,CPU {X86} State,MachineState等等。在我们进一步探讨内部构成之前,我想指出的是,对它们的详细介绍超出了本书的范围;然而,我依然将提供足够的指引来理解在底层发生的事情,并为进一步的解释提供额外的参考。

Data structures

待补充

Threading models in QEMU

QEMU-KVM是一个多线程的、事件驱动的(带有一个大锁)应用程序。重要的线程有:

  • 主线程
  • 后端虚拟磁盘I/O的工作线程
  • 对应每个vCPU的线程

对应每个VM,都有一个QEMU进程在主机系统中运行。如果Guest关闭,这个过程将被销毁/退出。除了vCPU线程之外,还有专门的iothreads运行一个select(2)事件循环来处理I/O,例如处理网络数据包和磁盘I/O。IO线程也由QEMU派生。简而言之,情况将是这样的:

<Mastering KVM Virtualization>:第二章 KVM内部原理

在我们进一步讨论之前,总有一个关于Guest物理内存的问题:它位于哪里?情况是这样的:如之前的图片所示,Guest RAM是在QEMU进程的虚拟地址空间内分配的。也就是说,Guest的物理内存在QEMU进程地址空间内。

NOTE:关于线程的更多细节可以从线程模型中获取:http://blog.vmsplice.net/2011/03/qemu-internals-overall-architecutre-and-html?m=1

事件循环线程也称为iothread。事件循环用于计时器、文件描述符监控等。main_loop_wait()是QEMU主事件循环线程,它的定义如下所示。这个main event loop thread负责main loop services:包括文件描述符回调、 bottom halves和计时器(在qemu-timer.h中定义)。Bottom halve类似于立即执行的计时器,但开销要更低一些,调度它们是无等待的、线程安全的和信号安全的。

File:vl.c

static void main_loop(void)  {
bool nonblocking;
int last_io = ;
...
do {
nonblocking = !kvm_enabled() && !xen_enabled() && last_io > ;
…...
last_io = main_loop_wait(nonblocking);
…...
} while (!main_loop_should_exit());
}

在我们离开QEMU代码库之前,我想指出,设备代码主要有两个部分。例如,目录hw/block/包含host端的块设备代码,hw/block/包含设备模拟的代码。

KVM实战

终于到了讨论KVM的时间。KVM开发者遵循了和Linux kernel开发者的一样的理念:不要重新发明*。也就是说,他们并没有尝试改变内核代码来创建一个hypervisor;相反,代码是围绕硬件供应商的虚拟化(VMX和SVM)的新硬件支持,以可加载内核模块的形式开发的。有一个通用的内核模块kvm.ko和其他硬件相关的内核模块,如kvm-intel.ko(基于Intel CPU系统)或kvm-amd.ko(基于AMD CPU系统)。相应的,KVM将载入kvm-intel.ko(如果存在vmx标志)或kvm-amd.ko(如果存在svm标志)模块。这将Linux内核变成了一个hypervisor,从而实现了虚拟化。KVM是由qumranet开发的,自2.6.20后成为Linux内核主线的一部分。后来,qumranet被红帽收购。

KVM暴露设备文件/dev/kvm以供应用程序调用ioctls()。QEMU利用这个设备文件与KVM通信,并创建、初始化和管理VM的内核模式上下文。前面,我们提到QEMU-KVM用户空间在QEMU-KVM的用户模式地址空间内,提供了VM的物理地址空间,其中包括memory-mapped I/O。KVM帮助实现这一点。在KVM的协助下还实现了更多的事情。以下是其中一些:

  • Emulation of certain I/O devices, for example (via "mmio") the per-CPU local APIC and the system-wide IOAPIC.
  • Emulation of certain "privileged" (R/W of system registers CR0, CR3 and CR4) instructions.
  • The facilitation to run guest code via VMENTRY and handling of "intercepted events" at VMEXIT.
  • "Injection" of events such as virtual interrupts and page faults into the flow of execution of the virtual machine and so on are also achieved with the help of KVM.

再重复一遍,KVM不是hypervisor!是不是有些迷糊?好的,让我重新解释一下。KVM不是一个完整的hypervisor,但是借助QEMU和模拟器的帮助(一个为I/O设备仿真和BIOS轻度修改的QEMU),它可以成为一个hypervisor。KVM需要支持硬件虚拟化的处理器才能运行。通过使用这些功能,KVM将标准Linux内核变成了一个hypervisor。当KVM运行虚拟机时,每个VM都是一个普通的Linux进程,很明显,它可以在Host kernel的CPU上运行,就像在Host kernel中运行其他进程一样。在第1章《理解Linux虚拟化》中,我们讨论了不同的CPU运行模式。如果您还记得,主要有USER模式和Kernel/Supervisor模式。KVM是Linux内核中的一个虚拟化特性,它允许像QEMU这样的程序直接在主机CPU上执行客户代码。只有当目标架构受到主机CPU的支持时,这才可以实现。

然而,KVM引入了一种名为 “guest mode“ 的模式!简而言之,guest模式是客户系统代码的执行。它可以运行Guest的user或者kernel代码。在具备虚拟化特性的硬件支持下,KVM虚拟化了进程状态、内存管理等。

通过其硬件虚拟化功能,处理器通过Virtual Machine Control Structure(VMCS)和Virtual Machine Control Block(VMCB)来管理host和guest os的处理器状态,并代表虚拟机的OS管理I/O和中断。也就是说,随着这种类型的硬件的引入,诸如CPU指令拦截、寄存器读/写支持、内存管理支持(扩展页表(EPT)和NPT)、中断处理支持(APICv)、IOMMU等等,得到了支持。

KVM使用标准的Linux调度器、内存管理和其他服务。简而言之,KVM所做的是帮助用户空间程序利用硬件虚拟化功能。在这里,您可以将QEMU作为一个用户空间程序来对待,因为它对不同的用户场景进行了良好的集成。当我们说 ”硬件加速虚拟化“时,我们主要指的是Intel VT-x和ADM-Vs SVM。引入虚拟化支持的处理器带来了额外的指令集,称为Virtual Machine Extensions或VMX。

使用Intel的VT-x,VMM在“VMX根模式”中运行,而Guest(未修改的OSs)在“VMX非根模式”中运行。这个VMX给CPU带来了额外的虚拟化指令,比如VMPTRLD、VMPTRST、VMCLEAR、VMREAD、VMWRITE、VMCALL、VMLAUNCH、VMRESUME、VMXOFF和VMXON。VMXON可以打开虚拟化模式(VMX),VMXOFF可以禁用。要执行客户代码,必须使用VMLAUNCH/VMRESUME指令,并使用VMEXIT离开。VMEXIT表示从非根操作切换到根操作。显然,当我们进行这个切换时,需要保存一些信息,以便稍后可以获取它。Intel提供了一种结构,以促进这种被称为Virtual Machine Control Structure(VMCS)的切换;它将用来处理大部分虚拟化管理功能。例如在VMEXIT中,exit的原因将被记录在这个结构中。那么,我们如何从这个结构中读取或写入信息呢?VMREAD和VMWRITE指令可以用于读取或写入VMCS结构中的字段。

最近Intel处理器也提供了一项功能,允许每个guest有自己的页面表来跟踪内存地址。如果没有EPT,hypervisor必须退出虚拟机以执行地址转换,性能就会降低。正如我们在Intel虚拟化CPU中观察到的操作模式一样,AMD的Secure Virtual Machine (SVM)也有一系列操作模式,称为Host mode和Guest mode。正如您所猜测的那样,hypervisor运行在Host mode下,Guest OS运行在Guest mode。显然,在Guest mode下,一些指令可以导致VMEXIT,并以特定的方式处理Guest mode。AMD也有一个等效于VMCS的结构,它被称为Virtual Machine Control Block(VMCB);正如前面所讨论的,它记录VMEXIT的原因。AMD增加了8种新的指令码支持支持SVM。例如,VMRUN指令启动Guest OS的操作,VMLOAD指令从VMCB加载处理器状态,VMSAVE指令将处理器状态保存到VMCB。此外,为了提高内存管理单元的性能,AMD引入了一种称为NPT(嵌套分页)的技术,类似于Intel的EPT。

KVM APIs

如前所述,ioctl()的主要类型有三种。

三套ioctls组成了KVM API。KVM API是一组用于控制虚拟机的各个方面的ioctls。这些ioctls分为这三类:

  • System ioctls:它可以查询并设置了全局属性,影响整个KVM子系统。另外,系统ioctl用于创建虚拟机。
  • VM ioctls:这些查询和设置的属性将影响整个VM,例如内存分布。此外,VM ioctl用于创建虚拟CPU(vCPUs)。它们从创建VM的同一进程(地址空间)运行VM ioctls。
  • vCPU ioctl:这些查询和设置的属性控制单个vCPU的操作。它们从创建vCPU的同一线程上运行vCPU ioctls。

要进一步了解KVM对外发布的ioctl()和属于特定的fd组的ioctls(),请参阅KVM.h:

/*  ioctls for /dev/kvm fds: */
#define KVM_GET_API_VERSION _IO(KVMIO, 0x00)
#define KVM_CREATE_VM _IO(KVMIO, 0x01) /* returns a VM fd */
….. /* ioctls for VM fds */
#define KVM_SET_MEMORY_REGION _IOW(KVMIO, 0x40, struct kvm_memory_region)
#define KVM_CREATE_VCPU _IO(KVMIO, 0x41)
… /* ioctls for vcpu fds */
#define KVM_RUN _IO(KVMIO, 0x80)
#define KVM_GET_REGS _IOR(KVMIO, 0x81, struct kvm_regs)
#define KVM_SET_REGS _IOW(KVMIO, 0x82, struct kvm_regs)

Anonymous inodes and file structures

待补充

Data structures

待补充

Execution flow of vCPU

待补充

概要

在本章中,我们讨论了定义libvirt、QEMU和KVM的内部实现的重要数据结构和函数。我们还讨论了vCPU执行的生命周期以及QEMU和KVM如何在主机CPU上运行Guest OS。我们还讨论了虚拟化的硬件支持、围绕它的重要概念以及它如何在KVM虚拟化中发挥作用。有了这些概念和说明,我们可以更详细地探索KVM虚拟化的细节。

在下一章中,我们将介绍如何使用libvirt管理工具建立一个独立的KVM。

上一篇:js在工作中遇到的一些问题


下一篇:node.js第一次