了解 QEMU 设备模拟原理『译』
原文地址:https://www.qemu.org/2018/02/09/understanding-qemu-devices/
Author:Eric Blake
Translator:Su Hang
以下是一些可帮助新手了解 QEMU 设备实际原理的笔记:
在使用 QEMU 时,需要记住的一件事就是:以操作系统的视角来说,当其运行于我们试图模拟的裸机硬件之上时,操作系统在裸机硬件上会看到什么。大多数裸机的模拟实现仅仅是简单的内存映射,在特定地址上的软件戳(software poking) 会产生特定的边际效应(side effect)(最常见的边际效应当然是访问内存;但是内存中的其他常见区域还包括用于控制寄存器组特定的硬件,如硬盘或网卡,甚至 CPU 本身)。模拟的最终目标是允许只使用普通 ring3 内存访问(译者注:这里的普通内存访问与操作系统的 ring0 特权访问相对应)的用户空间程序来管理 guest 操作系统所期望的所有边际效应。
从实现细节上来说,一些硬件(如 x86)实际上有两个内存空间,其中 I / O 空间使用与普通内存访问(译者注:例如 mov a, b) 不同的汇编代码(译者注:这里指 in a,out a 等等 x86 架构特有汇编指令); QEMU 必须模拟这些特殊访问。同样,许多现代 CPU 在内存地址映射范围中,也为提供了一组 CPU 本地寄存器,例如中断控制寄存器。
对于在某些特定硬件,我们通过虚拟化挂钩技术(virtualization hooks),使得 CPU 本身可以很容易地捕获非正常存储器访问的汇编指令(那些访问 I / O 空间或 CPU 内部寄存器的指令,因此需要不同于正常存储器访问的边际效应),所以 guest 机只是执行与裸机相同的汇编指令序列,但是执行之后会导致陷入到 trap 中,让运行于用户空间的 QEMU 在对控制权返回 guest 代码之前使用普通的用户空间内存访问对指令作出反应。QEMU 通过“加速器”实现了这个功能。
虚拟化加速器(如 KVM)可以使 guest 代码运行在 QEMU 虚拟机中的速度几乎与裸机一样快。其中速度减慢的部分是由来自 guest 代码的每个 trap 都会返回至 QEMU(vmexit)执行,以处理非正常的汇编指令或内存地址引起的。除此之外,QEMU 还支持其他虚拟化加速器(例如 HAXM 或 macOS 的 Hypervisor.framework)。
QEMU 还拥有一个 TCG 加速器,该加速器在运行期(译者注:可视为一种 Just in time 技术)将 guest 汇编指令编译为相应的主机指令或调用主机帮助例程。TCG 技术虽然速度不及硬件加速,但它允许跨硬件模拟,例如在 x86 上运行 ARM 代码。
接下来要弄清楚的是当操作系统访问各种硬件资源时都发生了些什么。例如,大多数操作系统都附带了管理 IDE 磁盘的驱动程序 - 驱动程序仅仅是一种软件,它被编程为对特定的内存映射子集(译者注:简单来说就是某一片内存区域)(IDE 总线所在的任何位置)发出特定的 I / O 请求,与不同的硬件相绑定)。当 IDE 控制器硬件接收到这些 I / O 请求时,它会执行相应的操作(通过 DMA 传输或其他硬件操作)将数据从内存复制到永久存储器(写入磁盘)或从永久存储器复制到内存(从磁盘读取)。
当我们购买一个包含未初始化磁盘的裸机硬件时,我们安装使用操作系统中相应驱动程序,对 IDE 硬件映射到内存的部分进行访问,然后将磁盘格式化为一系列分区和文件系统。
那么,QEMU 如何模拟这个功能呢?在 QEMU 提供给 guest 代码的内存映射中,它在与裸机相同的地址处(译者注:具体地址得查阅相应的硬件手册)模拟 IDE 磁盘。当 guest 操作系统驱动程序向 IDE 控制寄存器发出特定的内存写入操作以便将数据从内存复制到永久存储器时,QEMU 加速器会陷入该内存区域(译者注:通过 Hook 技术实现),并将请求传递到 QEMU IDE 控制器设备模型。设备模型会解析 I / O 请求,并通过发出主机系统调用来模拟它们。这一系列行为的结果是 guest 内存被复制到 host 机器的存储中。
在 host 端,模拟永久存储的最简单方法是将主机文件系统中的文件视为不包含任何结构信息的原始数据(raw data)(也就是说,host 文件中的偏移量与 guest 驱动程序访问的磁盘偏移量,以 1:1 比例映射). 但 QEMU 实际上能够将许多不同主机格式(raw, qcow2,qed, vhdx,…)和协议(文件系统,块设备, NBD, Ceph,gluster 等)的任意组合粘合在一起作为后端,然后在 QEMU 对硬件的模拟中绑定到提供服务的 guest 设备。
因此,当您告诉 QEMU 使用主机 qcow2 文件时,guest 虚拟机不必对 qcow2 文件格式有任何了解,仅仅只需要 guest 的驱动程序执行与裸机相同的寄存器读写操作,从而触发 vmexits 进入 QEMU 代码,然后 QEMU 将这些访问映射到 qcow2 文件的相应偏移量中来进行读写。在首次安装 guest 虚拟机时,所有 guest 虚拟机都会看到一个空白未初始化的线性磁盘(无论该磁盘在主机中是线性的——如原始数据(raw format);还是针对随机访问进行了优化,如 qcow2 格式);guest 操作系统决定如何划分其硬盘并在其上安装文件系统,而 QEMU 不关心 guest 代码正在使用什么文件系统,只关心原始磁盘(raw disk)I / O 寄存器控制序列的模式。
接下来要意识到的是,模拟 IDE 并不总是最高效的。每次 guest 试图写入控制寄存器时,都必须经过特殊处理,并且 vmexits 会减慢模拟速度。当然,不同的硬件模型在虚拟化时具有不同的性能特征。然而,一般来说,对真实硬件最有效实现方法并不一定适用于其在虚拟化之中的实现,直到最近,硬件并没有被设计成当通过 QEMU 等软件进行模拟时运行得更快。因此,QEMU 包含专为此目的而设计的半虚拟化设备(paravirtualized device)。
在 QEMU 这里的“半虚拟化”的含义,与半虚拟化的原始含义:“通过 guest 和主机之间的合作实现虚拟化”略有不同。QEMU 开发人员已经制定了一套硬件寄存器规范,并规定了这些寄存器的行为,这些寄存器旨在尽可能减少 vmexits 的数量,同时仍然完成硬盘必须做的事情,即实现 guest 内存和持久存储设备之间的传输。这个规范被称为 virtio;使用它需要在 guest 虚拟机中安装 virtio 驱动程序。尽管不存在与 virtio 具有相同的寄存器布局的物理设备,但其理念是相通的:virtio 磁盘的行为类似于内存映射寄存器组,guest 操作系统驱动程序知道将哪些操作硬件的寄存器值的写入该存储体,以使数据被复制进出其他 guest 内存。virtio 中的大部分加速功能都是通过它的如下设计实现的:guest 虚拟机为其大部分硬件命令序列设置了一部分常规内存,只需启动一个寄存器即可告知 QEMU 读取命令序列(较少的映射寄存器访问意味着更少的 vmexits),通过握手机制(handshaking)来保证 QEMU 处理这些命令序列时 guest 端驱动程序在不会改变正常内存。
顺带一提,就像最新的硬件在实现虚拟化时效率十分高效一样,virtio 也在演进为通过硬件来实现变得更加高效,当然不会以牺牲模拟或虚拟化的性能来达到此目的。因此,将来你也可能会偶然发现实现了物理 virtio 的高性能设备。
同样,许多操作系统都支持多个网卡,一个常见的例子就是 PCI 总线上的 e1000 板卡。在裸机上,操作系统将检测 PCI 空间,当看到具有 e1000 签名的寄存器组被填充时,就加载驱动程序,然后该驱动程序知道要写入的寄存器命令序列,以便让硬件传输网络流量。因此,QEMU 拥有一台 e1000 设备——作为众多网卡模拟实现之一——映射到同一个内存区域(译者注:这里的 guest 内存区域指的是 host 上面的用户内存区域),而真正的 guest 内存区域将裸露在被模拟的裸机内存上。
其次,e1000 寄存器布局往往需要大量的寄存器写入(并因此需要 vmexits)来满足硬件的工作需求,因此 QEMU 开发人员添加了 virtio-net 卡(PCI 硬件规范,尽管现在还没有实现它的真实物理硬件),因此在 guest 操作系统中安装 virtio-net 驱动程序可以最大限度地减少 vmexits 的数量,同时还能获得与发送网络流量的相同边际效应。如果您告诉 QEMU 使用 virtio-net 卡启动 guest 虚拟机,则 guest 虚拟机操作系统将探测 PCI 空间,并使用 virtio-net 签名查看一系列寄存器,并加载适用于任何其他 PCI 硬件的适当驱动程序。
总结一下,尽管 QEMU 最初是为了虚拟化 guest 操作系统而模拟硬件内存映射,但事实证明,最快的虚拟化还是取决于虚拟硬件:具有特定边际效应的寄存器内存映射的效率没有任何物理硬件能够匹敌。所有的硬件设备虚拟化实际上意味着运行一组特定的汇编指令(guest 操作系统)来处理映射到内存中的地址,以产生一组特定的边际效应,其中 QEMU 仅仅是一个提供内存映射,并模仿在裸机硬件上执行这些 guest 指令时所获得的相同边际效应的用户空间应用程序。