最近由于项目需要学习虚拟化的知识,于是在师兄的建议下读了 sjtu 出的新书《操作系统原理与实践》中的系统虚拟化的部分,随便记点笔记。

虚拟化发展历史:

  • 1960s出现分时操作系统概念。同时期IBM进行了另一个方向也就是虚拟化的探索,并在1968年的system360上实现了第一个VMM(Virtual Machine Monitor) CP/CMS
  • 1980s,开始出现纯软件实现的虚拟机监视器
  • 1990s, 互联网兴起,web服务器逐渐性能过剩
  • 1998年,VMware成立。
  • 云计算兴起,虚拟化越发重要

优势:

  • 充分利用服务器性能,搞超售
  • 虚拟机管理更为便捷,可以快速部署销毁
  • 可以热迁移
  • VMI (Virtual Machine Introspection 虚拟机自省)可以从外部检查虚拟机是否被入侵

系统虚拟化概述

组成部分

  • CPU 虚拟化(vCPU):若虚拟化的指令集和物理架构指令集相同,除了部分特殊指令其他都可以直接执行,效率较高(KVM);若架构不同,则需要全部转译执行。
  • 内存虚拟化:引入虚拟物理地址,内存访问需要多经过一层虚拟物理地址到物理地址的映射操作。
  • I/O 虚拟化:VMM 提供虚拟驱动供虚拟机使用,并将操作转化为实际的物理设备访问或其他操作。

VMM 类型

  • 半虚拟化: 运行在最高权限级别,相当于一个以OS作为进程的操作系统,如 Xen。
  • 完全虚拟化:作为一个进程运行在 OS 上,复用宿主OS的线程调度和资源管理,如 QEMU。

Trap-Emulate(下陷-模拟)

Trap:将系统级ISA拦截转由 VMM 进行模拟操作。如系统调用先转化为虚拟机内核态,然后由 VMM 拦截并完成对应调用的模拟。

Emulate:用软件模拟执行后的副作用

基础概念:

  • CPU 上下文:指CPU在执行特定进程或任务时所需的信息集合。一般包括该CPU的所有寄存器(包括PC)的值和当前系统状态(内存信息,调度优先级等)。
  • 中断向量表(Interrupt Vector Table):存放中断处理函数。
  • 中断处理过程:触发中断时,首先检查中断是否开启。若开启,CPU 接收中断信号并暂停当前程序处理。然后保存当前上下文以供恢复。然后确定中断类型并从中断向量表中查找对应处理函数的地址并跳转执行。执行结束后,恢复中断前的上下文。
  • 用户态:用户态是普通应用程序运行的模式。该模式下,程序对硬件的访问受限,一般通过OS提供系统调用切换到内核态来执行对应函数进行访问。
  • 内核态:OS 一般运行在该状态下。该状态下,OS 能够完全访问并操作硬件,访问所有内存。

Trap-Emulate 虚拟化实现

VMM 需提供数据结构,来存储原本存储在物理 CPU 上的所有进程上下文信息,以及对应的其它信息(如中断向量表,虚拟页表等)。

  • 处理中断:对于硬件中断,将会触发 VMM 查看虚拟机是否开启对应中断。如开启,虚拟机保存上下文后下陷到 VMM,然后 VMM 检查中断向量表处理中断。
  • 处理系统调用:同中断处理的模式,只是 VMM 需要隔离用户态和内核态的页表映射
  • 处理线程切换:本质软件中断到内核态然后进行线程调度,切换进程上下文即可。
  • 多 CPU 模拟:加个数据结构存进程上下文和 vCPU 的对应关系就行。vCPU 调度参考 OS 的进程调度,或者直接用线程 OS 进程来实现。

CPU虚拟化

特权指令:在用户态执行时会下陷进入特权级的指令

敏感指令:管理物理硬件资源或更改CPU状态的指令

eg:

  • x86 修改 CR 寄存器的值
  • 读写敏感内存
  • I/O 指令

可虚拟化和不可虚拟化架构

所有满足敏感指令都是特权指令的架构称为可虚拟化架构,反之则为不可虚拟化架构。

eg:

  • AArch32
  • 早期 x86

弥补不可虚拟化架构的方法

全虚拟化

适用于无需修改客户端源码的情况

  • 解释执行:用纯软件模拟cpu执行指令过程。具体操作是对于每条指令调用对应的用于模拟的函数,整个过程不产生任何下陷。优点是可以模拟任何 ISA 类型的虚拟机。缺点显而易见,效率低下。
  • 动态二进制编译:在解释执行的基础上将程序划分为只有一个入口和一个出口,中间无任何修改控制流指令的代码块(相当于创建了一个函数)。第一次执行该块时进行翻译并缓存,之后再执行时调用之前的缓存。翻译替换所有敏感指令,在块的末尾添加一条跳转指令来通知 VMM 执行完毕。
  • 扫描-翻译:用于虚拟机架构和宿主相同的情况,在程序执行前扫描可能存在敏感指令的代码,翻译并缓存翻译后的代码以便下次复用。大多数情况敏感指令只存在于 OS kernal 中,可以只扫描内核代码。

半虚拟化

在允许修改虚拟机客户端代码的情况下可以使用

原理:通过 VMM 提供的类似系统调用的服务使得虚拟机内核不再需要下陷模拟,而是类似进程使用系统调用一样进行对应操作,效率更高。半虚拟化使得 VMM 获得查看客户端内存分布的能力,能够更合理的分配资源。

硬件虚拟化

通过 CPU 硬件支持来实现更高效的虚拟化。具体来说就是增加一个专为 VMM 运行的特权级,该特权级下 VMM 拥有和宿主 OS 相同的硬件访问权限,来节省下陷的开销。

拓展——KVM (kernal-based virtual machine) 技术: 通过将 VMM 作为内核模块加载使得 VMM 能够使用 宿主 OS 内核的功能,这样下陷时某些支持的架构可以消除 KVM 到 OS 内核特权级转换带来的开销。指令无需翻译,直接在硬件上执行,使得虚拟化效率接近直接运行。

内存虚拟化

目标:实现虚拟机之间,虚拟机和物理机之间的内存隔离
术语:

  • GVA: Guest Virtual Address
  • GPA: Guest Physical Address
  • HPA: Host Physical Address

影子页表(Shadow Page Table)

复习进程页表的配置&使用:

  1. 调用前 OS 为进程配置一个虚拟地址映射到物理地址的静态页表(相对地址)
  2. OS 将页表基地址写入对应寄存器以让 MMU 能找到页表
  3. MMU 解析虚拟地址到物理地址

内存虚拟化中,VMM 需要根据存储的 GPA 到 HPA 的映射信息并生成一个 GVA 到 HPA 的影子页表。在虚拟机下陷时,将页表基地址替换为影子页表基地址,从而让 MMU 直接解析 GVA。

VMM 需要监视虚拟机对页表的更改,并对应地修改影子页表。同时为了分隔用户态和内核态,需要分离对应的页表,使得用户态页表中不包含内核态的映射

缺页处理:

  • 若为页表项不存在或无权限,VMM 需触发宿主 OS 的缺页中断进行处理
  • 若为访问权限足够,则需要同步影子页表与虚拟机页表

直接页表映射

客户端与宿主共用页表。该情况下客户端知道自己处在虚拟环境中,VMM 会告知客户端可以使用的页表范围,方便客户端规划。客户端的页表项被设为只读,修改页表需要使用 VMM 提供的超级调用,该调用会检查对页表的修改是否合法。

两阶段地址翻译

硬件虚拟化的一部分,需要 CPU 支持使用第二页表将 GPA 转换到 HPA,省去 VMM 手动维护影子页表的步骤。同时可以使用 TLB 来优化解析速度。

缺页中断处理:

  • 客户虚拟机缺页:无需下陷,硬件直接调用虚拟机注册的中断函数
  • 第二阶段页表缺页:下陷,硬件直接调用 VMM 注册的的对应中断函数

优点:

  • 不用维护影子页表
  • 不用为每个进程维护一个页表
  • 缺页处理更快

换页和内存气球

目的:运行时动态调整虚拟机内存大小,内存超售

换页逻辑

  • 保存将要交换的页的 GVA 和 GPA
  • 交换页内数据到持久化存储设备(硬盘什么的)
  • 将客户端页表项设为 INVALID
  • VMM 重新分配该页

内存气球(Memory Ballooning):在客户端插入一个驱动(内鬼),该驱动根据 VMM 的要求使用虚拟机的接口申请/释放内存,然后将申请的内存物理地址告诉 VMM 来使用。

I/O虚拟化

  • 限制虚拟机对物理硬件的直接访问
  • 提供虚拟设备接口
  • 充分利用I/O

软件模拟(全虚拟化)

捕获客户端对虚拟硬件的 MMIO, DMA, 中断,下陷后由 VMM 执行对应操作

半虚拟化

类似全虚拟化,客户端不再使用原生驱动而是前端驱动,用于与 VMM 后端驱动交互。 通过内存共享传递数据,并使用批处理加速,由后端驱动完成和物理硬件的交互。

设备直通

让客户端直接管理硬件设备,此时操作对应硬件不会产生任何下陷。为防止 DMA 攻击使用 IOMMU 进行二阶段 GPA 到 HPA 的映射并检查对应权限。

SR-IOV(single root I/O virtualization):实现硬件层面 IO 虚拟化,避免单一虚拟机独占硬件。通过创建多个 VF (virtual function) 让 VMM 分配给虚拟机使用

中断虚拟化

两种中断:

  • 物理中断:由硬件产生,在非直通情况下由 VMM 处理
  • 虚拟中断:由 VMM 产生

解决开关中断下陷的问题:增加可供虚拟机修改中断而无需下陷的虚拟寄存器

直接向虚拟机发送中断:增加一个中断翻译表,用于将物理中断翻译为虚拟机对应的中断。因此无需再进行下陷。

QEMU/KVM