原文地址:http://blog.vmsplice.net/2011/03/qemu-internals-overall-architecture-and.html
全文翻译如下:
1 概述
这是讲解QEMU内部原理的系列博客的第一篇,主要面向于开发者。旨在分享QEMU如何工作等相关的代码基础知识。
运行一个客户机涉及到执行客户机代码、处理计时器、处理I/O操作、相应模拟器的命令。并发处理这些工作要求一种安全调节资源的能力,当磁盘I/O或者模拟器命令需要执行很长时间的时候不暂停客户机。现有两个主流的架构可以用于响应来自多个源的事件:
1. 并行架构:分离工作到可以并行的进程或者线程中执行。
2. 事件驱动架构:通过执行一个主循环来发送事件到handler以对事件做处理。这一方法通常使用select(2)或者poll(2)系列的系统调用在多个文件描述符上进行等待。
QEMU实际上使用了一种将事件驱动编程与线程相结合的混合架构。 这样做是有道理的,因为事件循环不能利用多个核心,因为它只有一个执行线程。 此外,有时编写专用线程来装载一个特定任务而不是将其集成到事件驱动架构中更简单。 然而,QEMU的核心是事件驱动的,大多数代码在该环境中执行。
2 QEMU的事件驱动核心
事件驱动的体系结构以事件循环为中心,该事件循环将事件分派给处理函数。 QEMU的主事件循环是main_loop_wait(),它执行以下任务:
1. 等待文件描述符变得可读或可写。 文件描述符起着至关重要的作用,因为文件、套接字、管道、各种其他资源都是文件描述符。 可以使用qemu_set_fd_handler()添加文件描述符。
2. 运行过期的计时器。 可以使用qemu_mod_timer()添加计时器。
3. 运行下半部分(BHs),就像定时器立即到期一样。 BH用于避免重入和溢出调用堆栈。 可以使用qemu_bh_schedule()添加BH。
当文件描述符准备就绪、计时器到期、BH被调度时,事件循环将调用响应事件的回调。 回调有两个关于其环境的简单规则:
1) 没有其他核心代码同时执行,因此不需要同步。 回调相对于其他核心代码按顺序和原子方式执行。 在任何给定时间只有一个控制线程执行核心代码。
2)不应执行阻塞系统调用或长时间运行的计算。 由于事件循环在继续其他事件之前等待回调返回,因此避免在回调中花费无限量的时间是很重要的。 违反此规则会导致guest虚拟机暂停并且监视器无响应。
第二条规则有时难以兑现,QEMU中有代码阻塞。 事实上,甚至在qemu_aio_wait()中有一个嵌套的事件循环,它等待顶级事件循环处理的事件的子集。 希望将来通过重组代码来消除这些违规行为。 新代码不可能有合法的理由去阻塞执行,一个解决方案是使用专用的worker threads来卸载长时间运行代码或阻塞代码。
3 卸载特殊的任务到worker threads
尽管可以通过非阻塞方式执行许多I/O操作,但也存在没有非阻塞效果的系统调用。 此外,有时长时间运行的计算只会占用CPU并且难以分解为回调。 在这些情况下,可以使用专用worker threads小心地将这些任务移出QEMU的核心线程。
工作线程的一个示例用法是posix-aio-compat.c,一个异步文件I/O实现。 当核心QEMU发出aio请求时,它将被放置在队列中。 worker threads将请求从队列中取出并在核心QEMU之外执行它们。 它们可能会执行阻塞操作,因为它们在自己的线程中执行,所以不会阻塞QEMU的其余部分。 该实现负责在worker threads和核心QEMU之间执行必要的同步和通信。
另一个例子是ui/vnc-jobs-async.c,它在worker threads中执行计算密集型镜像压缩和编码。
由于大多数核心QEMU代码不是线程安全的,因此工作线程无法调用核心QEMU代码。 像qemu_malloc()这样的简单实用程序是线程安全的,但这只是例外而不是规则。这给将工作线程事件传递回核心QEMU带来了问题。
当worker thread需要通知核心QEMU时,一个管道或qemu_eventfd()文件描述符将添加到事件循环中。 worker thread可以写入文件描述符,当文件描述符变得可读时,事件循环将调用回调。 此外,必须使用信号来确保事件循环能够在所有情况下运行。 这种方法由posix-aio-compat.c使用,在理解了客户机代码的执行方式后更有意义(尤其是信号的使用)。
4 执行客户机代码
到目前为止,我们主要研究了事件循环及其在QEMU中的核心作用。 同样重要的是执行客户机代码的能力,没有这种功能,QEMU可以响应事件但不会非常有用。
执行访客代码有两种机制:Tiny Code Generator(TCG)和KVM。 TCG使用动态二进制转换(也称为即时(JIT)编译)来模拟客户机。 KVM利用现代Intel和AMD CPU中的硬件虚拟化扩展,直接在主机CPU上安全地执行访客代码。 出于本文的目的,先不关注各自具体技术细节,重要关注的是TCG和KVM都允许我们跳转到客户机代码并执行它。
跳转到客户机代码会将宿主机的控制权交给客户机。 当一个线程正在运行客户机代码时,它不能同时处于事件循环中,因为客户机具有(安全)CPU控制权。 通常,客户机代码中花费的时间是有限的,因为对模拟设备寄存器的读取和写入以及其他异常导致我们离开客户机并将控制权交还给QEMU。 在极端情况下,客户机可以花费无限的时间而不放弃控制,这将使QEMU无法响应。
为了解决客户机代码占用的问题,QEMU的控制信号线程用于跳出客户机。 一个UNIX信号从当前的执行流程中转移控制权并调用信号处理函数。 这允许QEMU采取措施来保留客户机代码并返回到QEMU主循环中,在主循环中事件循环可以有机会处理添加的各种事件。
这样做的结果是,如果QEMU当前在客户机代码中,则可能无法立即检测到新事件。 大多数情况下,QEMU最终会处理事件,但这种额外的延迟本身就是一个性能问题。 因此,定时器、I/O完成、从worker threads到核心QEMU的通知都使用信号来确保事件循环立即运行。
你可能想知道具有多个vcpus的事件循环与SMP客户机之间的整体概况。 到此为止已经讲述了线程模型和客户机代码,我们可以讨论整体架构。
5 iothread和non-iothread架构
传统架构是一个QEMU线程,它执行客户机代码和事件循环。 此模型也称为non-iothread或!CONFIG_IOTHREAD,并且是使用./configure && make构建QEMU时的默认模式。 QEMU线程执行客户机代码,直到异常或信号产生要求控制权。 然后它在select(2)中运行事件循环的一次迭代而不阻塞,之后它会重新回到客户机代码并重复以上过程,直到QEMU关闭。
如果使用-smp 2以多个vcpus启动客户机机,则不会创建其他QEMU线程。 而是单个QEMU线程在两个vcpus执行客户机代码和事件循环之间进行多路复用。 因此,non-iothread架构无法利用多核宿主机,并且可能导致SMP客户机性能低下。
请注意,尽管只有一个QEMU线程,但可能存在零个或多个worker threads。 这些线程可能是暂时的或永久的。 请记住,他们执行专门的任务,不执行客户机代码或处理事件。 我想强调这一点,因为在模拟客户机并将其解释为vcpu线程时,很容易被worker threads混淆。 请记住,non-iothread架构只有一个QEMU线程。
较新的体系结构是每个vcpu一个QEMU线程加上一个专用的事件循环线程。 此模型称为iothread架构或CONFIG_IOTHREAD,可以在构建时使用./configure –enable-io-thread启用。 iothread运行事件循环的同时,每个vcpu线程可以并行执行客户机代码,提供真正的SMP支持。 核心QEMU代码永远不会同时运行的规则通过全局互斥锁来维护,该互斥锁在vcpus和iothread之间同步核心QEMU代码。 大多数情况下,vcpus将执行客户机代码,而不需要持有全局互斥锁。 大多数情况下,iothread在select(2)中被阻塞,并且不需要持有全局互斥锁。
请注意,TCG不是线程安全的,因此即使在iothread模型下,它也会在单个QEMU线程中复用vcpus。 只有KVM可以利用per-vcpu线程。
备注:
QEMU的主要线程:
(1)主线程(main_loop),一个
(2)vCPU线程,一个或者多个
(3)I/O线程(aio),一个或者多个
(4)worker thread(VNC/SPICE),一个