虚拟化是云计算的基石,抛开虚拟化谈云计算无异于缘木求鱼,不得要领。

虚拟化简介

虚拟化是一种技术,它是对物理硬件资源的虚拟。通过虚拟化使得应用运行在虚拟化之后的虚拟机上,达到充分利用物理资源的目的。

 

根据虚拟化的类型可将虚拟化分为 I 型虚拟化和 II 型虚拟化。I 型虚拟化是直接作用于裸机上的,如 Xen 等虚拟化技术。II 型虚拟化是在操作系统之上的虚拟化,如 qemu-kvm,HyperV 等虚拟化技术。

 

根据虚拟化技术可将虚拟化分为:全虚拟化,半虚拟化和硬件辅助的虚拟化。

 

全虚拟化

虚拟化的两种架构是 虚拟化技术分为两大类_虚拟化


如上图所示,全虚拟化是通过 QEMU 的模拟代码来模拟 Guest 请求的虚拟化。全虚拟化需要经过捕获/拦截/模拟等一系列流程。

 

物理上的 CPU 作用在不同的运行级别上,分别是 Ring3,Ring2,一直到 Ring0。对于 kernel 的代码,CPU 是运行在 Ring0 级别,可以执行特权指令,例如访问硬件资源等指令。而,作用在其它级别上时,CPU 是处于受限模式的。对于 QEMU 和 Guest 来说,CPU 在执行它们的指令时没有用 Ring0 级别去执行,当 Guest 执行 I/O 请求的时候,CPU 会报异常,这个异常会被内核模块的 KVM 捕获到,KVM 将该异常消息放入 I/O sharing page 中,然后通过 /dev/kvm 接口,告知 QEMU 去读取 sharing page 中的异常消息,QEMU 读取到该异常消息,通过自己的模拟代码来驱动内核中相应的设备模块实现 I/O 操作。当操作成功后,把结果放到 I/O sharing page 中,并通知 KVM 内核去读取该操作成功后,把结果,KVM 读取到该结果,将结果发给 Guest 的驱动程序实现整个 I/O 请求。

 

全虚拟化的指令都需要经过捕获/拦截/模拟的过程,很复杂,效率很低。

 

半虚拟化

虚拟化的两种架构是 虚拟化技术分为两大类_虚拟化_02

 

如图所示,半虚拟化是对 Guest 的模块进行修改的虚拟化。现在的半虚拟化通常是基于 virtio 框架的虚拟化。在 Guest 中包含 virtio 的 Front-end,在 Host 上安装 virtio 的 Back-end。当有 I/O 请求时,Guest 上的前端 virtio driver 会直接和 Host 上的 virtio devcie 进行通信,实现对硬件资源的访问。

 

虚拟化的两种架构是 虚拟化技术分为两大类_磁盘镜像_03

相比于 qemu 的全虚拟化,半虚拟化绕过了 QEMU ,从而少了指令捕获/拦截/模拟的过程,效率要更高。

 

完整的 Guest I/O 流程如下图所示: 

虚拟化的两种架构是 虚拟化技术分为两大类_虚拟化_04


硬件辅助的全虚拟化

硬件辅助的全虚拟化最少需要硬件 CPU 支持。Intel VT 技术支持硬件辅助的全虚拟化方案,在使用时需要进入 BIOS 中打开 VT。

 

虚拟化的两种架构是 虚拟化技术分为两大类_虚拟化_05

 

在硬件辅助的全虚拟化中,CPU 有两种执行模式,root 模式和 non-root 模式,对于 QEMU + kernel 代码的运行,CPU 是运行在 root 模式,而对于 Guest 代码的执行,CPU 运行在 non-root 模式。在 non-root 模式中,CPU 处于受限状态,只能执行限制的指令,无法执行特权指令。当 Guest 需要执行特权指令时,CPU 会从 non-root 模式切到 root 模式来执行该指令,此时 Guest 会被置于挂起状态。当执行完特权指令之后,退出到 non-root 模式,继续 Guest 的运行。

 

从 root 到 non-root 模式的切换叫 VM-Entry,从 non-root 到 root 模式的切换叫 VM-Exit。

 

相比于全虚拟化,硬件辅助的虚拟化不需要逐条翻译 Guest 指令,大部分指令都可以通过 CPU 执行,从而缩短了“流程”,提升了虚拟化的效率。

 

虚拟化原理

虚拟化从本质上来说是对 CPU / 内存 以及 I/O 资源的虚拟,使得 Guest 有自己的硬件资源,从 Guest 角度看,它就是一台独立的 server。

 

CPU 虚拟化

CPU 的虚拟化是通过 KVM 内核模块完成的,KVM 虚拟出 vCPU,Guest 上看到的 socket / core 实际上是绑定到 vCPU 上的。

 

虚拟化的两种架构是 虚拟化技术分为两大类_虚拟化的两种架构是_06

vCPU 有三种模式,kernel / user 和 guest 模式,当 Guest 执行 I/O 相关的操作时,vCPU 是运行在 user 模式下的,当执行非 I/O 的用户空间操作,vCPU 是处在 guest 模式下的,当执行 kernel 代码时,vCPU 是处在 kernel 模式下的。

 

从 Host 上来看,Guest 就是一个 qemu-kvm 进程,而 Guest 上的 CPU 就是 qemu-kvm 上的一个线程,物理 CPU 会像调用普通进程一样调度它们。正因为 vCPU 是普通线程,所以它并不能真正代替 CPU 去执行 CPU 的指令,真正执行 Guest 上指令的还是 Host 上的物理 CPU。

虚拟化的两种架构是 虚拟化技术分为两大类_d3_07

 

从上图可以看到,vCPU 上的操作通过 CPU Scheduler 层层调度到指定的物理 CPU core 上执行,真正完成 Guest 操作的是物理 CPU(这也是为什么硬件辅助虚拟化最少需要物理 CPU 支持的原因)。

当然,可以通过绑核的机制实现 vCPU 和物理 CPU core 的绑定。

 

内存虚拟化

虚拟化的两种架构是 虚拟化技术分为两大类_虚拟化_08

 

KVM 实现客户机内存的方式是,利用 mmap 系统调用,在 QEMU 主线程的虚拟地址空间中申明一段连续大小的空间用于客户机物理内存映射。

 

 

虚拟化的两种架构是 虚拟化技术分为两大类_d3_09

在 VM 中,进程的虚拟内存(VA)会映射到 VM 的物理内存(PA),而 PA 向下被映射到 Host 的机器内存(MA)。 但是 VM 操作系统不能直接实现 PA 到 MA 的映射,需要 VMM(KVM) 实现 PA 到 MA 的映射。

 

VMM 实现 PA 到 MA 的映射有两种方式:

1. 软件方式:通过影子页表技术实现内存地址的翻译;

2. 硬件方式:基于 CPU 的硬件辅助虚拟化,如 Intel 的 EPT 和 AMD 的 NPT 技术。

 

I/O 资源的虚拟化

I/O 资源的虚拟化,分为:

1. 设备模拟:在虚拟机监控器中模拟传统 I/O 设备的特性,比如在 QEMU 中模拟一个 Intel 的网卡和 IDE 硬盘驱动器,在客户机中就暴露为对应的硬件设备。客户机中的 I/O 请求都由虚拟机监控器捕获并模拟执行后返回给客户机(前面全虚拟化方式介绍的就是这种)。

2. 前后端驱动接口:在 VMM 和 VM 之间定义一种全新的适合于虚拟化环境的交互接口,常见的如 virtio 框架下,暴露给客户机的 virtio-net,virtio-blk 等网络和磁盘设备(前端设备),在 QEMU 中实现为相应的后端驱动。

3. 设备直接分配:将 Host 上的物理设备直接分配给 VM,如网卡或硬盘驱动器直接分配给 VM 等。

4. 设备共享分配:设备直接分配的 I/O 虚拟化分配的设备是极为有限的,设备共享分配将物理设备虚拟为多个虚拟机功能接口,该接口独立地分配给客户机使用,从而支持多个 VM 的设备分配。如 SRIOV 就是一种常见的设备共享分配的虚拟化 I/O 方式。

 

搭建 KVM 虚拟机

这里实现的是 qemu-kvm 硬件辅助的虚拟化方式。首先搭建虚拟化环境,打开 BIOS 中的 VT,编译 kvm 模块进内核,下载安装 qemu 软件,准备好 VMM 环境:

[root@localhost home]# lsmod | grep -i kvm
kvm_intel             170181  5
kvm                   554609  1 kvm_intel
irqbypass              13503  15 kvm,vfio_pci
[root@localhost home]# rpm -qa | grep qemu
qemu-img-rhev-2.6.0-28.el7_3.6.x86_64
qemu-kvm-common-rhev-2.6.0-28.el7_3.6.x86_64
qemu-kvm-rhev-2.6.0-28.el7_3.6.x86_64
libvirt-daemon-driver-qemu-2.0.0-10.el7_3.4.x86_64
ipxe-roms-qemu-20160127-5.git6366fa7a.el7.noarch

 

环境准备好之后创建磁盘镜像,将操作系统加载到磁盘镜像中,通过 qemu-kvm 命令创建 VM:

[lianhua@localhost qemu-kvm]$ /usr/libexec/qemu-kvm -m 1G -smp 4 Virtualized.qcow2 -monitor stdio
QEMU 2.6.0 monitor - type 'help' for more information
(qemu) VNC server running on '::1;5900'
 
(qemu) info status
VM status: running
(qemu) info cp
cpus      cpustats
(qemu) info cpus
* CPU #0: pc=0xffffffff9141c0ee (halted) thread_id=825030
  CPU #1: pc=0xffffffff9141c0ee (halted) thread_id=825031
  CPU #2: pc=0xffffffff9141c0ee (halted) thread_id=825032
  CPU #3: pc=0xffffffff9141c0ee (halted) thread_id=825033
 
[lianhua@localhost ~]$ ps -lef | grep -i qemu
2 S lianhua     825024  819281 32  80   0 - 537525 poll_s 16:56 pts/0   00:02:16 /usr/libexec/qemu-kvm -m 1G -smp 4 Virtualized.qcow2 -monitor stdio

 

这里磁盘镜像已经事先制作好了,通过 qemu-kvm 启动 VM,其中 -m 表示 VM 的运行内存;-smp 表示 VM 的 CPU 架构, -smp 4 表示有 4 个 core;-monitor 表示直接将 VMM monitor 重定向到当前命令行所在的标准输入输出设备上。

 

进入 monitor 中使用 info status 和 info cpus 查询 VM 的状态和 CPU 的使用情况。可以看到 CPU 其实是 Host 上的线程,CPU 0 对应的线程 id 是 825030,CPU 1 对应的线程 id 是 825031,依次类推...,而 VM 则是 Host 上的一个 qemu 进程,其进程 id 为 825024。

 

值得注意的是,即时使用一个空的磁盘镜像,qemu-kvm 还是能让它运行,但是可以想见,由于空磁盘镜像并没有安装操作系统,实际上是没有任何意义的(能想到的用处是测试 OpenStack 上 cinder / swift 组件是否正常,通常会使用一个测试镜像测试该镜像是否可以上传/安装/被 VM 使用):

[lianhua@localhost qemu-kvm]$ ll -h lianhua.raw
-rw-r--r--. 1 lianhua lianhua 20G Jul  5 16:52 lianhua.raw
[lianhua@localhost qemu-kvm]$ du lianhua.raw
0       lianhua.raw
[lianhua@localhost qemu-kvm]$ /usr/libexec/qemu-kvm -m 1G -smp 5 lianhua.raw -monitor stdio
WARNING: Image format was not specified for 'lianhua.raw' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
QEMU 2.6.0 monitor - type 'help' for more information
(qemu) VNC server running on '::1;5900'
 
(qemu) info status
VM status: running
(qemu) info cpus
* CPU #0: pc=0x000000003fefa56a thread_id=888039
  CPU #1: pc=0x00000000000fd374 (halted) thread_id=888041
  CPU #2: pc=0x00000000000fd374 (halted) thread_id=888042
  CPU #3: pc=0x00000000000fd374 (halted) thread_id=888043
  CPU #4: pc=0x00000000000fd374 (halted) thread_id=888044
(qemu) info cpus
* CPU #0: pc=0x00000000000fc373 (halted) thread_id=888039
  CPU #1: pc=0x00000000000fd374 (halted) thread_id=888041
  CPU #2: pc=0x00000000000fd374 (halted) thread_id=888042
  CPU #3: pc=0x00000000000fd374 (halted) thread_id=888043
  CPU #4: pc=0x00000000000fd374 (halted) thread_id=888044

仔细看在创建 image 的时候, qemu 会聪明的不让镜像占用磁盘空间,而是等需要占用的时候再为它分配磁盘空间。使用 prealloction=full 选项可以在创建的时候为镜像分配磁盘空间。

 

 

芝兰生于空谷,不以无人而不芳。