QEMU 内部原理:整体架构和线程模型『译』
原文地址:http://blog.vmsplice.net/2011/03/qemu-internals-overall-architecture-and.html
Author:Stefan Hajnoczi
Translator:Su Hang
本文经原作者Stefan Hajnoczi授权翻译
这是针对开发人员的 QEMU 内部原理探析系列中的第一篇文章。它旨在分享 QEMU 工作原理的知识,并使新贡献者更容易了解 QEMU 代码。
执行 guest 代码涉及到如下任务:处理定时器、处理 I/O 请求、响应虚拟机监视器的命令,等等。要想设计一个良好的、能够解决这些问题的架构,需要安全地一次性地解决所有这些资源的分发问题。尤其是当某些请求,譬如 I/O 请求、来自用户的命令,需要大量的时间去执行的话。
一个 guest 的运行包括执行 guest 代码,处理定时器,处理 I / O 以及响应监视器命令。要想一次性地安排好所有这些事情需要一个能够以安全的方式调解资源的体系结构,而且最好不会暂停 guest 代码的执行。如果磁盘 I / O 或监视器命令需要很长时间才能完成,对于需要响应来自多个来源的事件的程序,有两种流行体系结构:
- 并行体系架构,将工作分解为可同时执行的进程或线程。我将称之为线程架构。
- 事件驱动架构,通过在一个主循环分派各个事件到与之对应的事件处理函数。这通常使用
select(2)
或poll(2)
系列调用系列来实现,以等待多个文件描述符。
但 QEMU 实际上使用了一种将事件驱动编程与多线程相结合的混合架构。这样做是有道理的,因为事件循环不能利用多个 CPU 内核的特性,因为它只有一个执行线程。另外,有时编写专用线程来处理一个特定的任务,而不是将其集成到事件驱动的体系结构,在编程实现中更为简单。尽管如此,QEMU 的核心还是事件驱动的,大多数代码在这种环境中执行。
QEMU 的事件驱动核心
事件驱动的架构以事件循环为中心,该事件循环将事件分派给处理函数。QEMU 的主要事件循环是main_loop_wait()
,它执行以下任务:
- 等待文件描述符变为可读或可写。文件描述符起着至关重要的作用,因为无论是文件、套接字、管道还是各种其他资源都是通过文件描述符来控制的。文件描述符可以使用
qemu_set_fd_handler()
来添加。 - 运行会定时过期的定时器。定时器可以使用
qemu_mod_timer()
添加。 - 运行下半部机制 (BHs)(译者注:所谓 bottom-halves 机制,是指在允许中断的情况下,将中断处理程序延迟执行),就像立即过期的定时器一样。BH(译者注:原文中 BH 是 bottom halves 的缩写,下同)用于避免重入和溢出调用堆栈。使用
qemu_bh_schedule()
添加 BHs。
Linux 将一些中断处理分成两部分,第一部分是在关中断的条件下执行的,具有”原子”性,而且是中断发生以后一般要立即执行的,第二部分,就是 bottom half 了,是在开中断的条件下执行,这部分是可以延迟一段时间再做的,而且有可能将多个中断的 bottom half 合并起来一起做。
当文件描述符准备就绪,计时器到期或调度 BH 时,事件循环会调用相应该事件的回调函数。关于回调函数的执行场景,有两条简单规则:
- 没有其他核心代码正在同时执行,因此不需要考虑同步问题。回调函数相对于其他核心代码是顺序的,并且以原子的方式执行。在任何时候只有一个控制线程执行核心代码。
- 回调函数不应阻塞系统调用或进行长时间运行的运算。由于事件循环会在回调函数返回之前等待,所以其他想要被执行的事件就因此被阻塞,避免在回调中花费大量时间非常重要。打破此规则会导致 guest 虚拟机暂停并且监视器无法再响应用户。
第二条规则有时在 QEMU 的某些代码中其实是难以实现的。事实上,qemu_aio_wait()
中甚至有一个嵌套事件循环等待顶层事件循环处理的事件子集。希望在未来重构代码时,可以消除这些违背了第二天规则的代码。新代码几乎从来没有合法的理由阻止,一种解决方案是使用专用工作线程来卸载长时间运行或阻塞的代码。
将特定任务分配到工作线程
虽然许多 I / O 操作可以以非阻塞方式执行,但是有些系统调用没有非阻塞版本。此外,有时候长时运行的计算任务会影响 CPU,并且很难将其分解成多个小的回调函数。在这些情况下,可以谨慎地通过分配专用工作线程的方式,将这些任务移出 QEMU 核心函数。
一个工作线程的用户示例是posix-aio-compat.c
,一个异步文件 I / O 实现。当 QEMU 核心代码发出 aio 请求时,该请求将被放置在一个队列中。工作线程将从队列中取出该 aio 请求并在 QEMU 核心函数之外去执行。这时就可以执行阻塞操作了,因为这些任务在自己的线程中执行并且不会阻塞 QEMU 的其余部分。通过这种方式需要注意在工作线程和 QEMU 核心函数之间执行必要的同步和通信。
另一个例子是ui/vnc-jobs-async.c
,它在工作线程中进行密集的图像压缩和编码计算。
由于大多数 QEMU 核心代码不是线程安全的,所以工作线程不能调用 QEMU 核心代码代码。对于简单的实用程序——如qemu_malloc()
——是线程安全的,但这算是例外而非规则。这种特性使得将工作线程事件传回 QEMU 核心函数变成了一个难题。
当工作线程需要通知 QEMU 核心代码时,会在事件循环中添加管道或qemu_eventfd()
文件描述符。工作线程可以写入文件描述符,并且当文件描述符的状态变为可读时,事件循环将调用回调函数。另外,必须有一个信号来确保事件循环能够在任何情况下运行。在了解 guest 代码的执行方式后,posix-aio-compat.c
使用的这种方法更加自然。
执行 guest 代码
到目前为止,我们主要关注的是事件循环及其在 QEMU 中所扮演的的核心角色。但同样重要的是执行 guest 代码的能力,如果没有可执行的 guest 代码,QEMU 即使可以对事件做出响应,但这并没有太大意义。
执行 guest 代码有两种机制:微型代码生成器 (TCG) 和 KVM。TCG 使用动态二进制翻译(也称为即时 (JIT) 编译)模拟 guest。KVM 利用现代英特尔和 AMD CPU 中的硬件虚拟化扩展技术,直接在主机 CPU 上安全地执行 guest 代码。对于本文来说,在使用中实际使用哪种技术并不重要,但重要的是 TCG 和 KVM 都允许我们跳入 guest 代码并执行。
跳入 guest 代码会将执行的控制权转移给 guest。当线程正在运行 guest 代码时,它不能同时处于事件循环中,因为 guest 端对 CPU 具有(安全的)控制权。通常来说,在 guest 代码中花费的时间是有限的,因为对模拟设备寄存器的读写和其他异常处理都将导致我们离开 guest 并将控制权交还给 QEMU。但在极端情况下,guest 可以花费无限的时间去执行某段代码,而且不放弃其控制权限,在这种情况下 QEMU 会无法响应外界信息。
为了解决 guest 代码占用 QEMU 的控制线程的问题,信号被用来打破 guest 的控制权限。一个 UNIX 信号会将控制权限拉离 (yank) 当前的执行流程,并调用信号处理函数。这使 QEMU 得以采取一系列步骤脱离 guest 代码,并返回到其主循环中,其中事件循环可以有机会处理被持续推入到队列中的事件。
这样做的结果是,如果 QEMU 当前处于 guest 代码中,则可能无法立即检测到新事件。当然,大多数时候 QEMU 最终都会处理事件,但这种额外的延迟本身就是一个需要克服的性能问题。由于这个原因,定时器、I / O 完成 (completion) 和从工作线程到 QEMU 核心代码的通知 (notification),使用信号机制来确保事件循环将立即运行。
你可能想知道事件循环与具有多个 vcpus 的 SMP guest 虚拟机之间的整体情况。在已经讨论了线程模型和执行 guest 代码之后,我们可以讨论它的整体架构。
iothread 和 non-iothread 架构
传统的体系结构是单个 QEMU 线程来执行 guest 代码和事件循环。这个模型也被称为非 iothread 或!CONFIG_IOTHREAD
,并且在使用./configure && make
编译 QEMU 源码时是默认的。QEMU 线程执行 guest 代码,直到异常或信号产生一次回退控制。然后在select(2)
中以非阻塞的方式运行事件循环的一次迭代。之后,它回到 guest 代码并重复这一过程,直到 QEMU 进程退出。
如果 guest 虚拟机使用-smp 2
的方式启动多个 vcpus,在这种情况下,就不会创建额外的 QEMU 线程。而是单个 QEMU 线程以多路复用的方式,在两个 vcpus 的 guest 代码和事件循环之间执行。因此,non-iothread 无法利用多核主机,并可能导致 SMP guest 机性能不佳。
请注意,尽管只有一个 QEMU 线程,但可能有零个或多个工作线程。这些线程既可能是暂时的有可能是永久的。请记住,它们执行特定的任务,而不执行 guest 代码或处理事件。我想强调一下,因为在监视 host 上面的线程,并将它们解释为 vcpu 线程时,工作线程很容易被 vcpu 线程所混淆。请记住,non-iothread 线程只有一个 QEMU 线程。
较新的体系结构是每个 vcpu 一个 QEMU 线程以及一个专用的事件循环线程。这种模式被称为 iothread 或 CONFIG_IOTHREAD,可以在构建时使用./configure --enable-io-thread
启用。每个 vcpu 线程可以并行执行 guest 代码,提供真正的 SMP 支持,而 iothread 负责运行事件循环。通过全局互斥体来维护,QEMU 核心代码代码永远不会同时运行的规则是。该全局互斥体通过 vcpus 和 iothread 同步 QEMU 核心代码。大多数情况下,vcpus 将执行 guest 代码,并且不需要保存全局互斥锁。大多数情况下,select(2)
中的线程被阻塞,并且不需要保持全局互斥。
请注意,TCG 不是线程安全的,因此即使在 iothread 模型下,它也是以单个 QEMU 线程来实现多路复用 vcpus。只有 KVM 可以利用 per-vcpu 线程。
结论和展望
希望这有助于交流 QEMU 的整体架构 (KVM 继承)。欢迎在下面的评论中留下问题。
在将来,上面讨论到的细节可能会改变,我希望我们会默认使用 CONFIG_IOTHREAD,甚至可能会删除!CONFIG_IOTHREAD。
当 qemu 的 master 分支做出更改时,我会尝试更新此帖。