本文转自https://www.ibm.com/developerworks/community/blogs/5144904d-5d75-45ed-9d2b-cf1754ee936a/entry/20161222?lang=en
本文面向开发者,尝试在QEMU的工作机制方面做知识的分享,以期对新人贡献者们于学习QEMU代码方面有所裨益。
运行一个客户机时需要处理很多工作,包括执行客户机指令、处理时钟信号、完成I/O操作,以及响应来自监视器的命令。这些工作要能够并发进行,QEMU不能因为处理一个耗时很长的磁盘I/O请求或者是监视器命令,就暂停客户机指令的执行。这要求在架构层次上能够安全的协调资源的使用。处理这种多源事件,现在有两种流行的程序架构:
QEMU实际上使用的是将事件驱动机制和线程相结合的一种混合架构。我们知道,处理事件的主循环只在一个线程上执行,它并不能够发挥多核CPU的优势,所以混合架构有其必然的合理性。另外,相比于把所有工作的分发处理都集成到事件驱动架构中,有时候用专用的线程去执行专门的任务会更简单。当然,QEMU的大多数代码运行还是受事件驱动的,QEMU核心程序基于事件驱动的模型。
一、QEMU事件驱动机制的核心
基于事件驱动的架构以事件处理循环为中心,分发事件到对应的处理函数。QEMU的主循环main_loop_wait()主要完成以下任务:
当一个文件描述符变的就绪时,一个定时器到期,或者一个BH被安排执行时,事件循环会调用对应于该事件的回调函数。回调函数要保证满足如下两条规定:
目前QEMU中的一些代码并没有符合上述第二条规定的要求。比如,主循环中有一个嵌套的qemu_aio_wait()循环,它会等待 主循环也会处理的一些事件。这些违规的代码将通过代码重构被清理掉。新的代码将严格依照规定来审核。如果新代码中需要进行阻塞或者耗时的操作,一个供选择的方案是将这些操作卸载到专用的工作者线程上执行。
二、卸载专门任务到工作者线程
尽管很多I/O操作可以非阻塞的完成,有一些系统调用是没有非阻塞的替代方法可用的。而且有些耗时的计算操作就是需要简单粗暴地占用CPU,我们也很难把它们分解为回调函数。以上这些情况,我们可以使用专用的工作者线程,把这些工作移出到QEMU核心程序之外。
一个使用工作者线程的例子可以参见posix-aio-compat.c(一个文件异步I/O的实现)。当QEMU核心程序发起一个aio请求时,它把该请求加入请求队列;工作者线程负责从队列中取出并执行请求。因为工作者线程是独立的线程,不会阻塞QEMU其它的部分,所以它可以执行阻塞操作。 这种实现方式要求小心处理工作者线程和QEMU核心程序之间必要的同步和通信问题。
另一个例子可参见ui/vnc-jobs-async.c。该例使用工作者线程进行计算密集型的图象压缩和编码操作。
大多数QEMU核心程序不是线程安全的,所以工作者线程不能直接调用QEMU的核心程序代码。确实有一些简单的工具类函数是线程安全的(如qemu_malloc()),但这仅仅是一些特例。这样就引发了一个问题:工作者线程的事件如何通知到QEMU核心?
当工作者线程有消息要通知给QEMU核心时,可以将一个管道或者qemu_eventfd()文件描述符添加到主事件循环中。工作者线程在该文件描述符上执行一个写操作,就可以导致文件描述符在事件主循环那一端变的可读,进而调用对应的回调函数做后续处理。除此之外,为了保证在任何场景下事件主循环都能得到执行,我们需要引入信号量机制。posix-aio-compat.c这个例子中也应用了该机制。当我们了解了客户机指令是如何被执行的之后,会更加地理解这种做法的合理性!详见下文。
三、执行客户机指令
前文中我们了解了事件处理循环及其在QEMU扮演的角色,现在再看一下同样重要的另一部分——如何执行客户机指令。这两部分对QEMU发挥作用来说缺一不可。
QEMU提供两种机制来执行客户机指令:Tiny Code Generator (TCG)和KVM。TCG使用动态二进制翻译技术(又称即时编译)模拟执行客户机指令。KVM则利用现代AMD和Intel CPU(篇者按:其他架构的CPU也有提供类似功能,如s390上的SIE指令扩展)提供的虚拟化扩展功能,在宿主机的CPU上直接安全地执行客户机的指令。对本文来说,具体哪种技术其实并不重要,重要的是无论TCG还是KVM都允许我们跳转到,并且执行客户机指令。
跳转进入到客户机指令,会导致程序的执行控制权交给客户机。因为客户机有CPU的控制和使用权,所以在同一时刻,一个线程不能既运行客户机指令,又在事件处理循环中。通常情况下,执行客户机指令占用的时间都很短,因为对模拟设备寄存器的读写操作,或者是其它的异常情况都会导致我们从客户机指令的执行中退出,并把CPU的控制权重新交还给QEMU。极端情况下,一个客户机会占用CPU很久的时间,这时候QEMU对用户操作会表现为没有响应。
为了解决客户机指令长时间霸占控制线程的问题,QEMU使用信号量来打断客户机的执行。UNIX的信号量机制会打断当前的程序运行流程,抢占控制权,并且触发信号处理函数的执行。通过应用这种机制,QEMU可以从客户机运行中返回到主循环,从而允许事件处理循环有机会对未决的事件进行处理。
当一个新的事件到来时,如果QEMU正在执行客户机指令,事件可能得不到立即的响应。经过一段延时后,QEMU才能最终对这些事件进行处理,这样就产生了一个性能问题。为了解决这个问题,QEMU中的定时器,I/O的完成,还有从工作者线程到QEMU核心的消息,都要使用信号量来保证事件循环能立即得到运行。
读者诸君可能会疑惑,事件处理循环和使用多个vCPU的SMP客户机在这个架构中的关系是一个什么概貌。上文已经介绍了线程模型和客户机指令,现在我们可以试着讨论一下这个问题了!下面来看一下整体架构。
四、iothread和non-iothread架构
QEMU的传统架构是单线程的:在QEMU线程中既执行客户机指令又运行事件循环。这个模型称为non-iothread,或者“!CONFIG_IOTHREAD”。它是在编译阶段,QEMU默认配置使用的模型。QEMU线程会一直执行客户机指令,直到异常产生或者通过信号量机制被收走了控制权。接着,QEMU会在事件循环中通过执行非阻塞的select(2)进行一次循环的迭代。之后就再次进行客户机指令的执行,并不停重复以上过程直到QEMU关闭。
如果用户在启动客户机时指定使用多个vCPU(例,使用“-smp 2”作为启动参数),QEMU并不会创建额外的线程。相反,QEMU线程会被两个vCPU复用来进行客户机指令执行和事件处理。因此,non-iothread模型不能利用宿主机的多核能力,在运行SMP客户机的情况下会表现不佳。
请注意,QEMU线程只有一个,工作者线程可能有零个或者多个。工作者线程有临时的,也有永久存在的。请谨记,工作者线程是用来执行专门的任务的,它们不能执行客户机指令,也不能处理事件。之所以强调这些,是因为读者在监视宿主机上的线程时,可能很容易地把工作者线程错误地当成vCPU线程。我们需要牢记:non-iothread有且只有一个QEMU线程。
新的架构为每一个vCPU分配一个QEMU线程,以及一个专用的事件处理循环线程。这个模型称为iothread,或者“CONFIG_IOTHREAD”。在编译阶段可以通过“./configure —enable-io-thread”启用。各个vCPU线程可以并行的执行客户机指令,进而提供真正的SMP支持;iothread则负责运行事件处理循环。前文中已经说明过QEMU核心代码是非线程安全的,为了能在各个vCPU和iothread线程之间正确同步,QEMU使用了一个全局的mutex互斥锁。大多数时间里,vCPU在运行客户机指令,iothread则阻塞在select(2)上,因此它们都不太常持有该锁。
需要注意的是,TCG不是线程安全的,即使在iothread模型下运行时,它仍然是在多个vCPU之间复用单QEMU线程。只有使用KVM时,每个vCPU才使用各自的线程。
五、结语
希望本文对读者理解QEMU的整体架构有所帮助。
受历史局限性,本文中提到的技术细节在未来的日子里很可能会发生变化。我个人很希望能够看到QEMU在之后的演进中能把CONFIG_IOTHREAD作为默认设置,甚至能够把!CONFIG_IOTHREAD移除。
参考资料:
http://blog.vmsplice.net/2011/03/qemu-internals-overall-architecture-and.html