一篇很不错的文章,所以翻译过来和大家分享,因为本人英语并不太好,所以有错误的地方敬请谅解指正。


=======================分割线==========================

这是针对开发者的QEMU内在系列的第一篇文章。本文旨在分享QEMU如何工作的相关知识以及帮助那些新的贡献者更简单的学习QEMU的代码。


执行一个guest(系统)包括执行guest代码、处理timer、处理I/O以及响应管理器的命令。要完成所有这些工作需要有一个很强的架构来以一个很安全的方式操控所有资源,使得如果当一个硬盘I/O或者管理器命令的执行花费很长时间才能完成时不会终止对guest的执行。对于这种需要回应从多重资源发来的事件的程序来说,现行有两种比较流行的架构:

    1. Parallel architecture(平行架构)把那些可以同时执行的工作分成多个进程或是线程。我叫他线程化的架构(threaded architecture)。

    2. Event-driven architecture(事件驱动架构)通过执行一个主循环来发送事件到handler以此对事件做反馈处理。这一方法通常通过使用select(2)或者poll(2)系列的系统调用等待多重文件描述符的方式来实现。


QEMU事实上使用一种混合架构,这种架构把事件驱动和线程组合在一起。这种做法之所以有效是因为只在单个线程上执行的事件循环不能有效利用底层多核心的硬件。再则,有时候使用一个专用线程来减少特定工作的负担要比把它整合在一个事件驱动的架构中更简单有效。虽然如此,QEMU的核心还是事件驱动的,大多数代码都是在这样一个环境中执行的。


QEMU的事件驱动核心

一个事件驱动的架构是以一个派发事件到处理函数的循环为核心的。

QEMU的主事件循环是main_loop_wait(),它主要完成以下工作:

    1. 等待文件描述符变成可读或可写。文件描述符是一个关键角色,因为files、sockets、pipes以及其他各种各样的资源都是文件描述符(file descriptors)。文件描述符的增加方式:qemu_set_fd_handler()。

    2. 处理到期的定时器(timer)。定时器的添加方式:qemu_mod_timer()。

    3. 执行bottom-halves(BHs),它和定时器类似会立即过期。BHs用来放置回调函数的重入和溢出。BHs的添加方式:qemu_bh_schedule()。


当一个文件描述符准备好了、一个定时器过期或者是一个BH被调度到时,事件循环就会调用一个回调函数来回应这些事件。回调函数对于它们的环境有两条规则:

    1. 没有其他核心同时在执行,所以不需要考虑同步问题。对于核心代码来说,回调函数是线性和原子执行的。在任意给定的时间里只有一个线程控制执行核心代码。

    2. 不应该执行可阻断系统调用或是长运行计算(long-running computations)。由于事件循环在继续其他事件时会等待当前回调函数返回,所以如果违反这条规定会导致guest暂停并且使管理器变的无响应。

第二条规定有时候很难遵守,在QEMU中会有代码会被阻塞。事实上,qemu_aio_wait()里面还有嵌套循环,它会等待那些顶层事件循环正在处理的事件的子集。庆幸的是,这些违反规则的部分会在未来重新架构代码时被移除。新代码几乎没有合理的理由被阻塞,而解决方法之一就是使用专属的工作线程来卸下(offload)这些长执行或者会被阻塞的代码。


卸下特殊的任务到工作线程

尽管很多I/O操作可以以一种非阻塞的形式执行,但有些系统调用却没有非阻塞的替代方式。再者,长运行的计算单纯的霸占着CPU并且很难被分割到回调函数中。在这种情况下专属的工作线程就可以用来小心的将这些任务移出核心QEMU。


在posix-aio-compat.c中有一个工作线程的例子,一个异步的文件I/O实现。当核心QEMU放出一个aio请求,这个请求被放到一个队列总。工作线程从队列中拿出这个请求,并在核心QEMU中执行它。它们可能会有阻塞的动作,但因为它们在它们自己的线程中执行所以并不会阻塞剩余的QEMU执行。这个实现对于必要的同步以及工作线程和核心QEMU的通信有小心的处理。

另一个例子是ui/vnc-jobs-async.c中将计算密集型的镜像解压缩和解码移到工作线程中。


因为核心QEMU的主要部分不是线程安全的,所以工作线程不能调用到核心QEMU的代码。当然简单的使用类似qemu_malloc()的函数是线程安全的,这些是例外,而不在规则之内。这也引发了工作线程如何将事件传回核心QEMU的问题。


当一个工作线程需要通知核心QEMU时,一个管道或者一个qemu_eventfd()文件描述符将被添加到事件循环中。工作线程可以向文件描述符中写入,而当文件描述符变成可读时,事件循环会调用回调函数。另外,必须使用信号来确保事件循环可以在任何环境下执行。这种方式在posix-aio-compat.c中被使用,而且在了解guest代码如何被执行之后变的更有意义。


执行guest代码

目前为止我们已经大概的看了一下QEMU中的事件循环和它的主要规则。其中执行guest代码的能力是特别重要的,少了它,QEMU可以响应事件但不会非常有用。


这里有两种方式用来执行guest代码:Tiny Code Generator(TCG)和KVM。TCG通过动态二进制转化(dynamic binary translation)来模拟guest,它也以即时编译(Just-in-Time compilation)被熟知。而KVM则是利用现有的现代intel和AMD CPU中硬件虚拟化扩展来直接安全的在host CPU上执行guest代码。在这篇文章中,真正重要的并不是实际的技术,不管是TCG还是KVM都允许我们跳转到guest代码中并且执行它。


跳入guest代码中会使我们失去对程序执行的控制而把控制交给guest。而一个正在执行guest代码的线程不能同时处在事件循环中,因为guest控制着CPU。一般情况下,花在guest代码中的时间是有限的。因为对于被模拟设备的寄存器的读写和其他异常导致我们离开guest而把控制交还给QEMU。在极端的情况下一个guest可以花费无限制的时间而不放弃控制权,而这会引起QEMU无响应。


为了解决guest代码霸占问题,QEMU线程使用信号来跳出guest。一个UNIX信号从当前的执行流程中抓取控制权并调用一个信号处理函数。这使得QEMU得以采取措施来离开guest代码并返回它的主循环,因而事件循环才有机会处理待解决的事件。


上述的结果是新事件可能第一时间被发觉如果QEMU当前正在guest代码中。事实上QEMU大多数时间规避处理事件,但因而产生的额外的延迟也成为的效能问题。因此,到核心QEMU的I/O结束和从工作线程来的通知使用信号来确保事件循环会被立即处理。


你可能会疑惑说到底事件循环和有多核心的SMP guest之间的架构图会是什么样子的。而现在,线程模型和guest代码都已经提到了,现在我们来讨论整体架构。


IOTHREAD和NON-IOTHREAD线程架构

传统的架构是单个QEMU线程来执行guest代码和事件循环。这个模型就是所谓的non-iothread或者说!CONFIG_IOTHREAD,它是QEMU默认使用./configure && make的设置。QEMU线程执行guest代码直到一个异常或者信号出现才回到控制器。然后它在select(2)不被阻塞的情况执行一次事件循环的一次迭代。然后它又回到guest代码中并重复上述过程直到QEMU被关闭。


如果guest使用,例如-smp 2,以启动一个多vcpu启动,也不会有多的QEMU线程被创建。取而代之的是在单个QEMU线程中多重执行两个vcpu和事件循环。因而non-iothread不能很好利用多核心的host硬件,而使得对SMP guest的模拟性能很差。


需要注意的是,虽然只有一个QEMU线程,但可能会有0或多个工作线程。这些线程可能是临时的也可能是永久的。记住这些工作线程只执行特殊的任务而不执行guest代码也不处理事件。我之说以要强调这个是因为当监视host时很容易被工作线程迷惑而把他们当做vcpu线程来中断。记住,non-iothread只有一个QEMU线程。


一种更新的架构是每个vcpu一个QEMU线程外加一个专用的事件循环线程。这个模型被定义为iothread或者CONFIG_IOTHREAD,它可以通过./configure --enable-io-thread在创建时开启。每个vcpu线程可以平行的执行guest代码,以此提供真正的SMP支持。而iothread执行事件循环。核心QEMU代码不能同时执行的规则通过一个全局互斥来维护,并通过该互斥锁同步vcpu和iothread间核心QEMU代码。大多数时候vcpu线程在执行guest代码而不需要获取全局互斥锁。大多数时间iothread被阻塞在select(2)因而也不需要获取全局互斥锁。


注意,TCG不是线程安全的,所以即使在在iothread模式下,它还是在一个QEMU线程中执行多个vcpu。只有KVM可以真正利用每个vcpu一个线程的优势。


结论以及对未来的展望

希望这篇文章能帮助交流QEMU(继承KVM)的整体架构。请随意在下面提问或评论。


在未来,细节部分会改变,而我希望看到把CONFIG_IOTHREAD作为默认设置甚至移除!CONFIG_IOTHREAD

我会随着qemu.git的改变而更新这篇文章。


=================翻译结束=======================

对于最后原作者对未来的展望,从我看到的QEMU-1.3的版本的configure来看,!CONFIG_IOTHREAD已经被移除了。