原创

虚拟化技术实现 — KVM 的 CPU 虚拟化

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://is-cloud.blog.csdn.net/article/details/99737161

目录

前文列表

虚拟化技术实现 — 虚拟化技术发展编年史
虚拟化技术实现 — QEMU-KVM

x86 体系结构的虚拟化

首先回顾一下,我们在《虚拟化技术实现 — 虚拟化技术发展编年史》介绍了 x86 体系结构虚拟化所需要面对的 Host OS 和 Guest OS 运行特权级交叉(Host OS 运行在 Ring0,Guest OS 运行在 Ring1,但 Guest OS 却以为自己运行在 Ring0,从而导致执行权限出错)的难题,以及根据 VMM 实现 Guest OS 对计算机系统硬件的访问方式不同而分为三种类型:

  • 全虚拟化:x86 的 Ring0 要运行 Host OS,Ring1 要运行 Guest OS,但 Guest OS 可不知道自己是虚拟机啊,它也要执行 Ring0 的指令集啊。这怎么行呢!那 VMM 就只好先陷入模拟(捕获 Guest OS 的特权指令)再特权降级(使用二进制翻译将特权指令翻译成非特权指令,欺骗 Guest OS)了。
  • 半虚拟化:那我直接告诉 Guest OS 其实自己是个虚拟机好了,把 Guest OS 原本要在 Ring0 上执行的特权指令改造成对 VMM 的调用(Hypercells),这样 Guest OS 就不需要也不会执行 Ring0 的指令了。比较麻烦的就是要改造操作系统代码,而且闭源的 Windows OS 还改不了。
  • 硬件辅助的虚拟化:Host OS 说要运行在 Ring0,Guest OS 你又说要运行在 Ring0,那你们俩就都在运行在 Ring0 好了,我(CPU)改我自己总可以了吧,我新增个运行模式(root mode、not root mode),你们一人占一个运行模式,并且都支持所有的 Ring(Guest OS 运行在 Ring0 中,客户机应用程序运行在 Ring3 中;对 KVM 来说,QEMU 运行在 Ring3,KVM 作为 VMM 运行在 Ring0),这样你们就不用抢了吧。

在这里插入图片描述
而 KVM 属于硬件辅助虚拟化技术,所以下文中我们主要讨论的实际是硬件辅助虚拟化的 CPU 虚拟化实现。

硬件辅助的 CPU 虚拟化

传统的 x86 CPU 具有 Ring 0~3 这 4 个运行状态等级,Linux 只使用了其中的 Ring0 和 Ring3,分别表示内核态和用户态。当 CPU 寄存器标记了当前 CPU 处于 Ring0 级别时,表示此时 CPU 正在运行的是内核的代码。而当 CPU 处于 Ring3 级别的时候,表示此时 CPU 正在运行的是用户空间的应用程序的代码。Ring3 级别是不允许执行硬件操作的,所以,用户态程序的硬件操作都需要经过 Linux 提供的 API 来完成,即发生系统调用(System Call)。这就是操作系统为了 “安全” 地运行多个用户进程而设计的。实际上,无论是系统调用还是进程切换,CPU 都会从 Ring3 转换到 Ring0。

比如用户态的程序需要执行一个 I/O 操作:

int nread = read(fd, buffer, 1024);

当 CPU 执行到此段代码时,首先查找到系统调用号并保存到寄存器 eax,然后会将对应的参数压栈,再产生一个系统调用中断,对应的是 int $0x80。产生了系统调用中断后,CPU 将切换到 Ring0 模式,内核通过寄存器读取到参数,并完成 I/O 后续操作,操作完成后返回 Ring3模式。

movel  $3,%eax
movel  fd,%ebx
movel  buffer,%ecx
movel  1024,%edx      
int    $0x80

而现代具有硬件虚拟化拓展特性的 CPU,本文以 Intel-VT 实现的 VT-x 硬件辅助虚拟化技术为例。VT-x 为 IA 32 处理器增加了两种操作模式:VMX root operation 和 VMX non-root operation。
在这里插入图片描述
VMM 自己运行在 VMX root operation 模式,VMX e’e’e模式则由 Guest OS 使用。两种操作模式都支持 Ring 0 及以上特权级,因此 VMM 和 Guest OS 都可以自由选择它们所期望的运行级别(0~3),以此解决了纯软件实现的全虚拟化的 Host OS 和 Guest OS 运行特权级交叉的难题,不再需要使用软件(VMM)来模拟硬件指令集,虚拟机(Virtual Machine,简称 VM)的指令集直接运行在宿主机处理器上。

由 VMX 切换支撑的 CPU 虚拟化技术

所谓的 VMX 切换,实际上就是 CPU 针对 VMM 与 Guest OS 所实现的在 root operation 和 non-root operation 两个操作模式之间进行的切换。VMM 和 Guest OS 在 CPU 上的协同工作就是通过不断的进行 VMX root operation 和 VMX non-root operation 操作模式的互相转换来完成的。CPU 受控制地在这两种模式之间切换,轮流执行 VMM 代码和 Guest OS 代码,这就是 CPU 虚拟化技术实现的核心
在这里插入图片描述

VMM 与 Guest OS 的操作模式切换主要分为两个部分:

  • VM entry:假设当前占用 CPU 的是 VMM,那么运行在 VMX root operation 模式下的 VMM 可以通过显式调用 VMLAUNCH 或 VMRESUME 指令将 CPU 运行模式切换到 VMX non-root operation 模式。如此,CPU 就会自动的从 VMCS 寄存器(这个寄存器实际是一个指针,保存了真实上下文的地址)加载 Guest OS 的上下文并运行 Guest OS。

  • VM exit:而当 Guest OS 在运行过程中遇到需要 VMM 处理的事件时,例如:外部中断或缺页异常(page fault)或者主动调用 VMCALL 指令调用 VMM 的服务的时候(与系统调用类似),CPU 则主动将该 Guest OS 的上下文保存到 VMCS 寄存器,然后 CPU 再挂起 Guest OS,并切换到 VMX root operation 模式,恢复 VMM 的运行。

在这里插入图片描述

再举一个具体的例子:如果 VM exit 的原因是 Guest OS 发出了 I/O 请求,那么 VMM 可以直接读取 VM 的内存并将 I/O 操作模拟出来,然后再调用 VMRESUME 指令进行 VM entry,VM 继续执行。此时在 VM 看来,Guest OS 的 I/O 操作的指令被 CPU 执行了。

在这里插入图片描述

又因为 VMM 和 Guest OS 共享底层的处理器资源,所以硬件需要一个物理内存区域来自动保存或恢复彼此执行的上下文。这个区域称为虚拟机控制块(VMCS)

VMCS(Virtual-Machine control structure,虚拟机控制结构)在上文提到过,VMCS 是一个 64 位的指针,指向一个真实的内存地址,VMCS 是以 vCPU 为单位的,就是说当前有多少个 vCPU,就有多少个 VMCS 指针。VMCS 的操作包括 VMREAD,VMWRITE,VMCLEAR。VMCS 包括了客户机状态区(Guest State Area),主机状态区(Host State Area)和执行控制区(Execute State Area)。VM entry 时,硬件会自动从客户机状态区加载 Guest OS 的上下文。值得注意的是,VMCS 并不需要保存 VMM 的上下文,原因与中断处理程序类似,因为 VMM 如果开始运行,就不会受到 Guest OS 的干扰,只有 VMM 将工作彻底处理完毕才可能自行切换到 Guest OS。而 VMM 的下次运行必然是处理一个新的事件,因此每次 VM entry 时, VMM 都从一个通用事件处理函数开始执行;VM exit 时,硬件自动将 Guest OS 的上下文保存到客户机状态区,从主机状态区中加载 VMM 的通用事件处理函数的地址,VMM 开始执行。而执行控制区存放的则是可以操控 VM entry 和 VM exit 的标志位,例如:标记哪些事件可以导致 VM exit,VM entry 时准备自动给 Guest OS “塞” 入哪种中断等等。

客户机状态区和主机状态区都应该包含部分物理寄存器的信息,例如控制寄存器 CR0,CR3,CR4;ESP 和 EIP(如果处理器支持 64 位扩展,则为 RSP,RIP);CS,SS,DS,ES,FS,GS 等段寄存器及其描述项;TR,GDTR,IDTR 寄存器;IA32_SYSENTER_CS,IA32_SYSENTER_ESP,IA32_SYSENTER_EIP 和 IA32_PERF_GLOBAL_CTRL 等 MSR 寄存器。客户机状态区并不包括通用寄存器的内容,VMM 自行决定是否在 VM exit 的时候保存它们,从而提高了系统性能。客户机状态区还包括非物理寄存器的内容,比如一个 32 位的 Active State 值表明 Guest OS 执行时处理器所处的活跃状态,如果正常执行指令就是处于 Active 状态,如果触发了三重故障(Triple Fault)或其它严重错误就处于 Shutdown 状态,等等。

在这里插入图片描述

执行控制区用于存放可以操控 VM entry 和 VM exit 的标志位,包括:

  • External-interrupt exiting:用于设置是否外部中断可以触发 VM exit,而不论 Guest OS 是否屏蔽了中断。
  • Interrupt-window exiting:如果设置,当 Guest OS 解除中断屏蔽时,触发 VM exit。
  • Use TPR shadow:通过 CR8 访问 Task Priority Register(TPR)的时候,使用 VMCS 中的影子 TPR,可以避免触发 VM exit。同时执行控制区还有一个 TPR 阈值的设置,只有当 Guest OS 设置的 TR 值小于该阈值时,才触发 VM exit。
  • CR masks and shadows:每个控制寄存器的每一位都有对应的掩码,控制 Guest OS 是否可以直接写相应的位,或是触发 VM exit。同时 VMCS 中包括影子控制寄存器,Guest OS 读取控制寄存器时,硬件将影子控制寄存器的值返回给 Guest OS。

VMCS 还包括一组位图以提供更好的适应性:

  • Exception bitmap:选择哪些异常可以触发 VM exit,
  • I/O bitmap:对哪些 16 位的 I/O 端口的访问触发 VM exit。
  • MSR bitmaps:与控制寄存器掩码相似,每个 MSR 寄存器都有一组 “读” 的位图掩码和一组“写”的位图掩码。

每次发生 VM exit 时,硬件自动在 VMCS 中存入丰富的信息,方便 VMM 甄别事件的种类和原因。VM entry 时,VMM 可以方便地为 Guest OS 注入事件(中断和异常),因为 VMCS 中存有 Guest OS 的中断描述表(IDT)的地址,因此硬件能够自动地调用 Guest OS 的处理程序。

传统的全虚拟化实现了硬件的辅助之后,由于 CPU 引入了新的操作模式,VMM 和 Guest OS 的执行由硬件自动隔离开来,任何关键的事件都可以将系统控制权自动转移到 VMM,因此 VMM 能够完全控制系统的全部资源。Guest OS 也可以运行在它所期望的最高特权级别,因此特权级压缩和特权级别名的问题迎刃而解,而且 Guest OS 中的系统调用也不会触发 VM exit。

硬件使用物理地址访问虚拟机控制块(VMCS),而 VMCS 保存了 VMM 和 Guest OS 各自的 IDTR 和 CR3 寄存器,因此 VMM 可以拥有独立的地址空间,Guest OS 能够完全控制自己的地址空间,地址空间压缩的问题也不存在了。中断和异常虚拟化的问题也得到了很好的解决。VMM 只用简单地设置需要转发的虚拟中断或异常,在 VM entry 时,硬件自动调用 Guest OS 的中断和异常处理程序,大大简化 VMM 的设计。同时,Guest OS 对中断的屏蔽及解除可以不触发 VM exit,从而提高了性能。而且 VMM 还可以设置当 Guest OS 解除中断屏蔽时触发 VM exit,因此能够及时地转发积累的虚拟中断和异常。

KVM 的 CPU 虚拟化实现

在明白了 VMX 模式切换的实现原理之后,再理解 KVM 的 CPU 虚拟化实现就简单了。

作为 VMM 的 KVM 是运行在 VMX root operation 的,在 KVM 完成了 VM 的 vCPU、内存初始化后,QEMU 再通过 ioctl 调用 KVM 的 /dev/kvm 接口完成 VM 的创建,并创建出一个线程(Thread)来运行 VM。由于 VM 在前期初始化的时候会设置各种寄存器来帮助 KVM 查找到线程运行需要加载的指令入口(main 函数),所以线程在调用了 /dev/kvm 接口后,物理 CPU 的控制权就交给了 VM,Guest OS 就运行在了 VMX non-root operation。相对的,Linux 内核本身具有内核(Kernel)和用户(User)两种执行模式,为了支持带有虚拟化功能的 CPU,KVM 向 Linux 内核再增加了第三种执行模式,即客户机模式(Guest),以该模式来对应 CPU 的 VMX non-root mode

在这里插入图片描述

三种模式的分工为:

  • Kernel mode:负责将 CPU 切换到 Guest mode 执行 Guest OS 代码,并在 CPU 退出 Guest mode 时回到 Kernel 模式。
  • User mode:执行 Guest OS 的 I/O 操作
  • Guest mode:执行 Guest OS 非 I/O 代码,并在需要的时候驱动 CPU 退出 Guest mode 模式。

在这里插入图片描述

KVM 内核模块作为 User mode 和 Guest mode 之间的桥梁:

  • User mode 中的 QEMU-KVM 会通过 ioctl 命令来运行虚拟机。
  • KVM 内核模块收到该请求后,它先做一些准备工作,比如将 vCPU 上下文加载到 VMCS 等,然后驱动 CPU 进入 VMX non-root 模式,开始执行 Guest OS 代码。

vCPU 的调度方式

KVM 虚拟机被实现为常规的 Linux 进程,即一个 Linux qemu-kvm 进程,与其他 Linux 进程一样被标准 Linux 进程调度器程序进行调度。虚拟机的每个 vCPU 作为线程(Thread)运行在 qemu-kvm 进程的上下文中,本质是一个常规的 Linux 线程。这使得 KVM 虚拟机可以直接使用 Linux 内核已有的操作系统功能。qemu-kvm 进程 进程包含下列几种线程:

  • I/O 线程用于管理模拟设备
  • vCPU 线程用于运行 Guest 的代码
  • 其它线程,比如处理 event loop,offloaded tasks 等的线程

在这里插入图片描述
pCPU、vCPU、QEMU 进程以及 LInux 进程调度器之间的逻辑关系如下图。
在这里插入图片描述

在调度层面,Guest OS 与 VMM 共同构成了 vCPU 的两级调度系统。Guest OS 负责第 2 级调度,即用客户机应用程序的线程或进程在 Guest OS 上的调度(将线程映射到相应的 vCPU 上)。VMM 则负责第 1 级调度,即 vCPU 对应的线程调度到 pCPU 之上。两级调度系统的调度策略和机制不存在依赖关系。由 VMM 中的 vCPU 调度器负责 pCPU 在各个 VM 之间的分配与调度,本质上把各个 VM 中的 vCPU 按照一定的策略和机制调度在 pCPU 上,可以采用任意的策略来分配物理资源,满足 VM 的不同需求。vCPU 可以调度在一个或多个 pCPUs 执行(分时复用或空间复用 pCPU,vCPU 会漂移),也可以与 pCPU 建立一对一固定的映射关系(即 vCPU 绑定)。

在这里插入图片描述
可见,要将客户机内的线程调度到某个物理 CPU,需要经历两个过程:

  1. 客户机线程调度到 vCPU,该调度由 Guest OS 完成。在 KVM 上,vCPU 在 Guest OS 看起来就像物理 CPU,因此其调度方式没有什么特别之处。
  2. vCPU 线程调度到 pCPU 即主机物理 CPU,该调度由 VMM(即 Linux)负责。

KVM 使用标准的 Linux 进程调度方法来调度 vCPU 进程。Linux 系统中,线程和进程的区别是进程有独立的内核空间,而线程则是代码的执行单位,也就是调度的基本单位。Linux 中,线程是就是轻量级的进程,也就是共享了部分资源(地址空间、文件句柄、信号量等等)的进程,所以线程也按照进程的调度方式来进行调度。

在这里插入图片描述

根据 Linux 进程调度策略,可以看出,在 Linux 主机上运行的 KVM 客户机的总 vCPU 数目最好是不要超过物理 CPU 内核数,否则,会出现线程间的 CPU 内核资源竞争,导致有虚机因为 vCPU 进程等待而导致速度很慢。

客户机 CPU 拓扑和模型

KVM 支持 SMP 和 NUMA 等多核处理器架构的虚拟机。

  • 对 SMP 类型的客户机,使用:
-smp <n>[,cores=<ncores>][,threads=<nthreads>][,sockets=<nsocks>][,maxcpus=<maxcpus>]
  • 对 NUMA 类型的客户机,使用:
-numa <nodes>[,mem=<size>][,cpus=<cpu[-cpu>]][,nodeid=<node>] 

KVM 支持自定义 CPU 模型 (models),CPU 模型定义了哪些宿主机的 CPU 功能(features)会暴露给 Guest OS。为了让虚拟机能够在具有不同 CPU 功能的宿主机之间做安全迁移,我们往往不应该将所有宿主机的 CPU 功能都暴露给虚拟机,而是取服务器集群的合集,以保证虚拟机迁移的安全性。

获取宿主机的 CPU 模型清单:

$ kvm -cpu ?
x86       Opteron_G5  AMD Opteron 63xx class CPU                      
x86       Opteron_G4  AMD Opteron 62xx class CPU                      
x86       Opteron_G3  AMD Opteron 23xx (Gen 3 Class Opteron)          
x86       Opteron_G2  AMD Opteron 22xx (Gen 2 Class Opteron)          
x86       Opteron_G1  AMD Opteron 240 (Gen 1 Class Opteron)           
x86          Haswell  Intel Core Processor (Haswell)                  
x86      SandyBridge  Intel Xeon E312xx (Sandy Bridge)                
x86         Westmere  Westmere E56xx/L56xx/X56xx (Nehalem-C)          
x86          Nehalem  Intel Core i7 9xx (Nehalem Class Core i7)       
x86           Penryn  Intel Core 2 Duo P9xxx (Penryn Class Core 2)    
x86           Conroe  Intel Celeron_4x0 (Conroe/Merom Class Core 2)   
x86      cpu64-rhel5  QEMU Virtual CPU version (cpu64-rhel5)          
x86      cpu64-rhel6  QEMU Virtual CPU version (cpu64-rhel6)          
x86             n270  Intel(R) Atom(TM) CPU N270   @ 1.60GHz          
x86           athlon  QEMU Virtual CPU version 0.12.1                 
x86         pentium3                                                  
x86         pentium2                                                  
x86          pentium                                                  
x86              486                                                  
x86          coreduo  Genuine Intel(R) CPU           T2600  @ 2.16GHz 
x86           qemu32  QEMU Virtual CPU version 0.12.1                 
x86            kvm64  Common KVM processor                            
x86         core2duo  Intel(R) Core(TM)2 Duo CPU     T7700  @ 2.40GHz 
x86           phenom  AMD Phenom(tm) 9550 Quad-Core Processor         
x86           qemu64  QEMU Virtual CPU version 0.12.1                 

Recognized CPUID flags:
  f_edx: pbe ia64 tm ht ss sse2 sse fxsr mmx acpi ds clflush pn pse36 pat cmov mca pge mtrr sep apic cx8 mce pae msr tsc pse de vme fpu
  f_ecx: hypervisor rdrand f16c avx osxsave xsave aes tsc-deadline popcnt movbe x2apic sse4.2|sse4_2 sse4.1|sse4_1 dca pcid pdcm xtpr cx16 fma cid ssse3 tm2 est smx vmx ds_cpl monitor dtes64 pclmulqdq|pclmuldq pni|sse3
  extf_edx: 3dnow 3dnowext lm|i64 rdtscp pdpe1gb fxsr_opt|ffxsr fxsr mmx mmxext nx|xd pse36 pat cmov mca pge mtrr syscall apic cx8 mce pae msr tsc pse de vme fpu
  extf_ecx: perfctr_nb perfctr_core topoext tbm nodeid_msr tce fma4 lwp wdt skinit xop ibs osvw 3dnowprefetch misalignsse sse4a abm cr8legacy extapic svm cmp_legacy lahf_lm

定义虚拟机的 CPU 模型:

qemu-kvm -cpu <models>

# -cpu host 表示 Guest OS 使用和 Host OS 相同的 CPU model。

-cpu 除了可以指定 Guest OS 的 CPU 模型,还可以指定附加的 CPU 特性。并且 -cpu 会将指定的 CPU 模型的所有功能全部暴露给 Guest OS,即使某些特性在实际的宿主机 pCPU 上并不支持,此时 QEMU-KVM 就会通过软件模拟的方式来支持这些特性,因此,也消耗一些性能。

虚拟机 vCPU 数量分配原则

  1. 不是虚拟机的 vCPU 越多,其性能就越好,因为线程切换会耗费大量的时间;应该根据虚拟机负载需要分配最少的 vCPU。
  2. 主机上的虚拟机的 vCPU 总数不应该超过物理 CPU 内核总数。不超过的话,就不存在 CPU 竞争,每个 vCPU 线程在一个物理 CPU 核上被执行;超过的话,会出现部分线程等待 CPU 以及一个 CPU 核上的线程之间的切换,这会有 overhead。
  3. 将负载分为计算负载和 I/O 负载,对计算负载,需要分配较多的 vCPU,甚至考虑 CPU 亲和性,将指定的物理 CPU 核分给给这些客户机。

总结

KVM 的 CPU 虚拟化依托于硬件辅助虚拟化技术,Intel-VT 为 Guest OS 提供了专属的 CPU 特权级,当需要执行特殊操作的时候,Guest OS 就会将 CPU 的控制权返回给 VMM。当 VMM 处理完特殊操作后再把结果和控制权一并返回给 Guest OS。通过这种让 CPU 在两个的不同特权级之间的切换(并且每个特权级都具有对计算机硬件资源的完全掌控能力)来实现 VMs 和 VMM 之间的协同运行,就是 KVM 的 CPU 虚拟化实现

参考文档

https://www.cnblogs.com/sammyliu/p/4543597.html

文章最后发布于: 2019-08-19 21:28:34
展开阅读全文
0 个人打赏
私信求帮助

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览