-
Notifications
You must be signed in to change notification settings - Fork 18
5. IO 虚拟化
对于一台虚拟的计算机,除了虚拟化最基本的 CPU 与内存外,还需要虚拟化各种外围设备(device)。这些设备种类繁多,如时钟、串口、网卡、键盘、磁盘控制器等等,不过 CPU 访问它们的基本方式都是一样的,通过一些特殊的指令,即 I/O 操作。因此设备的虚拟化也叫 I/O 虚拟化。
对于同一台计算机,会同时存在多种设备,且对于不同的计算机,即使是同种设备,也会因为厂家、型号的不同,需要使用不同的 I/O 操作进行访问。这也是 I/O 虚拟化时需要考虑的问题。
如果在 Guest 中执行一个 I/O 操作,最终的效果应该是要能够去访问真实物理设备。根据是直接访问还是间接访问,I/O 虚拟化可以分为两种:
- 直连(pass-through):Guest 可以直接访问物理设备,无需经过 hypervisor。这种方式的优点是开销很小,但缺点是不支持多个 Guest 访问相同设备,失去了隔离性。不过也可以使用 IOMMU 等技术进行支持。
- 模拟(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 操作可分为:
- 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 的访存地址来区分不同的设备,进行相应的软件模拟,从而实现设备的虚拟化。
对于不同的设备,需要不同的模拟方式。由于设备具有数量大、种类多的特点,如果把模拟不同设备的代码全部写进 hypervisor 中,会大大增加 hypervisor 以及内核的复杂性和安全隐患。因此,最好将这部分代码放入用户空间中,让用户程序实现对设备的模拟。
在 RVM 中,当发生 I/O 相关的 VM Exit 时,会先收集 I/O 操作的信息(PIO 端口号、MMIO 访存地址、I/O 数据等),然后打包进一个结构体,通过系统调用返回来转发给用户程序,让用户程序进行具体的 I/O 操作模拟。模拟完毕后,通过 RVM_VCPU_WRITE_STATE
进入内核来写入结果,然后再次执行 RVM_VCPU_RESUME
,继续 Guest 的执行。整个流程如下如图所示。
在 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 操作的类型、地址范围、key
、port
等。Guest 中有个结构 TrapMap
用于存储与查找这些 trap:
pub struct TrapMap {
io_traps: BTreeMap<usize, Trap>,
mem_traps: BTreeMap<usize, Trap>,
}
相关代码: