多进程 QEMU
备注
这是多进程 QEMU 的设计文档,不一定反映当前实现状态。当前实现可能缺少某些功能,也可能与本文描述有很大差异。本文仍可用于了解这项功能的目标和总体方向。
最新详情请参阅以下 wiki: https://wiki.qemu.org/Features/MultiProcessQEMU
QEMU 经常用作 Oracle 云中虚拟机的虚拟机监控器。云计算的优势之一是在同一云基础设施中运行多个租户的虚拟机,因此,如果客户机攻破其虚拟机监控器,就可能利用监控器的访问权限读取未经授权的数据。
QEMU 是一个提供多种虚拟机功能的大型单体程序,因此可能遭受安全攻击。虽然可以通过配置移除许多功能,但即使精简配置的 QEMU 仍包含大量客户机可能攻击的代码。拆分 QEMU 有助于让每个组件只能访问完成自身工作所需的资源,从而减少攻击面。
QEMU 服务
概括来说,QEMU 提供三项主要服务。第一是虚拟机控制入口,用于创建、迁移、重新配置和销毁虚拟机。第二是模拟虚拟机中的 CPU 指令,通常借助 Intel VT 扩展等硬件虚拟化功能加速。最后,它通过模拟磁盘、网络等硬件 I/O 设备,为虚拟机提供 I/O 服务。
多进程 QEMU
多进程 QEMU 将各项服务拆分到独立的主机进程中。每个进程仅获得提供服务所需的权限。例如,磁盘服务只能访问其提供的磁盘镜像,不能访问其他文件或任何网络设备。攻击者即使攻破该服务,也无法借此访问磁盘服务权限之外的文件或设备。
QEMU 控制进程仍然存在,但在多进程模式下,不再直接与虚拟机交互。虚拟机运行期间,它仍提供设备热插拔或虚拟机实时迁移的用户界面。
实现多进程 QEMU 的第一步,是从 QEMU 主程序分离 I/O 服务,主程序继续提供 CPU 模拟,即控制进程同时也是 CPU 模拟进程。在后续阶段,可以再将 CPU 模拟从控制进程中分离。
分离 I/O 服务
从将 I/O 服务拆分成独立主机进程开始比较合适,原因有两点。首先,QEMU 能模拟大量 I/O 设备,形成很大的潜在可利用接口面;过去也确实出现过源于这些接口的漏洞。其次,QEMU 设备模拟代码具有模块化结构,提供了接口分界点,可以将执行设备模拟的函数与管理客户机 CPU 指令模拟的函数分开。独立进程中模拟的设备称为远程设备。
QEMU 设备模拟
QEMU 设备模拟代码采用面向对象的软件架构。配置选中的对象均编译到 QEMU 二进制程序中,客户机使用时再按名称实例化。例如,模拟名为 "foo" 的设备的代码始终存在于 QEMU 中,但只有目标虚拟机包含该设备时,才会运行实例化代码,例如通过 QEMU 命令行指定 -device foo。
对象模型具有层级结构,因此设备模拟代码会指定父对象,例如 PCI 设备使用 "pci-device"。QEMU 会先实例化父对象,再调用设备的实例化代码。
现有分离模型
要将设备模拟代码与 CPU 模拟代码分离,设备对象代码必须运行在不同进程中。QEMU 已有一些功能可以在主进程之外运行模拟代码,下面逐一介绍。
vhost user 模型
Virtio 客户机设备驱动可以连接到 vhost user 应用,执行 I/O 操作。此模型在客户机中使用专用 virtio 设备驱动,在 QEMU 中使用 vhost user 设备对象。但 QEMU vhost user 代码完成应用配置后,正常运行阶段的 I/O 由该应用执行。vhost user 应用是守护进程,可通过已知的 UNIX 域套接字联系。
vhost 套接字
如前所述,QEMU 内 vhost 设备对象的一项任务是联系 vhost 应用,并发送该设备实例的配置信息。配置过程中,也可以通过套接字向应用发送其他文件描述符,供 vhost user 应用以不同方式使用,以下介绍其中一些用法。
vhost MMIO 存储加速
虚拟机通常通过 KVM 内核驱动使用硬件虚拟化功能。该驱动通过让客户机在虚拟硬件模式中运行,加速 QEMU 对客户机 CPU 指令的模拟。客户机执行无法在虚拟硬件模式中执行的指令时,控制权返回 KVM 驱动,由它通知 QEMU 以软件模拟这些指令。
客户机设备驱动访问 I/O 位置,是可能导致返回 QEMU 的事件之一。QEMU 随后将内存操作分派给对应的设备对象。对于 vhost user 设备,需要通过套接字将内存操作发给 vhost 应用。QEMU virtio 代码通过设置 eventfd 文件描述符加速这条路径,使 vhost 应用能够直接接收 KVM 驱动的 MMIO 存储通知,无需先发送到 QEMU 进程。
vhost 中断加速
vhost 应用使用的另一项优化,是通过 KVM 驱动直接向虚拟机注入中断,同样无需先将中断发回 QEMU 进程。QEMU virtio 初始化代码为 KVM 驱动配置 eventfd,向其写入时会触发客户机中的设备中断。随后将此 irqfd 文件描述符传给 vhost user 应用。
vhost 访问客户机内存
vhost 应用还可以直接访问客户机内存,无需以消息形式向 QEMU 发送数据。这同样通过 QEMU 向 vhost user 应用发送的文件描述符实现。vhost 应用可将描述符传给 mmap(),把客户机地址空间映射到应用中。
IOMMU 增加了一层复杂性,因为给客户机 virtio 设备用于 DMA 读写的地址不是客户机物理地址。此时,QEMU 内的 vhost 代码注册为 IOMMU 映射变化的监听器。vhost 应用维护 IOMMU 转换缓存:未命中时向 QEMU 发送转换请求,映射被清除时则接收 QEMU 的刷新请求。
对设备分离的适用性
vhost 模型的许多部分可复用于分离式设备模拟,尤其是 QEMU 与设备模拟应用之间使用套接字、通过文件描述符经 KVM 注入中断,以及允许应用对客户机执行 mmap() 等做法。
不过,vhost 应用的工作方式与分离式设备模拟的需求存在明显差异。最基本的一点是,vhost 使用定制 virtio 驱动,始终通过 MMIO 存储触发 I/O;分离式设备模拟则必须兼容现有 I/O 设备模型和客户机驱动。MMIO 加载会破坏 vhost 存储加速方式,因为加载是同步操作,完成模拟前客户机无法继续。相比之下,存储是异步的,存储事件发送到 vhost 应用后,客户机即可继续。
另一个区别是,在 vhost user 模型中,一个守护进程可以支持多个 QEMU 实例。这与期望的安全机制相悖:模拟应用只能访问其所服务虚拟机有权访问的文件或设备。 #### qemu-io 模型
qemu-io 是测试 QEMU 块后端对象代码修改的测试框架,例如实现磁盘驱动模拟所用磁盘镜像的代码。qemu-io 本身并不是设备模拟应用,但它确实将 QEMU 块对象编译为独立于 QEMU 主程序的二进制文件。这可能有助于磁盘设备模拟,因为磁盘模拟应用需要包含 QEMU 块对象。
基于代理对象的新分离模型
另一种模型是在 QEMU 程序中使用代理对象,与远程模拟程序通信。这样既能实现分离,也能尽量减少对设备模拟代码的修改。本节其余部分讨论代理对象模型的工作方式。
远程模拟进程
远程模拟进程将原样运行 QEMU 对象层次结构。设备模拟对象也基于 QEMU 代码,因为除最简单的设备外,重新实现对象模型及 QEMU 的众多设备后端都不现实。
这些进程通过 UNIX 域套接字与 QEMU 进程通信,可以独立启动,也可以由 QEMU 启动。两种情况下,模拟进程提供的主机后端都像 QEMU 一样通过命令行指定。例如:
disk-proc -blockdev driver=file,node-name=file0,filename=disk-file0 \
-blockdev driver=qcow2,node-name=drive0,file=file0
表示进程 disk-proc 使用名为 file0 的 qcow2 模拟磁盘作为后端。
模拟进程可以模拟多个客户机控制器。常见配置可能将同一设备类别的所有控制器,例如磁盘或网络控制器,放到同一进程中,使同类型的所有后端可由单个 QMP 监视器管理。
与 QEMU 通信
远程模拟进程的第一个参数是连接代理对象的 Unix 域套接字,此参数必填。
disk-proc <socket number> <backend list>
远程进程 QMP 监视器
与 QEMU 本身一样,可以通过 QMP 监视远程模拟进程。QMP 监视器套接字的指定方式也与 QEMU 相同:
disk-proc -qmp unix:/tmp/disk-mon,server
可通过 UNIX 套接字路径 /tmp/disk-mon 监视该进程。
QEMU 命令行
每个在主机远程进程中模拟的设备,都用类型为 pci-proxy-dev 的 -device 表示。该选项的 socket 子选项指定连接远程进程的 Unix 套接字。必须提供 id 子选项,并与远程进程使用的 ID 相同。
qemu-system-x86_64 ... -device pci-proxy-dev,id=lsi0,socket=3
可用于添加由远程进程模拟的设备。
QEMU 对远程进程的管理
QEMU 不知道远程 PCI 设备的类型。对 QEMU 而言,它就是直通设备。
与模拟进程通信
主通道
主通道在代码中称为 com,用于引导远程进程,也用于传递重置等与具体设备无关的命令。
各设备通道
每个远程设备使用专用通信通道与 QEMU 通信。代理对象在初始化期间通过主通道建立该通道。
QEMU 设备代理对象
QEMU 对象模型基于继承自 "object" 超类的子类。这里关注的是 "device" 和 "bus" 子类,其后代子类共同组成 QEMU 模拟系统的设备树。
代理对象模型使用设备代理对象,替代 QEMU 进程内的设备模拟代码。它们位于对象和总线层次结构中原对象的位置。例如,LSI SCSI 控制器的代理对象是 "pci-device" 的子类,具有与所替代 LSI 控制器对象相同的 PCI 总线父对象和 SCSI 总线子对象。
值得注意的是,所有类型的远程 PCI 设备都由同一种代理对象负责中介。
对象初始化
代理设备对象的初始化方式与其他 QEMU 设备完全相同。
此外,代理对象还执行两项任务: - 解析 "socket" 子选项,通过此通道连接远程进程。 - 使用 "id" 子选项连接独立进程中模拟的设备。
class_init
代理对象的 class_init() 方法通常与被替代对象的行为类似,包括设置代理所需的静态属性和方法。
instance_init / realize
instance_init() 和 realize() 只需执行与代理有关的任务,例如注册自己的 MMIO 处理程序,或创建供其他代理设备日后挂接的子总线。
其他任务取决于具体设备。例如,PCI 设备对象会初始化 PCI 配置空间,以便在 QEMU 进程内形成有效的 PCI 设备树。
地址空间注册
大多数设备由客户机驱动通过访问 I/O 地址或端口来驱动。QEMU 设备模拟代码使用 memory_region_init_io() 等内存区域函数添加回调,客户机访问设备的 I/O 地址空间区域时,QEMU 会调用它们。客户机驱动访问设备时,虚拟机退出硬件虚拟化模式并返回 QEMU,QEMU 再查找并执行对应回调。
代理对象需要复现真实设备模拟器初始化代码中的内存区域调用,但使用自己的回调。这些回调因客户机 I/O 操作被 QEMU 调用时,将操作转发给设备模拟进程。
PCI 配置空间
PCI 设备还具有客户机驱动可访问的配置空间。客户机对该空间的访问由 PCI 父对象处理,而非设备模拟对象。大部分空间只读,但某些寄存器,特别是 BAR 和 MSI 相关寄存器,需要同步到模拟进程。
PCI 父代理
转发客户机 PCI 配置访问的一种方式,是创建 "pci-device-proxy" 类,作为 PCI 设备代理对象的父类。该类继承 "pci-device",覆盖 PCI 父类的 config_read() 和 config_write() 方法,将操作转发给模拟程序。
接收中断
产生中断的设备代理需要创建套接字,接收模拟进程的中断通知。收到通知后,沿总线父对象向上传递,以注入客户机。例如,PCI 设备对象可以使用 pci_set_irq()。
实时迁移
代理会注册保存和恢复实时迁移期间所需的 vmstate。设备代理无需管理远程设备的 vmstate,这由远程进程代理负责,见下文。
QEMU 远程设备操作
DMA 等通用设备操作由远程进程代理通过向远程进程发送消息来执行。
DMA 操作
DMA 操作的处理方式类似 vhost 应用。发给模拟进程的初始消息之一是客户机内存表。每个条目包含文件描述符和大小,模拟进程可据此执行 mmap(),直接访问客户机内存,类似 vhost_user_set_mem_table()。客户机内存必须由基于共享文件的内存支撑,例如使用 -object memory-backend-file,share=on,再将该内存后端设为机器 RAM。
IOMMU 操作
模拟系统包含 IOMMU 时,QEMU 中的远程进程代理需要创建套接字,接收模拟进程的 IOMMU 请求,并通过 address_space_get_iotlb_entry() 处理。为处理 IOMMU 取消映射,远程进程代理还要注册为设备 DMA 地址空间的监听器。在 DMA 地址空间内创建 IOMMU 内存区域时,会向该区域添加取消映射通知器,通过 IOMMU 套接字将取消映射操作转发给模拟进程。
通过 QMP 热插拔设备
QMP "device_add" 命令可添加由远程进程模拟的设备。与 -device 命令行选项一样,它也带有 "rid" 选项。远程进程可以是在 QEMU 启动时启动的,也可以由前述 "add-process" QMP 命令添加。两种情况下,远程进程代理都会把新设备的 JSON 描述转发给相应模拟进程。
实时迁移
远程进程代理还会使用 vmstate_register() 注册实时迁移通知。被调用以保存状态时,代理向远程进程发送辅助套接字文件描述符,供其保存设备 vmstate。接收的字节流长度和数据会保存为代理的 vmstate。代理在新主机上恢复时,提取此 vmstate,并向新远程进程发送辅助套接字文件描述符,后者通过该描述符接收 vmstate,恢复设备。
远程进程中的设备模拟
模拟程序需要的 QEMU 部分包括对象模型、内存模拟对象、目标设备及其依赖设备的模拟对象,以及设备后端。还需要设置机器环境、处理 QEMU 进程请求,并将中断或 IOMMU 映射等机器级请求路由回 QEMU 的代码。
初始化
进程初始化顺序与 QEMU 相同:先初始化后端对象,再初始化设备模拟对象。QEMU 发来的 JSON 描述决定需要创建哪些对象。
地址空间
创建设备对象之前,必须通过 memory_map_init() 配置初始地址空间和内存区域。该调用创建 RAM 内存区域对象 system_memory 和 I/O 内存区域对象 system_io。
RAM
RAM 内存区域的创建方式遵循 pc_memory_init(),但必须使用 memory_region_init_ram_from_fd(),而非 memory_region_allocate_system_memory()。所需文件描述符由前述客户机内存表提供。随后使用 memory_region_add_subregion() 将 RAM 区域加入 system_memory 内存区域。
PCI
I/O 初始化由 QEMU 发来的 JSON 描述驱动。对于 PCI 设备,需要使用 pci_root_bus_new() 创建 PCI 总线,并创建 PCI 内存区域,通过 memory_region_add_subregion_overlap() 加入 system_memory。在 PCI 内存与 RAM 内存重叠的架构上,必须使用允许重叠的版本。
MMIO 处理
与当前 QEMU 中的做法一样,设备模拟对象使用 memory_region_init_io() 安装 MMIO 处理程序,并通过 pci_register_bar() 将这些处理程序关联到 PCI BAR。
为了在模拟进程中使用 address_space_rw() 处理 QEMU 发来的 MMIO 请求,QEMU 进程与设备模拟进程中的 PCI 物理地址必须相同。因此,客户机对 BAR 的编程也必须从 QEMU 转发到模拟进程。
注入中断
设备模拟需要向虚拟机注入中断时,请求沿设备总线对象层次结构向上传递,直到某个总线对象知道如何向客户机发出中断。具体过程取决于中断类型。
PCI 引脚中断
x86 系统在根 PCI 总线对象上挂接模拟的 IOAPIC 对象,根 PCI 对象将中断请求转发给它。IOAPIC 再调用 KVM 驱动,将对应中断注入虚拟机。在模拟进程中,最简单的处理方式是通过 pci_bus_irqs() 配置根 PCI 总线驱动,将中断请求发回 QEMU,再由设备代理对象沿 QEMU 的 PCI 树向上传递。
PCI MSI/X 中断
PCI MSI/X 中断在硬件上通过向 CPU 专用 PCI 地址执行 DMA 写入实现。在 x86 QEMU 中,KVM APIC 对象接收这些 DMA 写入,再调用 KVM 驱动,将中断注入虚拟机。一种简单的模拟进程实现,是初始化时由 QEMU 发消息传递 MSI DMA 地址,再在该地址安装地址空间处理程序,将 MSI 消息转发回 QEMU。
DMA 操作
模拟对象需要通过 DMA 读写客户机内存时,必须先用 dma_memory_map() 将 DMA 地址转换为本地虚拟地址。上述模拟进程内存区域对象会将 DMA 地址转换为设备模拟代码可访问的本地虚拟地址。
IOMMU
QEMU 使用 IOMMU 时,DMA 转换先通过 IOMMU 内存区域将 DMA 地址转换为客户机物理地址,再将物理地址转换为本地虚拟地址。模拟进程也需要类似功能。
IOTLB 缓存
模拟进程维护近期 IOMMU 转换的缓存,即 IOTLB。调用 IOMMU 内存区域的 translate() 回调时,会搜索 IOTLB 缓存,寻找将 DMA 地址映射到客户机物理地址的条目。未命中时,向 QEMU 发消息请求对应转换条目,既用它返回客户机地址,也将其加入缓存。
IOTLB 清除
IOMMU 模拟还必须处理 QEMU 发来的取消映射请求。客户机 IOMMU 驱动从转换表清除条目时,就会产生此类请求。
实时迁移
远程进程收到 QEMU 的实时迁移通知后,使用收到的文件描述符调用 qio_channel_socket_new_fd() 建立通道。再利用该通道创建 QEMUfile,传给 qemu_save_device_state(),将进程的设备状态发回 QEMU。恢复时则反向操作,将通道传给 qemu_loadvm_state() 以恢复设备状态。
加速设备模拟
QEMU 与模拟进程之间必须发送的消息,可能给 I/O 操作增加显著延迟。以下优化尝试通过允许模拟进程直接与内核 KVM 驱动通信来缓解影响。创建的 KVM 文件描述符通过初始化消息传给模拟进程,类似于客户机内存表。 #### MMIO 加速
vhost user 应用可以直接从 KVM 接收客户机 virtio 驱动的存储操作。其 eventfd 机制的问题在于,事件通知不携带数据,因此无法处理客户机加载,或携带存储数据的客户机存储。不过,可以扩展此思路以覆盖更多情况。
扩展方案需要新的 KVM 设备类型 KVM_DEV_TYPE_USER。它有两个文件描述符:主描述符供 QEMU 配置,从描述符供模拟进程接收 MMIO 通知。QEMU 通过 KVM 驱动创建两者,并通过初始化消息将从描述符传给模拟进程。
数据结构
客户机物理范围
客户机物理范围结构描述设备响应的地址范围,包含基址、长度以及所属总线。例如,在 x86 机器上,可以指定该范围属于内存地址还是 I/O 地址。
设备可能响应多个物理地址范围,例如 PCI 设备可以具有多个 BAR,因此结构还包含枚举标识符,指出具体引用哪个范围。
名称 |
描述 |
|---|---|
addr |
范围基地址 |
len |
范围长度 |
bus |
地址类型(内存或 I/O) |
id |
范围 ID(如 PCI BAR) |
MMIO 请求结构
该结构描述 MMIO 操作,包含其所在的客户机物理范围、范围内偏移、MMIO 类型(例如加载或存储)、长度和数据。还包含可用于回复 MMIO 的序列号,以及发出 MMIO 的 CPU。
名称 |
描述 |
|---|---|
rid |
MMIO 所在范围 |
offset |
rid 内的偏移 |
type |
例如加载或存储 |
len |
MMIO 长度 |
data |
存储数据 |
seq |
序列 ID |
MMIO 请求队列
MMIO 请求队列是 MMIO 请求结构的 FIFO 数组。有两个队列:pending 队列保存模拟程序尚未读取的 MMIO,sent 队列保存尚未确认的 MMIO。后者主要用于验证模拟程序的 MMIO 回复。
记分板
虚拟机中每个 CPU 都由 QEMU 中的独立线程模拟,因此可能有多个 MMIO 等待模拟程序处理,也可能有多个线程等待 MMIO 回复。记分板为每 CPU 线程保存等待队列和序列号,使收到模拟程序的 MMIO 回复时能单独唤醒相应线程。它还跟踪向设备发出但尚未回复的 posted MMIO 存储数量,以满足 PCI 约束:对设备的加载必须等待此前发给该设备的所有存储完成后,才能完成。
设备影子内存
有些 MMIO 加载没有设备副作用。如果模拟程序与 KVM 驱动共享设备内存的影子副本,就无需向模拟程序发送 MMIO 请求,也能完成这些操作。
模拟程序请求 KVM 驱动为影子副本分配内存,再使用 mmap() 直接访问。模拟程序可向 KVM 发送访问映射,控制 KVM 对影子副本的访问,说明哪些区域没有副作用、可立即完成访问,哪些区域需要发送 MMIO 请求。访问映射还可告诉 KVM 驱动允许的访问大小。
主描述符
QEMU 使用主描述符配置新的 KVM 设备。当 QEMU 发出类型为 KVM_DEV_TYPE_USER 的 KVM_CREATE_DEVICE ioctl() 时,KVM 驱动返回该描述符。
KVM_DEV_TYPE_USER 设备操作
KVM 系统由 kvm_init() 初始化时,通过 kvm_register_device_ops() 注册 KVM_DEV_TYPE_USER 操作向量。QEMU 对 KVM 文件描述符执行某些 ioctl() 时,KVM 驱动调用这些设备操作,包括:
create
当 QEMU 在每虚拟机文件描述符上发出 KVM_CREATE_DEVICE ioctl() 时调用此例程。它分配并初始化 KVM 用户设备专用数据结构,再让 kvm_device 的 private 字段指向该结构。
ioctl
QEMU 对主描述符发出 ioctl() 时调用此例程。支持的命令由 KVM 设备类型定义。KVM_DEV_TYPE_USER 需要以下几个命令:
KVM_DEV_USER_SLAVE_FD 创建要传给设备模拟程序的从文件描述符。每个主描述符只能创建一个从描述符。该描述符执行的文件操作见下文。
KVM_DEV_USER_PA_RANGE 配置客户机物理地址范围,从描述符将接收该范围的 MMIO 通知。范围由客户机物理范围结构参数指定。对于动态为设备分配地址的总线,可以在客户机运行时执行该命令,例如客户机修改设备 PCI BAR 寄存器时。
KVM_DEV_USER_PA_RANGE 使用 kvm_io_bus_register_dev() 注册 kvm_io_device_ops 回调,客户机在该范围执行 MMIO 操作时调用它们。范围改变时,使用 kvm_io_bus_unregister_dev() 移除之前的实例。
KVM_DEV_USER_TIMEOUT 配置超时值,指定 KVM 等待模拟进程响应 MMIO 通知的时长。
destroy
销毁虚拟机实例时调用此例程。它需要销毁从描述符,释放驱动分配的全部内存,以及 kvm_device 结构本身。
从描述符
从描述符具有自己的文件操作向量,响应设备模拟程序对该描述符执行的系统调用。
read
读取将以 MMIO 请求结构的形式返回 KVM 驱动中待处理的 MMIO 请求。如果有多个待处理操作,可以一次返回多个结构。请求从 pending 队列移到 sent 队列;如果某些线程因等待 pending 队列空位以添加新 MMIO 操作而阻塞,也会在此唤醒。
write
写入同样包含一组 MMIO 请求。将其与 sent 队列中的请求比较,匹配项从队列移除,并唤醒等待回复的线程。如果移除的是存储操作,就递减每 CPU 记分板中的 posted 存储数量。当该数量为零,且有无副作用的加载正在等待 posted 存储完成时,继续该加载。
ioctl
从描述符支持几种 ioctl()。
KVM_DEV_USER_SHADOW_SIZE ioctl() 让 KVM 驱动为影子副本分配内存。模拟进程随后可对其执行 mmap(),与 KVM 驱动共享设备内存的模拟视图。
KVM_DEV_USER_SHADOW_CTRL ioctl() 控制对影子副本的访问,向 KVM 驱动发送影子控制映射,指定哪些区域可以不将加载请求发送给模拟程序就完成客户机加载,并指定允许的加载操作大小。
poll
模拟程序通过带 POLLIN 标志的 poll() 调用,判断是否有等待读取的 MMIO 请求。pending MMIO 请求队列非空时返回。
mmap
此调用允许模拟程序直接访问 KVM 驱动分配的影子副本。设备模拟更新设备内存时,无副作用的修改会反映到影子中,KVM 驱动可直接从影子副本满足客户机加载,无需等待模拟程序。
kvm_io_device 操作
每个 KVM 每 CPU 线程都能代表客户机虚拟机处理 MMIO。KVM 根据 MMIO 的客户机物理地址查找匹配的 kvm_io_device,判断是否能由 KVM 驱动处理,而无需退出到 QEMU。找到匹配项后,调用对应回调。
read
客户机从设备加载时调用此回调。有副作用的加载必须同步处理:KVM 驱动让 QEMU 线程休眠,等待模拟进程回复后再恢复客户机。无副作用的加载可通过影子副本优化完成,前提是该 CPU 没有向该设备发出尚未完成的存储。PCI 内存顺序要求,加载必须等待此前向同一设备发出的所有存储完成后才能完成。
write
存储可以异步处理,除非 pending MMIO 请求队列已满。队列满时,QEMU 线程必须休眠等待空位。存储会增加每 CPU 记分板中的 posted 存储数量,以实现上述 PCI 顺序约束。
中断加速
这项性能优化类似 vhost user 应用:QEMU 进程设置 eventfds,由 KVM 驱动触发设备对应的中断。这些 IRQ 文件描述符在初始化时发送给模拟进程,供模拟代码产生设备中断时使用。
intx 加速
传统 PCI 引脚中断基于电平,因此除了 IRQ 文件描述符,还需要向模拟程序发送重采样文件描述符。第二个描述符让共享 IRQ 的多个设备在客户机确认中断时得到通知;如果设备尚未撤销中断信号,就可以再次触发。
intx IRQ 描述符
代理对象通过 using event_notifier_init() 创建 IRQ 和重采样 eventds,并通过 kvm_vm_ioctl(KVM_IRQFD) 将其绑定到中断。中断路由可通过 pci_device_route_intx_to_irq() 查找。
intx 路由变化
客户机配置设备引脚所连接的 APIC 时,可能改变 Intx 路由。QEMU 中的代理对象通过 pci_device_set_intx_routing_notifier() 接收客户机路由修改通知。该处理程序大体遵循 VFIO 中断逻辑:先解除现有 IRQ 描述符与路由的关联,再绑定新路由,参见 vfio_intx_update()。
MSI/X 加速
MSI/X 中断以 DMA 事务形式发往主机。中断数据包含客户机配置的向量。一个设备可能关联多个 MSI 中断,因此可能需要向模拟程序发送多个 IRQ 描述符。
MSI/X IRQ 描述符
这种情况也遵循 VFIO 示例。为每个 MSI/X 中断创建 eventfd,通过 kvm_irqchip_add_msi_route() 分配虚拟中断,再通过 kvm_irqchip_add_irqfd_notifier() 将虚拟中断绑定到 eventfd。
MSI/X 配置空间变化
客户机可以动态更新设备 PCI 配置空间中的多个 MSI 相关表,包括每个 MSI 中断的使能状态和向量数据。此外,MSIX 表位于设备内存空间,而非配置空间。与前述 BAR 情况类似,代理对象必须观察客户机对配置空间的编程,保持 QEMU 与模拟程序的 MSI 中断状态一致。
分离 CPU 模拟
I/O 服务分离后,第二阶段是将 CPU 指令模拟从 QEMU 主控制功能中分离到独立进程。这部分代码没有对象分离点,因此首要任务是创建一个。
主机访问控制
QEMU 分离依靠主机操作系统的访问限制机制,确保不同进程只能访问获准访问的对象。通用操作系统通常提供以下几类机制。
自主访问控制
自主访问控制允许每个用户决定谁能访问自己的文件。在 Linux 中,这种控制通常过于粗糙,不适合 QEMU 分离,因为它只提供三类访问控制:同一用户 ID、同一组 ID 的用户,以及其他所有用户。要实现访问控制,每个设备实例都需要单独的用户 ID,对于动态创建的虚拟机很可能难以管理。
强制访问控制
强制访问控制允许操作系统在自主访问控制之上添加额外规则,由操作系统掌控。它还为进程和文件添加类型、角色、类别等属性,并能制定进程与文件交互的规则。
类型强制
类型强制为进程和文件分配 类型 属性,并通过规则规定给定类型的进程,可以对给定类型的文件执行哪些操作。QEMU 分离可以利用类型强制,让模拟进程以不同类型运行,既区别于 QEMU 主进程,也区别于其他设备类别的模拟进程。
例如,客户机磁盘镜像和磁盘模拟进程可以采用独立类型,区别于 QEMU 主进程和非磁盘模拟进程,并通过类型规则禁止磁盘模拟进程之外的进程访问客户机磁盘镜像。类似地,网络模拟进程可以具有独立于主进程和非网络模拟进程的类型,仅该类型可访问提供客户机网络的主机 tun/tap 设备。
类别强制
类别强制从给定范围中选取一组数字,分配给进程或文件。只有进程的集合包含文件的集合时,才允许访问。此机制可用于分离同一类别设备的多个实例。
例如,客户机拥有多个磁盘设备时,可以为每个设备模拟进程配置不同类别,使不同进程无法访问彼此的后备磁盘镜像。
类别也可以替代前述类型强制方案。在这种情况下,使用不同类别阻止不同设备类别的模拟进程访问分配给其他类别的资源。