1. 什么是虚拟化
IBM对虚拟化给出的定义是:虚拟化能够将单台计算机中的CPU、内存、存储器等硬件资源划分为名为虚拟机 (VM) 的多个虚拟计算机。本文我们要讨论的是CPU虚拟化,根据我的理解,CPU虚拟化就是将一个物理CPU虚拟化成多个虚拟CPU (vCPU)。实现将一个物理CPU虚拟化成多个虚拟CPU (vCPU)的方法论其实就是”CPU的时分复用”。CPU时间调度的最小单位是线程。我们将从线程模型的CPU虚拟机,讲到操作系统级别的CPU虚拟化。
2. 线程模型的 CPU 虚拟化
从线程视角来看,一个物理CPU包含的范畴主要是各种CPU寄存器。所谓线程,就是正在被执行的一段代码。如果将代码反汇编,其本质就是在不断操作各种寄存器。对于一个线程来说,其所能感知到的CPU实体主要是CPU的各种寄存器,这也就是本科操作系统教科书中所说的“CPU现场”。
最典型的两个寄存器是:
1. SP(Stack Pointer):指示当前程序的栈顶位置。
2. IP(Instruction Pointer):指示当前程序正在执行的指令位置。
为了给每个线程呈现出一个独立的物理CPU,通常的方法是为每个线程定义一个所谓的“CPU上下文”,这一般通过软件实现。虽然x86架构中的TSS(Task State Segment)设计尝试从硬件上提供任务上下文保存及切换机制,但正统的Linux内核并未采用这一机制。
“CPU上下文”被称为TCB(Thread Control Block)。一个TCB中包含的CPU上下文信息,就是实现线程级别CPU虚拟化的完整闭包。如下图所示,通过CPU上下文切换实现分时复用的基本手法:
图1 [1]
1. 当内核加载某个线程到物理CPU上运行时
内核通过读取该线程的TCB,将物理CPU的上下文恢复成该线程TCB所描述的CPU现场。
2. 当某个线程不再被内核执行时
内核将当前物理CPU的现场保存到该线程的TCB中。
这种机制确保了每个线程在被调度到物理CPU上运行时,都能有一个独立且完整的运行环境,即独立的“虚拟CPU”。线程独占CPU的时间片,让它以为它是独占地连续地在CPU上运行。
线程模型的 CPU 虚拟化是建立在同一个操作系统的基础上。线程都有一个共同的内核。但是我们所说的CPU虚拟化,是操作系统级别的虚拟化。操作系统级别的虚拟化,线程会有不同的内核。
3. 操作系统级别的CPU虚拟化
下面我们举个例子来说明为什么线程模型的 CPU 虚拟化不能实现操作系统级别的虚拟化。中断向量表是由操作系统的内核来维护。X86 体系结构下通过 LIDT(Load Interrupt Descriptor Table)指令设置中断向量表基地址,操作系统内核启动时对中断向量表进行初始化,并将中断向量表的基地址通过 LIDT 指令设置到物理 CPU 的 IDTR(Interrupt Descriptor Register)中。
中断向量表的基地址是不能乱设置的。如果使用线程模型的 CPU 虚拟化来实现操作系统级别的虚拟化,那么宿主机操作系统中将运行着 N 个操作系统。每个操作系统有不同的内核,这些操作系统在内核启动时,都会设置一次宿主机操作内核中断向量表基地址,最终导致的结果是宿主机器内核的中断向量表会被搞乱,最直接的后果就是宿主机自己先挂了。像 LIDT 这种会对整个系统级别资源(硬件)产生影响的操作指令,被称为 “敏感指令”。
下面我们举个例子来说明为什么线程模型的 CPU 虚拟化不能实现操作系统级别的虚拟化。现代操作系统有内核态(Kernel Mode)和用户态(User Mode)两种不同的运行模式。用户态不能执行内核态的指令。线程运行在用户态。如果用线程模型来实现操作系统级别的虚拟化,运行在线程里的操作系统将无法执行内核态的指令。这些内核态的指令就是通常我们所说的“敏感指令”。解决“敏感指令”的运行是虚拟化技术的关键。解决“敏感指令”的运行有两种常见的手段:
常见的技术方案有:基于二进制翻译的全虚拟化技术(Full-Virtualization)、需要改造 GuestOS 的半虚拟化技术(Para-Virtualization)和 Intel VT-x 硬件辅助的虚拟化技术(Hardware-assisted virtualization)。
接下来我们来介绍一下Intel VT-x 硬件辅助的虚拟化技术(Hardware-assisted virtualization)。
图2 [2]
如图2所示,Intel VT-x硬件辅助的虚拟化技术原理如下:
● CPU 有 Root Mode 和 Non-root Mode 这 2 种运行模式。其中,HostOS(VMM)运行在 Root Mode 拥有最高执行权限,而 GustOS(VM)和 User Application 则运行在 Non-root Mode,并且这 2 种模式都支持 Ring 0~3。因此 VMM 和 Guest OS 都可以自由选择它们所期望的 CPU 运行级别。GustOS(VM)和 User Application 可以对硬件直接执行部分“敏感指令”。
● GustOS(VM)和 User Application 不能对硬件直接执行的“敏感指令”,比如HLT指令。运行在 Root Mode 上的 CPU 会自动捕获 GuestOS 发出的“敏感指令”(触发异常),然后交由 VMM 来完成翻译,处理后再使用 VMLAUNCH 或 VMRESUME 指令返回到 GuestOS。
下面解释一下为什么 GustOS(VM)和 User Application 不能对硬件直接执行HLT指令。HLT指令是停止处理器的指令。GustOS(VM) 发出HLT指令是想停止GustOS(VM)的CPU,而不是想停止实际物理CPU,所以GustOS(VM) 不能直接对硬件执行HLT指令。HLT指令会被HostOS(VMM)截获,完成翻译,处理后再使用 VMLAUNCH 或 VMRESUME 指令返回到 GustOS(VM)。
VT-x提供的GuestOS CPU 的上下文称为 VMCS(virtual machine control structure)。VT-x提供了 Root和Non-root 两种处理器模式,HostOS(VMM)运行在 Root 模式下,GuestOS(VM)运行在 Non-root 模式下。当 GuestOS(VM)执行HLT指令时,HostOS(VMM)将自动捕获这条“敏感指令”,进而引发 CPU 从 Non-root 模式退出到Root 模式,也就是从 GuestOS(VM)中退出(VMEXIT)到 HostOS(VMM)中,并且GuestOS(VM)的 VMCS 中会带有本次从 Non-root 下退出来的原因(reason),VMM 会根据这个 reason 执行相应的模拟逻辑。VMM 会将模拟逻辑的结果写入 GuestOS(VM) 所对应的 VMCS 中。等到下次该 GuestOS 再次被调度执行时,VMRESUME 指令会直接将带有模拟逻辑结果的VMCS 所刻画的 CPU 现场恢复到Non-root 模式下的 CPU 上下文中。
简单来说就是,GuestOS(VM)的“敏感指令”会被HostOS(VMM)截获,并在HostOS(VMM)中模拟“敏感指令”的运行,将“敏感指令”模拟运行的结果写入GuestOS (VM) 所对应的 VMCS 。GuestOS (VM) 根据VMCS来将GuestOS (VM)的CPU上下文更新到“敏感指令”执行后的结果。
所以CPU虚拟化最主要的性能开销就是CPU在Root和Non-root模式之间的切换。
参考文章:
[1]虚拟化技术 — 硬件辅助的虚拟化技术
[2]虚拟化科普之 CPU 篇