Skip to content

6. 中断虚拟化

equation314 edited this page Sep 16, 2020 · 3 revisions

VMX 的中断处理方式

在 x86 架构中,中断其实包含了中断(interrupt)和异常(exception)两类。两者最主要的区别是中断发生的时机是不确定的,而异常发生在执行一条特定指令时。

对于异常,发生的原因与那条被执行的指令有关,如除零异常、缺页异常等等。因此,在虚拟化环境中,Guest 模式下执行指令时发生的异常自然地由 Guest 处理;Host 模式下执行指令时发生的异常自然地由 Host 处理。这一过程不涉及 Guest 与 hypervisor 之间的交互,我们一般不需要特别考虑对异常的虚拟化。

对于中断,发生的原因一般与 CPU 之外的硬件有关,也被称为外部中断(external interrupts)。我们无法区分中断是发给 Guest 还是 Host 的,因此,与 I/O 虚拟化一样,根据由谁来处理中断,中断虚拟化可以分为:

  1. 直连:中断直接发给 Guest,通过 Guest 的 IDT (interrupt descriptor table) 进行处理。这种方法的特点是开销小,但不支持多个 Guest,而且 hypervisor 会在 Guest 运行时收不到任何中断。
  2. 模拟:所有外部中断都会触发 VM Exit,由 hypervisor 来决定该中断的去向,是交给 Host 的中断处理例程,还是向 Guest 中注入一个虚拟中断。这种方法的特点是非常灵活,支持多个 Guest,但实现复杂、开销大。

可以在 VMCS Pin-Based VM-Execution Controls 中设置 “External-interrupt exiting” 字段是否为 1 来分别支持这两种配置。

中断又可被进一步细分为可屏蔽中断、不可屏蔽中断(non-maskable interrupt, NMI)、软件中断(software-generated interrupts)等。RVM 没有专门实现对 NMI 的处理;而中软件中断与异常类似,是由 INT n 指令产生的,不用特别处理;因此我们只讨论最基本的外部中断。

外部中断捕获

RVM 目前使用的是中断模拟的方式,所有外部中断都会导致 VM Exit,然后交给 hypervisor 来进一步处理。hypervisor 可以从 VMCS 中获取中断向量号等信息,并根据中断向量号跳转到相应的中断处理例程。原来从中断发生到进入中断处理例程的过程都由硬件完成,现在需要由软件来模拟这些操作:

  1. 压栈。要压栈的内容包括 SS、RSP、RFLAGS、CS、RIP、错误代码、中断向量。
  2. 跳转到 Host 的中断处理例程,即在 Host IDT 中设置的地址。
  3. 对于特定的中断,向 Guest 中注入相应的虚拟中断。

虚拟中断注入

虚拟中断注入发生在下面两种情况:

  1. 发生了一个外部中断,hypervisor 向 Guest 注入相应的虚拟中断;
  2. 在用户态实现的虚拟设备要向 Guest 注入虚拟中断。

可以在 VMCS VM-Entry Interruption-Information Field 设置要注入的虚拟中断信息,如中断向量号、中断类型、错误代码等。对于上述第一种情况,直接写 VMCS 进行注入即可;对于第二种情况,用户态不能访问 VMCS,需要通过系统调用接口,即使用 RVM_VCPU_INTERRUPT API。

RVM 中的实现

下面是 RVM 实现中断虚拟化的一些细节。

中断处理例程地址

中断处理例程的地址是由 Host OS 设置的,RVM 并不能直接获取。这里也像内存虚拟化中“Host 物理内存分配”一样,通过 Rust FFI 导出,由 Host OS 提供实现。例如 zCore 中需要进行以下设置:

#[cfg(target_arch = "x86_64")]
#[rvm::extern_fn(x86_all_traps_handler_addr)]
unsafe fn rvm_x86_all_traps_handler_addr() -> usize {
    extern "C" {
        fn __alltraps();
    }
    __alltraps as usize
}

相关代码:

InterruptController

同一时刻可能有不止一个要注入的中断,RVM 中有个结构来管理当前所有要向 Guest 注入的中断:

pub struct InterruptController {
    max_num: usize,
    bitset: BitSet,
}

其中 max_num 是最大支持的中断向量号,bitset 模拟了中断的优先级,支持快速插入、删除、和找到当前优先级最高的中断。

根据 Intel SDM Volume 3, Section 6.9, Table 6-2 和 RVM 所支持的中断类型,我们将 x86 下中断的优先级简化为:NMI(中断向量号 2) > 可屏蔽硬件中断(中断向量号 32-255) > 异常(中断向量号 0-31 中的某些)。RVM 先判断是否有 NMI,如果没有则从 bitset 中找出中断向量号最大的中断。

实际的中断注入发生在每次进入 Guest 之前,并且一次只注入一个中断,即在 InterruptController 中优先级最高的中断。目前没有提供设置中断屏蔽的接口,Guest 无法设置屏蔽特定的中断,因此无法完全模拟类似 i8259A 可编程中断控制器(PIC)这样的设备。

相关代码:

Interrupt-window exiting

如果 Guest 设置了阻塞中断(通过设置 RFLAGS.IF = 0 等方式),就会收不到由 hypervisor 注入的虚拟中断,导致该中断丢失。

为了解决这一问题,VMCS Processor-Based VM-Execution Controls 中可以设置 “Interrupt-window exiting” 字段。当该字段为 1 时,一旦 Guest 取消了中断屏蔽(设置 RFLAGS.IF = 1),就会发生原因为 Interrupt window 的 VM Exit。

在 RVM 注入虚拟中断时,如果发现 Guest 此时禁用了中断,就不进行注入,也不从 InterruptController 中弹出要注入的中断,而是设置 “Interrupt-window exiting” 为 1。当 Guest 在之后一启用中断,就会发生 VM Exit。此时再把 “Interrupt-window exiting” 恢复为 0,就可以在再次进入 Guest 前重新注入了。

相关代码:

虚拟时钟

有了中断之后就能使用时钟设备了。不过与之前 I/O 虚拟化中的不一样,时钟虽然也是一个设备,不过不是在用户态模拟的,而是在内核态中模拟。这是因为时钟对性能的要求比较高,如果也放到用户态会大大增加用户—内核的切换开销,导致时钟精度下降。另外在内核态实现会产生中断的设备也比较简单。

RVM 中模拟的是 i8253 PIT 时钟(Programmable Interval Timer),可以使用 PIO 对其进行操作(端口号是 0x40),并支持 Guest 设置时钟频率。每次遇到真实时钟中断时都会更新其计数器,当计数器达到设定值后就会向 Guest 注入一个虚拟时钟中断。

相关代码: