虚拟化技术实现 — QEMU-KVM

目录

前文列表

虚拟化技术实现 — 虚拟化技术发展编年史网络

KVM

KVM(Kernel-based Virtual Machine,基于内核的虚拟机)是一种用于 Linux 内核中的虚拟化基础设施。本质是一个嵌入到 Linux 内核中的虚拟化功能模块 kvm.ko(kvm-intel.ko/kvm-AMD.ko),该模块在利用 Linux 内核所提供的部分操做系统能力(e.g. 任务调度、内存管理、硬件设备交互)的基础上,再加入了处理器和内存虚拟化的能力,使得 Linux 内核具有了成为 VMM 的条件。KVM 于 2007 年 2 月 5 日被集成到 Linux 2.6.20 内核中。使用 KVM 的前提是宿主机必须拥有支持硬件虚拟化拓展特性(Intel VT 或者 AMD-V)的处理器。数据结构

在这里插入图片描述

KVM 的功能清单多线程

  • 支持 CPU 和 Memory 超分(Overcommit)
  • 支持半虚拟化 I/O(virtio)
  • 支持热插拔 (CPU、块设备、网络设备等)
  • 支持 SMP(Symmetric Multi-Processing,对称多处理)处理器架构
  • 支持 NUMA (Non-Uniform Memory Access,非一致存储访问)处理器架构
  • 支持实时迁移(Live Migration)
  • 支持 PCI 设备直接分配(Pass-through)和单根 I/O 虚拟化 (SR-IOV)
  • 支持合并相同内存页 (KSM )

以 Intel VT 为例,当启动 Linux 操做系统并加载 KVM 内核模块时:架构

  1. 初始化 KVM 模块内部的数据结构;
  2. KVM 模块检测当前的 CPU 体系结构,而后打开 CPU 控制器以及存取 CR4 寄存器的虚拟化模式开关,并经过执行 VMXON 指令将 Host OS/VMM(在 KVM 环境中,Host OS 便是 VMM)置于虚拟化模式的根模式(Root Mode);
  3. 最后,KVM 模块建立特殊的接口设备文件 /dev/kvm 并等待来自用户空间(QEMU)的指令。

但须要注意的是,KVM 是运行在内核态的且自己不能进行任何设备的模拟。因此,KVM 还必须借助于一个运行在用户态的应用程序来模拟出虚拟机所须要的虚拟设备(e.g. 网卡、显卡、存储控制器和硬盘)同时为用户提供操做入口。目前这个应用程序的最佳选择就是 QEMU。性能

QEMU

QEMU(Quick Emulator)是一款免费的、开源的、纯软件实现的、可执行硬件虚拟化的 VMM。与 Bochs,PearPC 等模拟器相比,QEMU 具备高速(配合 KVM)以及跨平台的特性。ui

在这里插入图片描述

事实上,QEMU 自己做为一套完整的 VMM 实现,包括了处理器虚拟化,内存虚拟化,以及模拟各种虚拟设备的功能。QEMU 4.0.0 版本甚至几乎能够模拟任何硬件设备,但因为这些模拟都是纯软件实现的,因此其性能低下。在 KVM 开发者在对 QEMU 进行稍加改造后,QEMU 能够经过 KVM 对外暴露的 /dev/kvm 接口来对其进行调用。从 QEMU 角度来看,也能够说是 QEMU 使用了 KVM 的处理器和内存虚拟化功能,为本身的虚拟机提供了硬件辅助虚拟化加速。除此之外,虚拟机的配置和建立、虚拟机运行所依赖的虚拟设备、虚拟机运行时的用户环境和用户交互,以及一些虚拟机的特定技术,好比:动态迁移,都是交由 QEMU 来实现的。spa

在这里插入图片描述

总的来讲,QEMU 具备如下几种使用方式:操作系统

  1. 纯软件(二进制翻译)实现的全虚拟化虚拟机
  2. 基于硬件辅助虚拟化(KVM)的全虚拟化虚拟机
  3. 仿真器:为用户空间的进程提供 CPU 仿真,让在不一样处理器结构体系上编译的程序得以跨平台运行。例如:让 SPARC 架构上编译的程序在 x86 架构上运行(借由 VMM 的形式)。

QEMU-KVM

KVM 官方提供的软件包下载包含了 KVM 内核模块、QEMU、qemu-kvm 以及 virtio 四个文件。其中,qemu-kvm 本质是专门针对 KVM 的 QEMU 分支代码包(一个特殊的 QEMU 版本)。.net

QEMU-KVM 相比原生 QEMU 的改动:线程

  • 原生的 QEMU 经过指令翻译实现 CPU 的彻底虚拟化,可是修改后的 QEMU-KVM 会调用 ICOTL 命令来调用 KVM 模块。
  • 原生的 QEMU 是单线程实现,QEMU-KVM 是多线程实现。

然而在 QEMU 1.3 版本以后二者又保持一致了,但咱们能仍习惯在 KVM 语境中将其称之为 QEMU-KVM。

NOTE:在 RHEL6/CentOS6 中,qemu-kvm 存放在 /usr/libexec 目录下。不过 PATH 环境变量缺省是不包含此目录的,因此用户没法直接使用 qemu-kvm,这样作是为了防止 QEMU 替代了 KVM 做为 VMM 的角色。若是但愿启用 QEMU 做为 VMM 的话,能够经过将 /usr/libexec/qemu-kvm 连接为 /usr/bin/qemu 来完成。

在这里插入图片描述
在 QEMU-KVM 中,KVM 运行在内核空间,提供 CPU 和内存的虚级化,以及 Guest OS 的 I/O 拦截。QEMU 运行在用户空间,提供硬件 I/O 虚拟化,并经过 ioctl 调用 /dev/kvm 接口将 KVM 模块相关的 CPU 指令传递到内核中执行。当 Guest OS 的 I/O 被 KVM 拦截后,就会将 I/O 请求交由 QEMU 处理。例如:

open("/dev/kvm", O_RDWR|O_LARGEFILE)    = 3
ioctl(3, KVM_GET_API_VERSION, 0)        = 12
ioctl(3, KVM_CHECK_EXTENSION, 0x19)     = 0
ioctl(3, KVM_CREATE_VM, 0)              = 4
ioctl(3, KVM_CHECK_EXTENSION, 0x4)      = 1
ioctl(3, KVM_CHECK_EXTENSION, 0x4)      = 1
ioctl(4, KVM_SET_TSS_ADDR, 0xfffbd000)  = 0
ioctl(3, KVM_CHECK_EXTENSION, 0x25)     = 0
ioctl(3, KVM_CHECK_EXTENSION, 0xb)      = 1
ioctl(4, KVM_CREATE_PIT, 0xb)           = 0
ioctl(3, KVM_CHECK_EXTENSION, 0xf)      = 2
ioctl(3, KVM_CHECK_EXTENSION, 0x3)      = 1
ioctl(3, KVM_CHECK_EXTENSION, 0)        = 1
ioctl(4, KVM_CREATE_IRQCHIP, 0)         = 0
ioctl(3, KVM_CHECK_EXTENSION, 0x1a)     = 0

QEMU-KVM 调用 KVM 内核模块启动虚拟机的流程概要

  1. 获取 /dev/kvm fd(文件描述符)
kvmfd = open("/dev/kvm", O_RDWR);
  1. 建立虚拟机,获取虚拟机的句柄。KVM_CREATE_VM 时,能够理解成 KVM 为虚拟机建立了对应的数据结构,而后,KVM 会返回一个文件句柄来表明该虚拟机。针对这个句柄执行 ioctl 调用便可完成对虚拟机执行相应的管理,好比:建立用户空间虚拟地址(Virtual Address)、客户机物理地址(Guest Physical Address)以及主机物理地址(Host Physical Address)之间的映射关系;
vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0);
  1. 为虚拟机映射内存和其余的 PCI 设备,以及信号处理的初始化。
ioctl(kvmfd, KVM_SET_USER_MEMORY_REGION, &mem);
  1. 将虚拟机镜像数据映射到内存,至关于物理机的 boot 过程,把操做系统内核映射到内存。

  2. 建立 vCPU,并为 vCPU 分配内存空间。KVM_CREATE_VCPU 时,KVM 为每个 vCPU 生成对应的文件句柄,对其执行相应的 ioctl 调用,就能够对 vCPU 进行管理。

ioctl(kvmfd, KVM_CREATE_VCPU, vcpuid);
vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
  1. 建立 vCPU 个数的线程并运行虚拟机。
ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0);
  1. 线程进入循环,监听并捕获虚拟机退出缘由,作相应的处理。这里的退出并不必定指的是虚拟机关机,虚拟机若是遇到 I/O 操做,访问硬件设备,缺页中断等都会执行退出。执行退出能够理解为将 CPU 执行上下文返回到 QEMU。
open("/dev/kvm")

ioctl(KVM_CREATE_VM)
ioctl(KVM_CREATE_VCPU)

for (;;) {
     ioctl(KVM_RUN)
     switch (exit_reason) {  /* 分析退出缘由,并执行相应操做 */
     case KVM_EXIT_IO:  /* ... */
     case KVM_EXIT_HLT:  /* ... */
     }
}