- QEMU/KVM源码解析与应用
- 李强编著
- 5364字
- 2021-08-24 11:53:39
第2章 QEMU基本组件
2.1 QEMU事件循环机制
2.1.1 glib事件循环机制
“一切皆文件”是UNIX/Linux的著名哲学理念,Linux中的具体文件、设备、网络socket等都可以抽象为文件。内核中通过虚拟文件系统(Virtual File System,VFS)抽象出一个统一的界面,使得访问文件有统一的接口。Linux通过fd来访问一个文件,应用程序也可以调用select、poll、epoll系统调用来监听文件的变化。QEMU程序的运行即是基于各类文件fd事件的,QEMU在运行过程中会将自己感兴趣的文件fd添加到其监听列表上并定义相应的处理函数,在其主线程中,有一个循环用来处理这些文件fd的事件,如来自用户的输入、来自VNC的连接、虚拟网卡对应tap设备的收包等。这种事件循环机制在Windows系统或者其他GUI应用中非常常见。QEMU的事件循环机制基于glib,glib是一个跨平台的、用C语言编写的若干底层库的集合。本节对glib提供的事件循环机制进行简单介绍。
glib实现了完整的事件循环分发机制,在这个机制中有一个主循环负责处理各种事件,事件通过事件源描述,事件源包括各种文件描述符(文件、管道或者socket)、超时和idle事件等,每种事件源都有一个优先级,idle事件源在没有其他高优先级的事件源时会被调度运行。应用程序可以利用glib的这套机制来实现自己的事件监听与分发处理。glib使用GMainLoop结构体来表示一个事件循环,每一个GMainLoop都对应有一个主上下文GMainContext。事件源使用GSource表示,每个GSource可以关联多个文件描述符,每个GSource会关联到一个GMainContext,一个GMainContext可以关联多个GSource。
glib的一个重要特点是能够定义新的事件源类型,可以通过定义一组回调函数来将新的事件源添加到glib的事件循环框架中。新的事件源通过两种方式跟主上下文交互。第一种方式是GSourceFuncs中的prepare函数可以设置一个超时时间,以此来决定主事件循环中轮询的超时时间;第二种方式是通过g_source_add_poll函数来添加fd。
glib主上下文的一次循环包括prepare、query、check、dispatch四个过程,分别对应glib的g_main_context_prepare()、g_main_context_query()、g_main_context_check()以及g_main_context_dispatch()四个函数,其状态转换如图2-1所示。
图2-1 glib事件循环状态转换图
下面简单介绍这几个步骤:
1)prepare:通过g_main_context_prepare()会调用事件对应的prepare回调函数,做一些准备工作,如果事件已经准备好进行监听了,返回true。
2)query:通过g_main_context_query()可以获得实际需要调用poll的文件fd。
3)check:当query之后获得了需要进行监听的fd,那么会调用poll对fd进行监听,当poll返回的时候,就会调用g_main_context_check()将poll的结果传递给主循环,如果fd事件能够被分派就会返回true。
4)dispatch:通过g_main_context_dispatch()可以调用事件源对应事件的处理函数。
上面就是glib事件循环机制的处理流程,应用程序需要做的就是把新的事件源加入到这个处理流程中,glib会负责处理事件源上注册的各种事件。
2.1.2 QEMU中的事件循环机制
QEMU的事件循环机制如图2-2所示。QEMU在运行过程中会注册一些感兴趣的事件,设置其对应的处理函数。如对于VNC来说,会创建一个socket用于监听来自用户的连接,注册其可读事件为vnc_client_io,当VNC有连接到来时,glib的框架就会调用vnc_client_io函数。除了VNC,QEMU中还会注册很多其他事件监听,如网卡设备的后端tap设备的收包,收到包之后QEMU调用tap_send将包路由到虚拟机网卡前端,若虚拟机使用qmp,那么在管理界面中,当用户发送qmp命令过来之后,glib会调用事先注册的tcp_chr_accept来处理用户的qmp命令。本节将分析QEMU的事件循环实现。关于QEMU的事件循环机制,Fam Zheng在KVM Forum 2015上有一个非常不错的演讲,题为“Improving the QEMU Event Loop”,读者可以自行搜索学习。
图2-2 QEMU事件循环机制
可以通过如下命令启动虚拟机。
在此命令行下启动的QEMU程序,其主循环事件总共包含了图2-3所示的5个事件源,其中前面两个qemu_aio_context和iohander_ctx都是类型为AioContext的自定义事件源,中间两个VNC的事件源是glib标准事件源,最后一个不是QEMU通过调用g_source_attach添加的事件源,而是glib内部库自己使用的加入到事件循环的fd。qemu_aio_context和iohandler_ctx是两个比较特殊的自定义的类型为AioContext的事件源,前者主要用于处理QEMU中块设备相关的异步I/O请求通知,后者用于处理QEMU中各类事件通知,这些事件通知包括信号处理的fd、tap设备的fd以及VFIO设备对应的中断通知等。glib中事件源可以添加多个事件fd,对应的AioContext表示为每一个fd在AioContext都有记录,glib框架在执行iohandler_ctx的分发函数时,会遍历其上所有的fd,如果某个fd上的数据准备好了,就会调用相应的回调函数。这里需要注意,每一个事件源本身都会有一个fd,当添加一个fd到事件源时,整个glib主循环都会监听该fd。以前述命令为例,QEMU主循环总共会监听6个fd,其中5个是事件源本身的fd,还有一个是通过系统调用SYS_signalfd创建的用来处理信号的fd,图2-3中的tap设备fd只是作为一个例子,在上述命令行下并不会添加该fd。任何一个fd准备好事件之后都可以唤醒主循环。本节末会对这6个fd的产生及其分析过程进行介绍。
QEMU主循环对应的最重要的几个函数如图2-4所示。QEMU的main函数定义在vl.c中,在进行好所有的初始化工作之后会调用函数main_loop来开始主循环。
图2-3 QEMU事件源实例
图2-4 QEMU主循环对应的函数
main_loop及其调用的main_loop_wait的主要代码如下。main_loop_wait函数调用了os_host_main_loop_wait函数,在后者中可以找到对应图2-4的相关函数,即每次main_loop循环的3个主要步骤。main_loop_wait在调用os_host_main_loop_wait前,会调用qemu_soonest_timeout函数先计算一个最小的timeout值,该值是从定时器列表中获取的,表示监听事件的时候最多让主循环阻塞的事件,timeout使得QEMU能够及时处理系统中的定时器到期事件。
QEMU主循环的第一个函数是glib_pollfds_fill,下面的代码显示了该函数的工作流程。该函数的主要工作是获取所有需要进行监听的fd,并且计算一个最小的超时时间。首先调用g_main_context_prepare开始为主循环的监听做准备,接着在一个循环中调用g_main_context_query获取需要监听的fd,所有fd保存在全局变量gpollfds数组中,需要监听的fd的数量保存在glib_n_poll_fds中,g_main_context_query还会返回fd时间最小的timeout,该值用来与传过来的cur_timeout(定时器的timeout)进行比较,选取较小的一个,表示主循环最大阻塞的时间。
os_host_main_loop_wait在调用glib_pollfds_fill之后就完成了图2-4的第一步,现在已经有了所有需要监听的fd了,然后会调用qemu_mutex_unlock_iothread释放QEMU大锁(Big Qemu Lock,BQL),BQL会在本章第2节“QEMU线程模型”中介绍,这里略过。接着os_host_main_loop_wait函数会调用qemu_poll_ns,该函数代码如下。它接收3个参数,第一个是要监听的fd数组,第二个是fds数组的长度,第三个是一个timeout值,表示g_poll最多阻塞的时间。qemu_poll_ns在配置CONFIG_PPOLL时会调用ppoll,否则调用glib的函数g_poll,g_poll是一个跨平台的poll函数,用来监听文件上发生的事件。
qemu_poll_ns的调用会阻塞主线程,当该函数返回之后,要么表示有文件fd上发生了事件,要么表示一个超时,不管怎么样,这都将进入图2-4的第三步,也就是调用glib_pollfds_poll函数进行事件的分发处理,该函数的代码如下。glib_pollfds_poll调用了glib框架的g_main_context_check检测事件,然后调用g_main_context_dispatch进行事件的分发。
下面以虚拟机的VNC连接为例分析相应的函数调用过程。VNC子模块在初始化的过程中会在vnc_display_open中调用qio_channel_add_watch,设置其监听的回调函数为vnc_listen_io,该过程最终会创建一个回调函数集合为qio_channel_fd_source_funcs的事件源,其中的dispatch函数为qio_channel_fd_source_dispatch,该函数会调用vnc_listen_io函数。
以本小节最开始的命令启动虚拟机,然后在vnc_listen_io处下断点,使用VNC客户端连接虚拟机,QEMU进程会中断到调试器中,使用gdb的bt命令可以看到图2-5所示的函数调用堆栈。
上面是QEMU效仿glib实现的主循环,但主循环存在一些缺陷,比如在主机使用多CPU的情况下伸缩性受到限制,同时主循环使用了QEMU全局互斥锁,从而导致VCPU线程和主循环存在锁竞争,使性能下降。为了解决这个问题,QEMU引入了iothread事件循环,把一些I/O操作分配给iothread,从而提高I/O性能。
图2-5 vnc连接fd的事件处理函数堆栈
2.1.3 QEMU自定义事件源
QEMU自定义了一个新的事件源AioContext,有两种类型的AioContext,第一类用来监听各种各样的事件,比如iohandler_ctx,第二类是用来处理块设备层的异步I/O请求,比如QEMU默认的qemu_aio_context或者模块自己创建的AioContext。这里只关注第一种情况,即事件相关的AioContext。下面的代码列出了AioContext结构中的主要成员。
这里简单介绍一下AioContext中的几个成员。
● source:glib中的GSource,每一个自定义的事件源第一个成员都是GSource结构的成员。
● lock:QEMU中的互斥锁,用来保护多线程情况下对AioContext中成员的访问。
● aio_handlers:一个链表头,其链表中的数据类型为AioHandler,所有加入到AioContext事件源的文件fd的事件处理函数都挂到这个链表上。
● notify_me和notified都与aio_notify相关,主要用于在块设备层的I/O同步时处理QEMU下半部(Bottom Halvs,BH)。
● first_bh:QEMU下半部链表,用来连接挂到该事件源的下半部,QEMU的BH默认挂在qemu_aio_context下。
● notifier:事件通知对象,类型为EventNotifier,在块设备进行同步且需要调用BH的时候需要用到该成员。
● tlg:管理挂到该事件源的定时器。
剩下的结构与块设备层的I/O同步相关,这里略过。
AioContext拓展了glib中source的功能,不但支持fd的事件处理,还模拟内核中的下半部机制,实现了QEMU中的下半部以及定时器的管理。
接下来介绍AioContext的相关接口,这里只以文件fd的事件处理为主,涉及AioContext与块设备层I/O同步的代码会省略掉。首先是创建AioContext函数的aio_context_new,该函数的核心调用如下。
aio_context_new函数首先创建分配了一个AioContext结构ctx,然后初始化代表该事件源的事件通知对象ctx->notifier,接着调用了aio_set_event_notifier用来设置ctx->notifier对应的事件通知函数,初始化ctx中其他的成员。
aio_set_event_notifier函数调用了aio_set_fd_handler函数,后者是另一个重要的接口函数,其作用是添加或者删除事件源中的一个fd。如果作用是添加,则会设置fd对应的读写函数,aio_set_fd_handler即可用于从AioContext中删除fd,也可以用于添加fd,下面的代码去掉了删除事件源中fd监听处理的步骤,其代码如下。
aio_set_fd_handler的第一个参数ctx表示需要添加fd到哪个AioContext事件源;第二个参数fd表示添加的fd是需要在主循环中进行监听的;is_external用于块设备层,对于事件监听的fd都设置为false;io_read和io_write都是对应fd的回调函数,opaque会作为参数调用这些回调函数。
aio_set_fd_handler函数首先调用find_aio_handler查找当前事件源ctx中是否已经有了fd,考虑新加入的情况,这里会创建一个名为node的AioHandler,使用fd初始化node->pfd.fd,并将其插入到ctx->aio_handlers链表上,调用glib接口g_source_add_poll将该fd插入到了事件源监听fd列表中,设置node事件读写函数为io_read,io_write函数,根据io_read和io_write的有无设置node->pfd.events,也就是要监听的事件。aio_set_fd_handler调用之后,新的fd事件就加入到了事件源的aio_handlers链表上了,如图2-6所示。
图2-6 AioContext的aio_handlers链表
aio_set_fd_handler函数一般被块设备相关的操作直接调用,如果仅仅是添加一个普通的事件相关的fd到事件源,通常会调用其封装函数qemu_set_fd_handler,该函数将事件fd添加到全部变量iohandler_ctx事件源中。
glib中自定义的事件源需要实现glib循环过程中调用的几个回调函数,QEMU中为AioContext事件源定义了名为aio_source_funcs的GSourceFuns结构。
这几个函数是自定义事件源需要实现的,这里介绍一下最重要的事件处理分派函数aio_ctx_dispatch。aio_ctx_dispatch代码如下,其会调用aio_dispatch,aio_dispatch要完成3件事:第一是BH的处理,第二是处理文件fd列表中有事件的fd,第三是调用定时器到期的函数。这里分析一下文件fd的处理部分。
aio_dispatch_handlers函数会遍历aio_handlers,遍历监听fd上的事件是否发生了。fd发生的事件存在node->pfd.revents中,注册时指定需要接受的事件存放在node->pfd.events中,revents变量保存了fd接收到的事件。对应G_IO_IN可读事件来说,会调用注册的fd的io_read回调,对G_IN_OUT可写事件来说,会调用注册的fd的io_write函数。当然,如果当前的fd已经删除了,则会删除这个节点。
2.1.4 QEMU事件处理过程
上一节介绍了QEMU的自定义事件源,本节以signalfd的处理为例介绍QEMU事件处理的过程。signalfd是Linux的一个系统调用,可以将特定的信号与一个fd绑定起来,当有信号到达的时候fd就会产生对应的可读事件。以如下命令启动虚拟机。
在sigfd_handler函数下断点,在另一个终端向QEMU发送SIGALARM信号,命令如下,其中2762是QEMU进程号。
在第一个命令行的中断中可以看到QEMU进程已经在sigfd_handler函数被中断下来,图2-7显示了此时的函数调用情况,从中可以看到整个过程调用了glib的事件分发函数g_main_context_dispatch,然后调用了AioContext自定义事件源的回调函数aio_ctx_dispatch,最终调用到QEMU为信号注册的可读回调函数sigfd_handler。
图2-7 sigfd_handler函数的栈回溯
下面对这个过程进行简单分析,首先分析signal事件源的初始化。vl.c中的main函数会调用qemu_init_main_loop进行AioContext事件源的初始化,该函数代码如下。
qemu_init_main_loop函数调用qemu_signal_init将一个fd与一组信号关联起来,qemu_signal_init调用了之前提到的qemu_set_fd_handler函数,设置该signalfd对应的可读回调函数为sigfd_handler。qemu_set_fd_handler在首次调用时会调用iohandler_init创建一个全局的iohandler_ctx事件源,这个事件源的作用是监听QEMU中的各类事件。最终qemu_signal_init会在iohandlers_ctx的aio_handlers上挂一个AioHandler节点,其fd为这里的signalfd,其io_read函数为这里的sigfd_handler。
qemu_init_main_loop函数接着会调用aio_context_new创建一个全局的qemu_aio_context事件源,这个事件源主要用于处理BH和块设备层的同步使用。
最后,该函数调用aio_get_g_source和iohandler_get_g_source分别获取qemu_aio_context和iohandler_ctx的GSource,以GSource为参数调用g_source_attach两个AioContext加入到glib的主循环中去。
将信号对应的fd加入事件源以及将事件源加入到glib的主循环之后,QEMU就会按照2.1.2节所述,在一个while循环中进行事件监听。当使用kill向QEMU进程发送SIGALARM信号时,signalfd就会有可读信号,从而导致glib的主循环返回调用g_main_context_dispatch进行事件分发,这会调用到aio_ctx_dispatch,最终会调用到qemu_signal_init注册的可读处理函数sigfd_handler。
2.1.5 QEMU主循环监听的fd解析
2.1.2节中介绍了QEMU的事件循环机制,并且在随后的几节中介绍了与事件循环机制相关的源码,本节将实际分析QEMU主事件循环监听的fd来源。首先以如下命令启动虚拟机。
为了方便稍后的叙述,这里再把glib_pollfds_fill的代码和行号列在图2-8中:
图2-8 glib_pollfds_fill函数源码
使用gdb在第200行下断点。
输入“r”让QEMU进程运行起来。
gpollfds是一个数组,存着所有需要监听的fd,其成员类型为pollfd,成员都存放在gpollfds.data中,所以这里可以判断到底监听了哪些fd。图2-9显示了所有监听的fd,总共有6个fd,分别是4、6、8、9、e、f。
图2-9 QEMU监听的fd
从图2-9可以看出来,第一个fd 4是在monitor_init_globals初始化调用iohandler_init并创建iohander_ctx时调用的,其本身对应iohander_ctx中的事件通知对象的fd。gdb继续输入“c”让程序运行起来,在随后的g_source_add_poll断点中可以看到6、8、e、f这几个fd的来源。6是调用qemu_signal_init创建signalfd对应的fd, 8是qemu_aio_context对应的fd, e和f是vnc创建的fd。但是没有fd 9的信息。
找到QEMU对应的进程id,查看/proc/目录下该QEMU进程对应fd情况,如图2-10所示。这里可以看到fd 9是一个eventfd,其虽然在glib事件循环监听中,但是其并没有通过g_source_add_poll加入。
图2-10 QEMU进程fd分布
在eventfd函数下断点,每次停下来之后在gdb中输入finish命令完成当前函数的执行,然后查看rax寄存器的值,当其是9的时候查看堆栈,结果如图2-11所示。从中可以看出,fd 9这个eventfd是由glib库自己创建使用的。
这样,glib监听的6个fd就搞清楚了。当然,如果给QEMU提供不同的参数,其监听的fd也会随着变化。
图2-11 fd 9注册过程