- 1 VCPU描述符
- 2 VCPU的创建
- 2.1 VMCS的创建与初始化
- 3 VCPU的运行
- 3.1 上下文切换
- 3.1.1 上下文切换步骤
- 3.1.2 惰性保存/恢复
- 3.2 VCPU的硬件优化
- 3.2.1 读写CR0
- 3.2.2 读取TSC
- 3.2.3 GDTR/LDTR/IDTR/TR的访问
- 3.2.4 读CR2
- 3.2.5 SYSENTER/SYSEXIT
- 3.2.6 APIC访问控制
- 3.2.7 异常控制
- 3.2.8 I/O控制
- 3.2.9 MSR位图
- 3.3 总结
- 3.1 上下文切换
- 4 VCPU的退出
- 4.1 VCPU退出步骤
- 4.2 VCPU退出原因
- 5 VCPU的再运行
- 6 进阶
- 6.1 CPU模式的虚拟化
- 6.2 多处理器虚拟机
1 VCPU描述符
硬件虚拟化使用VCPU(Virtual CPU)描述符来描述虚拟CPU.
VCPU描述符类似操作系统中进程描述符(或进程控制块), 本质是一个结构体, 通常由下列几部分构成.
⓵ VCPU标识信息: 用于标识VCPU的一些属性, 例如VCPU的ID号, VCPU属于哪个客户机等.
⓶ 虚拟寄存器信息: 虚拟的寄存器资源, 在使用Intel VT-x情况下, 这些内容包含在 VMCS!!! 中, 例如客户机状态域保存的内容.
⓷ VCPU状态信息: 类似于进程的状态信息, 标识该VCPU当前所处的状态, 例如睡眠、运行等, 主要供调度器使用。
⓸ 额外寄存器/部件信息: 主要指未包含在VMCS中的一些寄存器或CPU部件. 例如浮点寄存器和虚拟的LAPIC等.
⓹ 其它信息: 用于VMM进行优化或存储额外信息的字段, 例如存放该VCPU私有数据的指针等.
Intel VT-x情况下, VCPU可划分为两个部分,
- 一个是VMCS为主由硬件使用和更新的部分, 主要是虚拟寄存器;
- 一个是除VMCS之外, 由VMM使用和更新的部分, 主要指VMCS以外的部分. 比如VCPU标识、状态信息等.
图5-4展示了VCPU的构成.
当VMM创建客户机, 首先为客户机创建VCPU, 整个客户机运行实际上可以看作VMM调度不同的VCPU运行.
下面以VCPU创建 --- 运行 --- 退出为主线, 介绍Intel VT-x技术的CPU虚拟化的实现.
2 VCPU的创建
实质上是创建VCPU描述符, 由于本质上VCPU描述符是一个结构体, 因此创建VCPU描述符简单来说就是分配相应大小的内存.
VCPU描述符包含的内容很多, 通常会被组织成多级结构, 例如第一级结构体可以是各个平台通用的内容, 中间包含一个指针指向第二级结构体, 包含平台相关的内容. 对于这样的多级结构, 需要为每一级结构体相应分配内存.
创建之后, 需要进一步初始化才能使用. 物理CPU上电后, 硬件会自动将CPU初始化为特定的状态. VCPU的初始化也是一个类似过程. 通常包含:
⓵ 分配VCPU标识: 首先标识该VCPU属于哪个客户机, 再为该VCPU分配一个客户机范围内唯一的标识(!!!).
⓶ 初始化虚拟寄存器组: 主要指出初始化VMCS相关域!!!. 这些寄存器的初始化值通常根据物理CPU上电后各寄存器的值设定.
⓷ 初始化VCPU状态信息: 被调度前需要配置的必要标志. 具体依据调度器实现决定.
⓸ 初始化额外部件: 将未被VMCS包含!!!的虚拟寄存器初始化为物理CPU上电后的值, 并配置虚拟LAPIC等部件.
⓹ 初始化其它信息: 根据VMM的实现初始化VCPU的私有数据.
2.1 VMCS的创建与初始化
VMCS分配时, 只需要分配一块4KB大小, 并对齐到4KB边界的内存即可. 初始化则需要根据VT-x的定义, 对前面内容进行初始化, 基本思想就是按照物理CPU初始化的定义, 提供了一个和物理CPU初始化后类似的状态.
此外, 根据VMM的CPU虚拟化策略, 设置相应的VMCS控制位.
⓵ 客户机状态域: 描述了VCPU运行时的状态, 因此, 初始化的取值基本上是参考了物理CPU初始化后的状态. 例如, 物理CPU加电后会通过复位地址跳转到BIOS执行, 那么Guest RIP字段可直接设置为虚拟机Guest BIOS的起始指令地址!!!.
⓶ 宿主机状态域: VM-Exit时, CPU切换到VMM时寄存器值, 因此, 初始化值参考VMM运行时的CPU的状态. HOST RIP字段通常被设置为VMM中VMX Exit处理函数!!!(VMX Exit Handler)的入口.
⓷ VM-Execution控制域: 控制VCPU运行时的一些行为, 如执行某些敏感指令时是否发生VM-Exit. 例如IN/OUT指令, 如果VMM允许客户机直接访问某些I/O端口, 那么VMM就会将Use I/O bitmaps位置1, 并在I/O bitmap中将相应I/O端口所对应的位置0, 这样, 客户机访问这些I/O端口就不会发生VM-Exit.
⓸ VM-Entry控制域: 主要在每次VM-Entry之前设置, 因此在VCPU初始化时不需要特别设置.
⓹ VM-Exit控制域: 这个状态域有两个字段VMM通常会设置, 一个是Acknowledge interrupt on exit, 有助于更快响应外部中断; 另一个是Host Address Space, 用于支持IA32模式.
⓺ VM-Exit信息域: 硬件自动更新, 因此不需要初始化.
3 VCPU的运行
VCPU创建并初始化后, 可通过调度程序被调度运行. 调度程序会根据一定的策略算法来选择VCPU运行.
调度策略不表, 这里主要描述选定VCPU后, 如何将VCPU切换到物理CPU上运行.
3.1 上下文切换
第2章中, 上下文实际上是一个寄存器的集合, 包括通用寄存器、浮点寄存器、段寄存器、控制寄存器以及MSR等.
前面提到, Intel VT-x的支持下, VCPU的上下文可以分为两部分. 故上下文的切换也分为由硬件自动切换(VMCS部分)和VMM软件切换(非VMCS部分)两个部分. 其中,
- 硬件切换部分可以更好保证VMM与客户机的隔离, 但缺乏灵活性.
- 软件切换部分可以由VMM自己选择性切换需要的上下文(例如, 浮点寄存器就无须每次切换), 从而有更大灵活并节省切换的开销.
3.1.1 上下文切换步骤
图5-5描述了VT-x支持的CPU上下文切换过程, 下列几步.
⓵ VMM保存自己的上下文, 主要是保存VMCS不保存的寄存器!!!, 即宿主机状态域以外的部分. VMM软件切换.
⓶ VMM将保存在VCPU中的由软件切换的上下文加载到物理CPU中. VMM软件切换
⓷ VMM执行VMRESUME/VMLAUNCH指令, 触发VM-Entry, 此时CPU自动将VCPU上下文中VMCS部分加载到物理CPU, CPU切换到非根模式. CPU硬件自动完成.
注: 图中不正确, 因为宿主机状态域只需要在VM-Exit时恢复使用, VM-Entry不使用
此时, 物理CPU已经处于客户机的运行环境了, rip\eip也指向了客户机的指令, 这样VCPU就被成功调度并运行了.
3.1.2 惰性保存/恢复
上下文频繁切换会带来不小开销, 因此优化很有必要.
和OS一样, VMM也使用"惰性保存/恢复(Lazy Save/Restore)"的方法进行优化, 基本思想是尽量将寄存器的保存/恢复延迟到最后一刻, 即其它VCPU或VMM需要用该寄存器的时候再保存/恢复.
具体来说, VMM通过考察资源的使用情况来实现"惰性保存/恢复".
⓵ 对于VMM需要使用的寄存器, 每次VCPU和VMM切换时都要保存/恢复.
⓶ 对于VMM没有使用!!!的寄存器, 如果VMM无法知道VCPU是否在最近的执行中曾经修改了这个寄存器(如扩展通用寄存器CR6), 那么
- 在VCPU和VMM切换时, 不需要对这个寄存器进行保存和恢复.
- 当VMM进行不同的VCPU切换时, 例如使一个VCPU睡眠并调度另一个VCPU运行, 需要每次都保存和恢复这个寄存器.
⓷ 对于VMM没有使用的寄存器, 如果VMM可以知道客户机是否在最近执行中修改了这个寄存器(例如浮点寄存器), 还可以进一步优化. 不仅在VCPU和VMM切换时, 不需要对这个寄存器进行恢复和保存, 即使切换不同的VCPU, 也不需要每次都保存/恢复, 而是根据需要进行.
例如, 如图5-6, VCPU1、VCPU2和VCPU3按顺序调度到物理CPU上执行, 即VCPU1先执行, 其次VCPU2, 最后VCPU3. 其中, VCPU1和VCPU3在执行中会使用浮点寄存器, 而VCPU2不用. VMM了解到后,
- 在从VCPU1调度到VCPU2时, 只需保存VCPU1的浮点寄存器而无需加载VCPU2的;
- 从VCPU2调度到VCPU3时, 只需加载VCPU3的浮点寄存器而无需保存VCPU2的.
这就将原来两次保存/加载的工作减少为1次(保存VCPU1半次, 加载VCPU3半次).
3.2 VCPU的硬件优化
优化目的, 尽可能减少在客户机和VMM之间切换, 从而减少上下文切换开销.
Intel VT-x优化方法可分为两种.
⓵ 无条件优化: 指以往在软件虚拟化下必须陷入到VMM的敏感指令, 通过Intel VT-x已可以在客户机中直接执行. 如后面看到的CR2访问、SYSENTER/SYSEXIT指令.
⓶ 条件优化: 指通过VMCS的VM-Execution控制域, 可配置某些敏感指令是否产生VM-Exit而陷入到VMM中. 如CR0、TSC的访问.
下面举例说明Intel VT-x带来的优化技术.
3.2.1 读写CR0
CR0是一个控制寄存器, 控制寄存器的状态, 如启动保护模式、打开分页机制。
操作CR0的指令有MOV TO CR0、MOV FROM CR0、CLTS和LMSW, 这些指令必须在 特权级0!!! 执行, 否则产生保护异常.
纯软件虚拟机中, 客户OS是特权级1、特权级2上执行CR0读写指令, 因此所有指令都产生保护异常, 然后VMM模拟操作CR0指令的执行.
硬件辅助虚拟机中, 虽然CR0访问同样需要VMM模拟处理!!!, 但VT-x提供了加速方法, 能减少因访问CR0所引起的VM-Exit的次数.
-
首先, 读CR0. VMCS的"VM-Execution控制域"的CR0 read shadow字段用来加速客户机读CR0的指令. 每次客户机试图写CR0!!!时, 该字段都会自动得到更新(会产生VM-Exit!!!), 保存客户机要写的值. 这样, 客户机所有读CR0的指令都不用产生VM-Exit!!!, CPU只需返回CR0 read shadow的值即可.
-
其次, 写CR0. VMCS的"VM-Execution控制域"的CR0 guest/host Mask字段提供了客户机写CR0指令的加速. 该字段每一位和CR0的每一位对应, 表示CR0对应的位是否可以被客户软件修改.
- 若为0, 表示CR0中对应位可被客户软件修改!!!, 不产生VM-Exit;
- 若为1, 表示不能, 如果客户软件修改该位, 产生VM-Exit.
读CR0. 通过VM-Execution控制域的CR0 read shadow字段加速, VMCS的客户机状态域CR0, 为什么不使用呢??
写CR0. 通过VM-Execution控制域的相应字段控制是否让客户OS修改还是VM-Exit.
但, 读写CR0只能在特权级0, 所以非根模式的客户机非特权级0时是否VM-Exit由VMCS的VM-Execution控制域的Exception bitmap决定.
同样机制被用于加速CR4的访问. 该优化属于条件优化.
3.2.2 读取TSC
纯软件虚拟机中, 读取TSC可以在任何特权级中执行, VMM必须想办法截获TSC读取指令.
在硬件辅助的虚拟机中, 当"VM-Execution控制域"中RDTSC exiting字段为1, 客户软件执行RDTSC产生VM-Exit, 由VMM模拟该指令. 客户读取TSC非常频繁, 为提高效率, VT-x提供下面的硬件加速. 当"VM-Execution控制域"中RDTSC exiting字段为1并且Use TSC offset为1, 硬件加速有效.
- VMCS(每个VMCS一个!!!) 中 TSC偏移量 表示该VMCS所代表的虚拟CPU TSC相对于物理CPU TSC的偏移, 即虚拟TSC=物理TSC+TSC偏移量. 当客户软件执行RDTSC时, 处理器直接返回虚拟TSC, 不产生VM-Exit. 这样, 对TSC的虚拟化只需在 适时更新VMCS中TSC偏移量!!! 即可, 不需要每次TSC访问都产生VM-Exit.
VMCS存在TSC偏移量.
这属于条件优化.
3.2.3 GDTR/LDTR/IDTR/TR的访问
必须在0特权级
纯软件虚拟机中, 客户OS是特权级1、特权级2上, 执行LGDT、LIDT、LLDT和LTR指令, 会产生保护异常, 需要VMM模拟这些指令的执行. 模拟过程对不同情况, 有很多复杂的处理. 例如,
- 客户机OS在GDT中, 为自身内核段设置的描述符的DPL是0. 由于它自身运行在非特权级0上, 所以VMM要通过截获LGDT指令, 对GDT描述符修改.
- 同时, 像SGDT这样的指令可以在任何特权级下执行, 客户OS中的程序只需要读取GDT并判断描述符的DPL就知道自身运行在虚拟环境, 这也是一个虚拟化漏洞!!!.
硬件虚拟机中. 使用Intel VT-x技术, VMCS(!!!) 为客户机和VMM都提供了一套GDTR、IDTR、LDTR和TR, 分别保存在客户机状态域和宿主机状态域中(宿主机状态域没有LDTR, 因为VMM不需要使用), 由硬件转换.
- 而客户机运行在非根的特权级0, 所以无需对GDT表等做任何修改, 客户机执行LGDT等指令也无需VM-Exit.
VMCS中的宿主机状态域和客户机状态域中存在.
该优化属于无条件优化.
3.2.4 读CR2
发生缺页异常时, CR2保存产生缺页错误的虚拟地址. 缺页错误处理程序通常会读取CR2获得产生该错误的虚拟地址. 缺页错误是一个发生频率较高的异常, 所以读取CR2是一个高频操作. 读取CR2必须在 特权级0!!! 上执行, 否则产生保护错误.
纯软件虚拟机中, 客户OS在特权级1、特权级2上执行读取CR2指令, 产生保护错误, 需要VMM模拟该指令.
Intel VT-x中, VM-Entry/VM-Exit时会切换CR2.
-
并且, 客户OS是在非根模式的特权级0执行读取CR2指令, 不产生保护错误, 故无需VMM模拟该指令.
-
此外, 如果客户机在特权级0以外级别执行读CR2指令, 会产生保护错误, 该错误是否引发VM-Exit由Exception bitmap控制.
因为VMCS中没有CR2!!!
page_fault的处理程序会使用,
该优化属于无条件优化.
3.2.5 SYSENTER/SYSEXIT
早期系统调用通过INT指令和IRET指令实现. 当前主流的IA32e CPU中, Intel推出了经过优化的SYSENTER/SYSEXIT指令以提高效率. 现代OS倾向使用SYSENTER/SYSEXIT指令.
正常情况下, SYSENTER指令要求跳转的目标代码段!!!运行在特权级0, 否则产生保护错误.
在软件虚拟机中, 客户OS运行在特权级1、特权级2, 当客户应用执行SYSENTER会产生保护错误, 需要由VMM模拟SYSENTER指令. SYSEXIT指令必须在 特权级0!!! 执行, 否则产生保护错误. 和SYSENTER一样, SYSEXIT在软件虚拟化技术中必须由VMM模拟.
Intel VT-x中, 客户OS运行在非根模式的特权级0, SYSENTER/SYSEXIT都不会引起VM-EXIT, 即客户OS的系统调用无需VMM干预而直接执行. 该优化属于无条件优化.
3.2.6 APIC访问控制
现代主流的支持SMP的OS来说, LAPIC在中断的递交中扮演很重要角色. LAPIC有很多寄存器, 通常OS以MMIO方式访问. 其中, OS使用TPR(Task Priority Register)来屏蔽中断优先级小于或等于TPR的外部中断.
通过虚拟化客户机的MMU, 当客户机试图访问LAPIC(!!!MMIO!!!)时, 发生一个缺页异常类型的VM-Exit, 从而被VMM拦截到. VMM经过分析, 知道客户机正试图访问LAPIC后, 会模拟客户机对LAPIC的访问.
通常, 对于客户机的每个虚拟CPU, VMM都会分配一个虚拟APIC结构与之对应!!!, 客户机的MMIO操作不会真正影响物理的LAPIC, 而只反映到相应的虚拟LAPIC结构!!!里面. VMM这种模拟开销很大, 如果客户机的每个LAPIC访问都导致一次缺页异常类型的VM-Exit并由VMM模拟, 会严重影响客户机的性能.
VT-x提供了硬件加速.
-
可设置VMCS的Use TPR shadow=1, Virtualize APIC accesses=1, 设置Virtual APIC page为虚拟LAPIC结构的地址, 同时修改VCPU页表, 使客户机访问LAPIC时不发生Page Fault(需要相应设置VMCS中的Virtual-APIC address寄存器).
-
同时, 对于那些暂时不能注入客户机的中断(如果有), 还需要挑出优先级最高的那个(即向量号最大的那个), 将其优先级填入VMCS中的TPR threshold寄存器.
这样设置后,
-
对于除了TPR以外的LAPIC寄存器的访问, 客户机会直接发生APIC Access类型的VM-Exit. 此时, CPU可告知VMM客户机正试图访问哪个LAPIC, 这可降低VMM对客户机此次访问的模式开销;
-
而客户机对TPR的读操作则可以直接从虚拟LAPIC结构中的相应偏移处读取而无须发生任何VM-Exit.
-
最后, 客户机对TPR的写操作只在必要时候(客户机把TPR减少到比TPR threshold还要小的时候)才发生TPR-Below-Threshold类型的VM-Exit, 这种情况下VMM可检测是否有虚拟中断可以注入到客户机.
上面的TPR寄存器是用MMIO方式访问, 对于64位的x86, 专门有一个特别的系统控制寄存器CR8被映射到TPR(读写CR8等效于读写TPR). 64位客户机通常通过CR8寄存器来访问TPR. 当客户机试图访问CR8时, 会发生一个Control-Register-Accesses类型的VM-Exit. 为更快模拟客户机对CR8的访问, 除了上面设置外, 可以设置VMCS中的CR8-load exiting=0和CR8-store exiting=0. 这样,
- 客户机读CR8时, CPU可以从虚拟LAPIC结构中相应的偏移处直接返回正确值而不产生任何VM-Exit;
- 当客户机写CR8时, 只在必要时候才发生TPR-Below-Threshold类型的VM-Exit.
3.2.7 异常控制
在软件虚拟化技术中, 客户机产生异常都会被VMM截获, 由VMM决定如何处理, 通常是注入给客户OS.
Intel VT-x中, 可用Exception bitmap配置哪些异常需要由VMM截获. 对于不需要VMM截获的, 可将Exception bitmap中对应的位置1, 则异常发生时直接由客户OS处理. 属于条件优化.
3.2.8 I/O控制
软件虚拟化中, VMM需要截获I/O指令来实现I/O虚拟化. 但由于I/O指令通过设置可在特权级3执行, 截获I/O指令需要额外处理.
Intel VT-x中, 可通过VMCS的Unconditional I/O exiting、Use I/O bitmaps、I/O bitmap进行配置, 选择性让I/O访问产生VM-Exit而陷入VMM中. 这样, 对于不需要模拟的I/O端口, 可让客户机直接访问. 属于条件优化.
3.2.9 MSR位图
Intel VT-x和I/O控制一样, 可通过use MSR bitmaps、MSR bitmap来控制对MSR的访问是否触发VM-Exit. 属于条件优化.
3.3 总结
物理机情况下, 指令的执行都是有自己对应特权级的特权行为, 特权级别不匹配的话可能会异常, 可能无事发生.
软件虚拟化情况下,
在硬件辅助虚拟化中, 虚拟机也有一套完整的特权级. 一个指令开始执行, 这时候取决于VMX非根模式下的行为定义.
一般而言, 硬件虚拟化时一个指令的动作往往和物理情况下的动作有类似之处.
-
对于权限符合的(例如0级指令在非根0级
). 有些会有硬件无条件优化(不会导致VM-Exit, 往往借助VMCS), 有些可以条件优化(可选择触发VM-Exit还是直接修改物理寄存器, 往往通过VMCS相关字段可配置), 有些没有优化(类似于软件模拟, 会导致VM-Exit再由VMM模拟) -
对于权限不符合(例如0级指令在非根3级). 物理环境会发生异常/无事发生, 在硬件虚拟化中, 有些可能会直接发生VM-Exit, 有些无事发生, 有些又可以控制是否VM-Exit(比如通过VMCS的VM-Execution控制域的Exception bitmap字段控制产生VM-Exit还是直接客户OS处理)
对于IO端口也是类似
4 VCPU的退出
执行特权指令, 发生物理中断等. 这种退出表现为VM-Exit.
对于VCPU退出的处理是VMM进行CPU虚拟化的核心, 例如模拟各种特权指令.
4.1 VCPU退出步骤
图5-7描述了VMM处理VCPU退出的典型流程, 可归纳为下面几步.
⓵ 发生VM-Exit, CPU自动进行一部分上下文的切换. 见6.2.7.
⓶ 当CPU切换到根模式开始执行VM-Exit的处理函数后, 进行另一部分上下文的切换工作(见6.3.3)
根据VM-Exit信息域获得发生VM-Exit的原因, 并分发到对应的处理模块处理. 例如, 原因是执行了特权指令, 则调用相应指令的模拟函数进行模拟.
4.2 VCPU退出原因
图5-7列举了典型的VCPU退出原因. 总结起来, VCPU退出的原因大体有三类
(1) 访问了特权资源, 对CR和MSR寄存器的访问属于这一类
VMM通过特权资源的虚拟化解决. 特权资源虚拟化要点在于解决客户机与VMM在特权资源控制权的矛盾. 即客户机认为自己完全拥有特权资源, 可自由读写, 而实际拥有者是VMM, 不能允许客户机自由读写.
VMM通过引入"虚拟特权资源"和"影子特权资源"来解决.
- "虚拟特权资源"是客户机所看到的特权资源, VMM允许客户机自由读写.
- "影子特权资源"是客户机运行时特权资源真正的值, 通常是VMM在"虚拟特权资源"基础上经过处理得到的, 因此称为"影子".
图5-8以特权寄存器为例, 展示特权寄存器的虚拟化过程.
当VCPU读特权寄存器!!!时, VMM将"虚拟寄存器!!!"的值返回. 例如, 对于MOV EAX, CR0指令, VMM将Virtual CR0的值赋给EAX, 然后VM-Entry返回!!!.
当VCPU写特权寄存器时, VMM首先将值写入"虚拟寄存器", 然后根据"虚拟寄存器"的值以及虚拟化策略来更新"影子寄存器", 最后将"影子寄存器"的值应用到VCPU上, 将值写入VMCS"客户机状态域"的对应字段并且VM-Entry返回. 这里的虚拟化策略因特权虚拟器而异, 例如对于下面指令:
MOV EAX, 0x00000001
MOV CR0, EAX
假设原来Virtual CR0=0x80000001, VMM比较后会发现客户机试图将CR0的第31位(CR0.PG: 页模式)清掉, 即关掉CPU的页模式. 为实现内存的隔离, VT-x是不允许客户机的页模式关掉的!!!. 因此, VMM会将Virtual CR0按客户机要求设置为0x00000001, 但影子CR0依然设置为0x80000001(相应VMCS中的Guest CR0字段也会被设置). 此外, VMM会通知内存虚拟化模块有关客户机页模式的变化, 内存虚拟化模块做相应处理, 比如不再使用客户机的页表等.
(2) 客户机执行的指令引发了异常, 例如缺页异常.
客户机指令导致的异常, 很多是不需要虚拟化的, 可以直接由客户操作系统处理, 例如"除0错误"、“溢出错误”和“非法指令”等异常。这些都可以通过Exception bitmap设置。
对于需要虚拟化的异常, 没有一个通用的方法, VMM会对不同异常做不同处理. 以缺页异常为例, VMM会首先分析产生错误的原因:
- 如果是因为访问MMIO地址导致的, 则可以知道客户机在做I/O操作, VMM会调用I/O虚拟化模块处理;
- 如果是软件虚拟化中提到的影子页导致的异常, VMM会调用内存虚拟化模块处理;
- 如果都不是, 那就是客户机正常的缺页异常(即不要VMM处理的缺页异常), 该异常会被注入给客户机, 由客户机OS自己处理.
(3) 发生了中断. 可分为两种情况, 一种是发生真正的物理中断; 一种是客户机的虚拟设备发生了虚拟中断, 并通过VMM提供的接口使客户机发生VM-Exit.
-
对于前者, VMM首先读取VMCS的VM-Exit interruption information字段来获取中断向量号, 然后调用VMM中对应的中断处理函数.
-
对于后者, VMM在感知到虚拟中断发生时, 会用某种方法把虚拟中断的目标VCPU拖到VMM中, 常用一个方法是发一个IPI给运行该VCPU的物理CPU. 然后, VMM在IPI的处理函数中将该虚拟中断注入给客户机, 客户机OS处理.
5 VCPU的再运行
VMM在处理完VCPU的退出后, 会负责将VCPU投入再运行. 从VT-x的角度来看, 有几点需要额外考虑.
(1) 如果VCPU继续在相同物理CPU上运行, 可以用VMRESUME实现VM-Entry. VMRESUME比VMLAUNCH更轻量, 执行效率更高. 因此, 作为优化, VMM调度程序通常会尽量将VCPU调度在同一个物理CPU上.
(2) 如果由于某种原因(如负载均衡), VCPU被调度程序迁移到另一个物理CPU上, 那么VMM需要做如下几个事情.
⓵ 将VCPU对应的VMCS迁移到另一个物理CPU(见6.2.2节), 这通常可以由一个IPI中断实现.
⓶ 迁移完成后, 在重新绑定的物理CPU上执行VMLAUNCH发起VM-Entery.
此外, 在上一节看到, 某些异常和中断是需要注入给客户机, 这也是在VCPU运行时进行的. 通过"事件注入机制", 可很容易让VCPU在运行后直接进入到相应的中断/异常处理函数中执行. 整个虚拟化内容就是在VMM→客户机→VMM→∙∙∙中完成. 这里再细化下, 就是在VCPU运行→VCPU退出→VCPU再运行→∙∙∙的过程中完成的.
6 进阶
6.1 CPU模式的虚拟化
在一个物理机器上, 可能同时运行一个实模式的虚拟机、一个保护模式的虚拟机和一个IA-32e模式的虚拟机, 以及其他组合. 为此, VMM必须有模拟各种CPU运行模式的能力.
然而, 由于客户机物理地址空间和机器真实的物理地址空间并不相同, 且客户机物理地址空间占用的真实物理页面通常不连续, 因此, 目前VT-x要求物理CPU处于非根模式时, 分页机制必须是开启的, 而不考虑客户机当前运行模式. 即, 客户机运行在实模式, 其所在物理CPU的分页机制也是开启的. 由于实模式使用的内存访问模式和保护模式不同, VMM需要大量工作对客户机的实模式进行模拟.
客户机看到的CPU模式实际是VCPU中设置的模式, 即VCPU结构中CR0寄存器值反映的模式, 该模式和物理CPU的真正模式可能不同, 所以CPU模式的虚拟化包括两个方面.
⓵ 对标志CPU模式的控制寄存器(如CR0的PE/PG位)的虚拟化.
⓶ 对CPU运行环境的虚拟化, 如指令的操作数长度等.
当客户机运行在实模式, 由于物理CPU运行在保护模式, 两者在指令、运行环境都不同, 所以VMM对客户机的指令进行模拟执行.
当客户机运行在分页开启的保护模式, 和物理CPU的运行模式一样, 此时不需要就模式的虚拟化做额外的工作.
表5-7总结了客户机运行模式和物理CPU的实际模式之间的对应关系, 并总结模拟客户机运行模式的方法.
6.2 多处理器虚拟机
现在多数计算机具备多个CPU. 可将客户机配置多个CPU的虚拟平台, 提高客户机的计算能力. 多CPU的虚拟平台, 本质就是给客户机分配多个VCPU, 并通过调度器让它们共享一个物理CPU分时执行或分散到多个物理CPU同时执行. 这和OS中的多线程任务采用同样思想.
与单VCPU客户机相比, 多VCPU客户机实现上有几点需要注意.
⓵ 多VCPU发现的问题. 在物理机器上, OS需要知道平台所有CPU的信息, 同样, 需要让客户OS知道它所拥有的VCPU的信息, 例如VCPU个数、每个VCPU的ID号等. 这些信息在物理平台是通过BIOS提供, 虚拟环境, 客户机的虚拟BIOS负责.
⓶ 多个VCPU初始化问题. 物理平台, 多处理器初始化通常遵循一个规范, 例如IA32手册的7.5节MULTIPLE-PROCESSOR(MP) INITIALIZATION就定义了IA32平台上多处理器的初始化流程. 硬件通常会选择一个CPU作为主CPU, 称为BSP(Boot Strap Processor), 来执行BIOS, 其它从CPU, 称为AP(Application Processor), 处于Wait-for-SIPI状态, 等待BSP发SIPI(Start-up IPI)唤醒它们之后再开始执行. 客户机同样遵循这个规范, 包括挑选一个VCPU作为BSP执行BIOS, 将其它VCPU置于Wait-for-SIPI状态等待BSP发SIPI, 只是这个挑选工作是由VMM完成.
⓷ VCPU的同步问题. 物理平台, 各CPU同时在运行. 但多个VCPU的客户机, 同一时刻, 可能一部分VCPU正在运行, 一部分则处于睡眠或阻塞状态. 这对于VCPU间通信和同步都造成一定延时问题. 例如, 当客户机的两个VCPU都运行着竞争自旋锁的代码时, 考虑下面这段两个VCPU通过自旋锁进行同步的代码:
acquire_splinlock(lock)
critical p
release_splinlock(lock)
当VCPU0上运行的代码通过acquire_splinlock(lock)取得自旋锁, 进入临界区后, VCPU0可能会被调度出去. 其后, 当另一段运行在VCPU1上代码尝试获取锁, 它因为得不到而自旋等待, 知道VCPU0被重新调度执行, 完成临界区并执行release_splinlock(lock)释放自旋锁. 这样, VCPU1因为VCPU0被调度出去的缘故, 增加了同步的时延.
为解决这个问题, 一种解决方案是对一个多VCPU客户机的多个VCPU进行群体调度(Gang Scheduling), 即它们要么同时在多物理CPU上同时运行, 要么同时不运行. 群体调度带来的一个限制使一个多VCPU客户机的VCPU个数不可以超过物理平台的物理CPU个数.