QEMU深入浅出: 整体架构及线程模型

原文链接: https://www.ibm.com/developerworks/community/blogs/5144904d-5d75-45ed-9d2b-cf1754ee936a/entry/20161222?lang=en

本文转自https://www.ibm.com/developerworks/community/blogs/5144904d-5d75-45ed-9d2b-cf1754ee936a/entry/20161222?lang=en

本文面向开发者,尝试在QEMU的工作机制方面做知识的分享,以期对新人贡献者们于学习QEMU代码方面有所裨益。

运行一个客户机时需要处理很多工作,包括执行客户机指令、处理时钟信号、完成I/O操作,以及响应来自监视器的命令。这些工作要能够并发进行,QEMU不能因为处理一个耗时很长的磁盘I/O请求或者是监视器命令,就暂停客户机指令的执行。这要求在架构层次上能够安全的协调资源的使用。处理这种多源事件,现在有两种流行的程序架构:

  • 基于线程的架构:将工作分配到不同的进程或者线程中执行。
  • 基于事件驱动的架构:由一个主程序循环把事件分配到各自的处理函数处理。这种架构一般使用select(2)或者poll(2) 系列的系统调用通过监视多个文件描述符的变化来实现。

QEMU实际上使用的是将事件驱动机制和线程相结合的一种混合架构。我们知道,处理事件的主循环只在一个线程上执行,它并不能够发挥多核CPU的优势,所以混合架构有其必然的合理性。另外,相比于把所有工作的分发处理都集成到事件驱动架构中,有时候用专用的线程去执行专门的任务会更简单。当然,QEMU的大多数代码运行还是受事件驱动的,QEMU核心程序基于事件驱动的模型。

一、QEMU事件驱动机制的核心

基于事件驱动的架构以事件处理循环为中心,分发事件到对应的处理函数。QEMU的主循环main_loop_wait()主要完成以下任务:

  •  等待文件描述符就绪(可读或可写)。鉴于文件、套接字、管道,还有各种其他的资源都属于文件描述符,文件描述符非常关键。在QEMU中可以使用qemu_set_fd_handler()添加需要主循环等待的文件描述符。
  • 处理到期的定时器(Timer)。定时器可以通过qemu_mod_timer()添加。
  • 处理下半部程序(BH)。下半部程序像是会立刻到期的定时器,用来避免调用栈的重入和溢出。BH可以使用qemu_bh_schedule()添加。

当一个文件描述符变的就绪时,一个定时器到期,或者一个BH被安排执行时,事件循环会调用对应于该事件的回调函数。回调函数要保证满足如下两条规定:

  • 控制回调函数执行的代码运行在单线程上;回调函数循序地,原子地执行。同一时刻只会有一个核心回调函数在执行,所以函数中不需要进行同步处理。
  • 事件主循环在处理后续事件之前,会一直等待当前回调函数的返回,所以回调函数中不能执行阻塞的系统调用,或者耗时长的计算操作。回调函数必须要避免占用CPU太长时间,否则的话客户机会表现为执行暂停,或者监视器表现为没有响应。

目前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

你可能感兴趣的:(QEMU深入浅出: 整体架构及线程模型)