Skip to content

5. IO 虚拟化

equation314 edited this page Sep 16, 2020 · 3 revisions

对于一台虚拟的计算机,除了虚拟化最基本的 CPU 与内存外,还需要虚拟化各种外围设备(device)。这些设备种类繁多,如时钟、串口、网卡、键盘、磁盘控制器等等,不过 CPU 访问它们的基本方式都是一样的,通过一些特殊的指令,即 I/O 操作。因此设备的虚拟化也叫 I/O 虚拟化。

对于同一台计算机,会同时存在多种设备,且对于不同的计算机,即使是同种设备,也会因为厂家、型号的不同,需要使用不同的 I/O 操作进行访问。这也是 I/O 虚拟化时需要考虑的问题。

I/O 虚拟化的方式

如果在 Guest 中执行一个 I/O 操作,最终的效果应该是要能够去访问真实物理设备。根据是直接访问还是间接访问,I/O 虚拟化可以分为两种:

  1. 直连(pass-through):Guest 可以直接访问物理设备,无需经过 hypervisor。这种方式的优点是开销很小,但缺点是不支持多个 Guest 访问相同设备,失去了隔离性。不过也可以使用 IOMMU 等技术进行支持。
  2. 模拟(emulation):Guest 的 I/O 操作触发 VM Exit,然后由 hypervisor 来软件模拟 I/O 操作的行为。这种方法通过 hypervisor 的介入来支持多个 Guest 同时访问相同设备,也用于虚拟出物理上不存在的设备。不过缺点是软件模拟的方式实现复杂、开销大。

可以在 VMCS Processor-Based VM-Execution Controls 中设置 “Unconditional I/O exiting” 字段是否为 1 来分别支持这两种配置。VMX 不仅支持这两种配置,还支持两者结合的方式,当设置 “Use I/O bitmaps” 字段为 1 并设置好 bitmap 的地址时,只有位于该 bitmap 中的 I/O 操作会触发 VM Exit 以供软件模拟,其他 I/O 操作都是直连到硬件的。

I/O 操作类型

I/O 操作具有不同的类型。根据操作的指令不同,I/O 操作可分为:

  • PIO (Port-mapped I/O):使用专门的指令进行 I/O 操作,如 x86 的 IN/OUT 指令。
  • MMIO (Memory-mapped I/O):使用普通的内存读写指令进行 I/O 操作,一段特殊的内存区域会被映射到设备上,在嵌入式系统中很常见。

如果使用 I/O 模拟的方式,这两类 I/O 操作分别会触发以下原因的 VM Exit:

  • I/O instruction (需要在 VMCS 中配置 “Unconditional I/O exiting” 或使用 I/O bitmaps)
  • EPT violation (需要在 EPT 中取消映射相应的地址范围)

RVM 目前是对所有 I/O 操作都进行模拟,且不使用 I/O bitmaps,使得所有 PIO 指令和 MMIO 访问都会触发 VM Exit。然后根据 PIO 的端口号或 MMIO 的访存地址来区分不同的设备,进行相应的软件模拟,从而实现设备的虚拟化。

I/O 操作转发

对于不同的设备,需要不同的模拟方式。由于设备具有数量大、种类多的特点,如果把模拟不同设备的代码全部写进 hypervisor 中,会大大增加 hypervisor 以及内核的复杂性和安全隐患。因此,最好将这部分代码放入用户空间中,让用户程序实现对设备的模拟。

在 RVM 中,当发生 I/O 相关的 VM Exit 时,会先收集 I/O 操作的信息(PIO 端口号、MMIO 访存地址、I/O 数据等),然后打包进一个结构体,通过系统调用返回来转发给用户程序,让用户程序进行具体的 I/O 操作模拟。模拟完毕后,通过 RVM_VCPU_WRITE_STATE 进入内核来写入结果,然后再次执行 RVM_VCPU_RESUME,继续 Guest 的执行。整个流程如下如图所示。

I/O 操作转发

RVM 中的实现

在 RVM 中,Guest 因一段特定区域的 I/O 操作发生 VM Exit 而进入 hypervisor 被称为一个 “trap”,Trap 被分为三类:

  • GuestTrapIo:通过 I/O 指令产生的同步 I/O
  • GuestTrapMem:通过 MMIO 产生的同步 I/O
  • GuestTrapBell:通过 MMIO 产生的异步 I/O

上述“同步”和“异步”的区别在于,同步 I/O 会在发生后立即返回用户态,而异步 I/O 不会,而是将打包好的 I/O 操作存放在一个特殊区域(port),并继续 Guest 的执行,用户程序可在另一线程中读取到该 I/O 操作,从而避免用户/内核空间的切换,提高运行效率。

Trap 的具体结构如下:

pub enum TrapKind {
    GuestTrapBell = 0,
    GuestTrapMem = 1,
    GuestTrapIo = 2,
    _Invalid,
}

pub struct Trap {
    pub kind: TrapKind,
    pub addr: usize,
    pub size: usize,
    pub key: u64,
    pub port: Option<Arc<dyn RvmPort>>,
}

其中,key 用于让用户程序区分不同的 trap;port 用于处理异步 I/O,对应于 zCore 中的 Port 对象,当在发生 GuestTrapBell 类型的 I/O 操作时,向其中传入 I/O 操作信息(RvmExitPacket),用户程序可在另一线程从该 port 中读取到 I/O 操作信息。该 port 也可不必是 zCore 中的 Port 对象,只需实现 RvmPort

/// Used for sending asynchronous message
pub trait RvmPort: core::fmt::Debug + Send + Sync {
    fn send(&self, packet: RvmExitPacket) -> RvmResult;
}

/// Implementation in zCore
struct GuestPort(Weak<Port>);

impl RvmPort for GuestPort {
    fn send(&self, packet: RvmExitPacket) -> RvmResult {
        let packet: PortPacket = packet.try_into()?;
        if let Some(port) = self.0.upgrade() {
            port.push(packet);
            Ok(())
        } else {
            Err(RvmError::BadState)
        }
    }
}

在 I/O 操作转发时,I/O 操作信息会被打包为一个 RvmExitPacket,根据 trap 类型的不同其结构也不同,这是通过 union 来实现的:

pub union RvmExitPacketInnner {
    pub bell: BellPacket,
    pub io: IoPacket,
    pub mmio: MmioPacket,
}

pub struct RvmExitPacket {
    pub kind: RvmExitPacketKind,
    pub key: u64,
    pub inner: RvmExitPacketInnner,
}

用户程序通过 RVM_GUEST_SET_TRAP 向 Guest 添加一个 trap,传入的信息包括该 I/O 操作的类型、地址范围、keyport 等。Guest 中有个结构 TrapMap 用于存储与查找这些 trap:

pub struct TrapMap {
    io_traps: BTreeMap<usize, Trap>,
    mem_traps: BTreeMap<usize, Trap>,
}

相关代码:

Clone this wiki locally