QEMU事件循环机制

2,220 阅读10分钟

QEMU事件循环机制

关注微信公众号:Linux内核拾遗

文章来源:mp.weixin.qq.com/s/-fNJNfynm…

QEMU程序的运行是基于各类文件fd事件的,QEMU在运行过程中会将自己感兴趣的文件fd添加到其监听列表上并定义相应的处理函数,在其主线程中,有一个循环用来处理这些文件fd的事件,如来自用户的输入、来自VNC的连接、虚拟网卡对应tap设备的收包等。QEMU的事件循环机制基于glib,glib是一个跨平台的、用C语言编写的若干底层库的集合。

1 glib事件循环机制

glib实现了完整的事件循环分发机制,在这个机制中有一个主循环负责处理各种事件,事件通过事件源描述,事件源包括各种文件描述符(文件、管道或者socket)、超时和idle事件等,每种事件源都有一个优先级,idle事件源在没有其他高优先级的事件源时会被调度运行。

glib使用GMainLoop结构体来表示一个事件循环,每一个GMainLoop都对应有一个主上下文GMainContext。事件源使用GSource表示,每个GSource可以关联多个文件描述符,每个GSource会关联到一个GMainContext,一个GMainContext可以关联多个GSource。

glib事件循环结构

glib的一个重要特点是能够定义新的事件源类型,可以通过定义一组回调函数来将新的事件源添加到glib的事件循环框架中。因此应用程序可以利用glib的这套机制来实现自己的事件监听与分发处理。

glib主上下文的一次循环包括prepare、query、check、dispatch四个过程,分别对应glib的g_main_context_prepare()、g_main_context_query()、g_main_context_check()以及g_main_context_dispatch()四个函数,其状态转换如下图所示。

image-20230402110841813

  • prepare:通过g_main_context_prepare()会调用事件对应的prepare回调函数,做一些准备工作,如果事件已经准备好进行监听了,返回true。
  • query:通过g_main_context_query()可以获得实际需要调用poll的文件fd。
  • check:当query之后获得了需要进行监听的fd,那么会调用poll对fd进行监听,当poll返回的时候,就会调用g_main_context_check()将poll的结果传递给主循环,如果fd事件能够被分派就会返回true。
  • dispatch:通过g_main_context_dispatch()可以调用事件源对应事件的处理函数。

根据这个glib事件循环机制的处理流程,应用程序需要做的就是把新的事件源加入到这个处理流程中,glib会负责处理事件源上注册的各种事件。

2 QEMU中的事件循环机制

QEMU的事件循环机制如下图所示,QEMU在运行过程中会注册一些感兴趣的事件,设置其对应的处理函数。图示的QEMU主循环中添加了了来自tap设备、qmp以及VNC等的事件源:

  • 当监听到VNC有连接到来时glib框架就会调用vnc_client_io函数来处理。
  • 当网卡设备的后端tap设备接收到网络包后QEMU调用tap_send将包路由到虚拟机网卡前端。
  • 当用户发送qmp命令之后glib会调用tcp_chr_accept来处理qmp命令。

image-20230407154053308

通过如下命令启动虚拟机,并结合该虚拟机来介绍QEMU中的事件循环机制:

 root@ubuntu:~# qemu-system-x86_64 -m 1024 -smp 4 -hda /home/test/test.img --enable-kvm -vnc :0

该命令行启动的QEMU程序,它包含了如图所示的3种/5个事件源:

image-20230407155449980

  • AioContext自定义事件源:qemu_aio_context和iohander_ctx,前者用于处理QEMU中块设备相关的异步I/O请求通知,后者用于处理QEMU中各类事件通知,包括信号处理fd、tap设备的fd以及VFIO设备对应的中断通知等等。
  • glib标准事件源:vnc: GSource这两个VNC事件。
  • glib内部事件fd。

glib中事件源可以添加多个事件fd,每一个事件源本身都会有一个fd,当添加一个fd到事件源时,整个glib主循环都会监听该fd,并且任何一个fd准备好事件之后都可以唤醒主循环。当前例子中,QEMU主循环总共会监听6个fd,其中5个是事件源本身的fd,还有一个是通过系统调用SYS_signalfd创建的用来处理信号的fd。

QEMU主循环对应的最重要的几个函数如下图所示。QEMU的main函数定义在vl.c中,在进行好所有的初始化工作之后会调用函数main_loop来开始主循环。

QEMU主循环对应的函数

main_loop及其调用的main_loop_wait的主要代码如下:

 // vl.c
 static void main_loop(void) {
  ...
   do {
    ...
     last_io = main_loop_wait(nonblocking);
    ...
  } while (!main_loop_should_exit());
 }
 ​
 // main-loop.c
 void main_loop_wait(int nonblocking) {
  ...
   // 计算最小timeout值
   timeout_ns = qemu_soonest_timeout(timeout_ns,
                                    timerlistgroup_deadline_ns(
                                      &main_loop_tlg));
   ret = os_host_main_loop_wait(timeout_ns);
  ...
 }
 ​
 static int os_host_main_loop_wait(int64_t timeout) {
   int ret;
   static int spin_counter;
   
   // 主循环函数1
   glib_poolfds_fill(&timeout);
  ...
   if (timeout) {
     spin_counter = 0;
     qemu_mutex_unlock_iothread();
  } else {
     spin_counter++;
  }
   
   // 主循环函数2
   ret = qemu_poll_ns((GPollFD *)gpollfds->data, gpollfds->len, timeout);
   
   if (timeout) {
     qemu_mutex_lock_iothread();
  }
   
   // 主循环函数3
   glib_pollfds_poll();
   return ret;
 }

main_loop_wait在调用os_host_main_loop_wait前,会调用qemu_soonest_timeout函数先计算一个最小的timeout值,该值是从定时器列表中获取的,表示监听事件的时候最多让主循环阻塞的时间,timeout使得QEMU能够及时处理系统中的定时器到期事件

2.1 glib_pollfds_fill

该函数的主要工作是获取所有需要进行监听的fd,并且计算一个最小的超时时间。

 // main-loop.c
 static void glib_pollfds_fill(int64_t *cur_timeout) {
   // 1. 调用准备函数
   g_main_context_prepare(context, &max_priority);
   n = glib_n_poll_fds;
   do {
     GPollFD* pfds;
     glib_n_poll_fds = n;
     g_array_set_size(gpollfds, glib_pollfds_idx + glib_n_poll_fds);
     pfds = &g_array_index(gpollfds, GPollFD, glib_pollfds_idx);
     // 2. 获取需要监听的fd,并返回fd时间最小的timeout
     n = g_main_context_query(context, max_priority, &timeout, pfds, 
                             glib_n_poll_fds);
  } while (n != glib_n_poll_fds);
  ...
   *cur_timeout = qemu_soonest_timeout(timeout_ns, *cur_timeout);
 }
  1. 首先调用g_main_context_prepare开始为主循环的监听做准备。
  2. 接着在一个循环中调用g_main_context_query获取需要监听的fd,所有fd保存在全局变量gpollfds数组中,需要监听的fd的数量保存在glib_n_poll_fds中,g_main_context_query还会返回fd时间最小的timeout,该值用来与传过来的cur_timeout(定时器的timeout)进行比较,选取较小的一个,表示主循环最大阻塞的时间

2.2 qemu_poll_ns

glib_pollfds_fill调用完成后,此时已经有了所有需要监听的fd了,然后会调用qemu_mutex_unlock_iothread释放QEMU大锁(Big Qemu Lock,BQL)。

接着os_host_main_loop_wait函数会调用qemu_poll_ns,它接受3个参数:

  1. 要监听的fd数组。
  2. fds数组的长度。
  3. timeout值,表示g_poll最多阻塞的时间,这是一个跨平台的poll函数,用来监听文件上发生的事件。如果QEMU配置了CONFIG_PPOLL,那么就qemu_poll_ns会调用ppoll而不是g_poll。
 // qemu-timer.c
 int qemu_poll_ns(GPollFD* fds, guint nfds, int64_t timeout) {
 #ifdef CONFIG_POLL
   if (timeout < 0) {
     return ppoll((struct pollfd*)fds, nfds, NULLNULL);
  } else {
     struct timespec ts;
     int64_t tvsec = timeout / 1000000000LL;
    ...
     ts.tv_sec = tvsec;
     ts.tv_nsec = timeout % 1000000000LL;
     return ppoll((struct pollfd *)fds, nfds, &ts, NULL);
  }
 #else
 return g_poll(fds, nfds, qemu_timeout_ns_to_ms(timeout));
 #endif
 }

qemu_poll_ns的调用会阻塞主线程,当该函数返回之后,要么表示有文件fd上发生了事件,要么表示一个超时,不管怎么样,这都将进入第三步,即调用glib_pollfds_poll函数。

2.3 glib_pollfds_poll

glib_pollfds_poll函数负责对事件进行分发处理。

 // main-lool.c
 static void glib_pollfds_poll(void) {
   GMainContext* context = g_main_context_default();
   GPollFD* fds = &g_array_index(gpollfds, GPollFD, glib_pollfds_idx);
   
   // 检测事件并分发事件
   if (g_main_context_check(context, max_priority, pfds, glib_n_poll_fds)) {
     g_main_context_dispatch(context);
  }
 }

它首先调用了glib框架的g_main_context_check检测事件,然后调用g_main_context_dispatch进行事件的分发。

3 QEMU自定义事件源

QEMU自定义了一个新的事件源AioContext,有两种类型的AioContext:

  • 第一类用来监听各种各样的事件,比如iohandler_ctx。
  • 第二类是用来处理块设备层的异步I/O请求,比如QEMU默认的qemu_aio_context或者模块自己创建的AioContext。

这里只关注第一种情况,即事件相关的AioContext。

AioContext结构体定义如下:

 // include/block/aio.h
 struct AioContext {
     GSource source;
 ​
     /* Used by AioContext users to protect from multi-threaded access. */
     QemuRecMutex lock;
 ​
     /* The list of registered AIO handlers */
     QLIST_HEAD(, AioHandler) aio_handlers;
 ​
 ...
     uint32_t notify_me;
 ​
     /* lock to protect between bh's adders and deleter */
     QemuLockCnt bh_lock;
   
   /* Anchor of the list of Bottom Halves belonging to the context */
 ...
     bool notified;
     EventNotifier notifier;
 ...
     /* TimerLists for calling timers - one per clock type */
     QEMUTimerListGroup tlg;
 ...
 };
  • 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的事件处理为主。

3.1 aio_context_new

aio_context_new用于创建一个AioContext:

 // async.c
 AioContext *aio_context_new(Error **errp) {
   int ret;
   AioContext *ctx;
   
   ctx = (AioContext *) g_source_new(&aio_source_funcs, sizeof(AioContext));
   aio_context_setup(ctx);
   
   ret = event_notifier_init(&ctx->notifier, false);
  ...
   g_source_set_can_recurse(&ctx->source, true);
   aio_set_event_notifier(ctx, &ctx->notifier,
                         false,
                        (EventNotifierHandler *)
                         event_notifier_dummy_cb);
  ...
   timerlistgroup_init(&ctx->tlg, aio_timerlist_notifer, ctx);
   
   return ctx;
 }
  1. aio_context_new函数首先创建分配了一个AioContext结构ctx。
  2. 然后初始化代表该事件源的事件通知对象ctx->notifier。
  3. 接着调用了aio_set_event_notifier用来设置ctx->notifier对应的事件通知函数。
  4. 最后初始化ctx中其他的成员。

3.2 aio_set_fd_hander

AioContext的创建函数中,aio_set_event_notifer函数调用了aio_set_fd_handler函数,后者用于添加或者删除AioContext事件源中的一个fd,如果是添加则会设置fd对应的读写函数。添加事件源中fd监听处理的步骤如下:

 // aio-posix.c
 void aio_set_fd_handler(AioContext *ctx, 
                        int fd,
                        bool is_external,
                        IOHandler* io_read,
                        IOHandler* io_write,
                        void* opaque) {
   AioHandler* node;
   bool is_new = false;
   bool is_deleted = false;
   
   node = find_aio_handler(ctx, fd);
   
   /* Are we deleting the fd handler? */
   if (!io_read && !io_write) {
    ...
  } else {
     if (node == NULL) {
       /* Alloc and insert if it's not already there */
       node = g_new0(AioHandler, 1);
       node->pfd.fd = fd;
       QLIST_INSERT_HEAD(&ctx->aio_handlers, node, node);
       
       g_source_add_poll(&ctx->source, &node->pfd);
       is_new = true;
    }
     /* Update handler with latest information */
     node->io_read = io_read;
     node->io_write = io_write;
     node->opaque = opaque;
     node->is_external = is_external;
     
     node->pfd.events = (io_read ? G_IO_IN | G_IO_HUP | G_IO_ERR : 0);
     node->pfd.events |= (io_write ? G_IO_OUT | G_IO_ERR : 0);
  }
   
   aio_epoll_update(ctx, node, is_new);
   aio_notify(ctx);
   if (deleted) {
     g_free(node);
  }
 }

该函数的参数说明如下:

  • 第一个参数ctx表示需要添加fd到哪个AioContext事件源。
  • 第二个参数fd表示添加的fd是需要在主循环中进行监听的。
  • 第三个参数is_external用于块设备层,对于事件监听的fd都设置为false。
  • 剩余参数中,io_read和io_write都是对应fd的回调函数,opaque会作为参数调用这些回调函数。

该函数的主要流程如下:

  1. aio_set_fd_handler函数首先调用find_aio_handler查找当前事件源ctx中是否已经有了fd。
  2. 考虑新加入的情况,这里会创建一个名为node的AioHandler,使用fd初始化node->pfd.fd,并将其插入到ctx->aio_handlers链表上,调用glib接口g_source_add_poll将该fd插入到了事件源监听fd列表中
  3. 设置node事件读写函数为io_read,io_write函数,根据io_read和io_write的有无设置node->pfd.events,也就是要监听的事件。

aio_set_fd_handler调用之后,新的fd事件就加入到了事件源的aio_handlers链表上了,如下图所示:

image-20230408000206524

aio_set_fd_handler函数一般被块设备相关的操作直接调用,如果仅仅是添加一个普通的事件相关的fd到事件源,通常会调用其封装函数qemu_set_fd_handler,该函数将事件fd添加到全部变量iohandler_ctx事件源中。

3.3 aio_ctx_dispatch

glib中自定义的事件源需要实现glib循环过程中调用的几个回调函数,QEMU中为AioContext事件源定义了名为aio_source_funcs的GSourceFuns结构,这几个函数都是自定义事件源需要实现的:

 // async.c
 static GSourceFuncs aio_source_funcs = {
 aio_ctx_prepare,
 aio_ctx_check,
 aio_ctx_dispatch,
 aio_ctx_finalize
 };

这里介绍一下最重要的事件处理分派函数aio_ctx_dispatch,该函数会调用aio_dispatch,后者要完成3件事:

  1. 第一是BH的处理。
  2. 第二是处理文件fd列表中有事件的fd。
  3. 第三是调用定时器到期的函数。

其中第二步的代码逻辑如下:

 // aio-posix.c
 bool aio_dispatch(AioContext* ctx) {
   AioHandler* node;
   bool progress = false;
  ...
   node = QLIST_FIRST(&ctx->aio_handelrs);
   while (node) {
     AioHandler* tmp;
     int revents;
     
     ctx->walking_handlers++;
     
     revents = node->pfd.revents & node->pfd.events;
     node->pfd.revents = 0;
     
     if (!node->deleted &&
        (revents & (G_IO_IN | G_IO_HUP | G_IO_ERR)) &&
        aio_node_check(ctx, node->is_external) &&
        node->io_read) {
       node->io_read(node->opaque);
       
       /* aio_notify() does not count as progress */
       if (node->opaque != &ctx->notifier) {
         progress = true;
      }
    }
     if (!node->deleted && 
        (revents & (G_IO_OUT | G_IO_ERR)) &&
        aio_node_check(ctx, node->is_external) &&
        node->io_write) {
       node->io_write(node->opaque);
       progress = true;
    }
     
     tmp = node;
     node = QLIST_NEXT(node, node);
     
     ctx->walking_handlers--;
     
     if (!ctx->walking_handlers && tmp->deleted) {
       QLIST_REMOVE(tmp, node);
       g_free(tmp);
    }
  }
   
   /* Run our timers */
   progress |= timerlistgroup_run_timers(&ctx->tlg);
   
   return progress;
 }

aio_dispatch_handlers函数会遍历aio_handlers,遍历监听fd上的事件是否发生了。

  1. fd发生的事件存在node->pfd.revents中,注册时指定需要接受的事件存放在node->pfd.events中,revents变量保存了fd接收到的事件。
  2. 对应G_IO_IN可读事件来说,会调用注册的fd的io_read回调,对G_IN_OUT可写事件来说,会调用注册的fd的io_write函数。
  3. 如果当前的fd已经删除了,则会删除这个节点。

4 QEMU事件处理过程

以signalfd的处理为例介绍QEMU事件处理的过程。signalfd是Linux的一个系统调用,可以将特定的信号与一个fd绑定起来,当有信号到达的时候fd就会产生对应的可读事件。

4.1 signal事件源的初始化

vl.c中的main函数会调用qemu_init_main_loop进行AioContext事件源的初始化,如下:

 // main-loop.c
 int qume_init_main_loop(Error **errp) {
   int ret;
   GSource* src;
   Error* local_error = NULL;
   
   init_clocks();
   
   ret = qemu_signal_init();
  ...
   qemu_aio_context = aio_context_new(&local_error);
  ...
   qemu_notify_bh = qemu_bh_new(notify_event_cb, NULL);
   gpollfds = g_array_new(FALSEFALSE, sizeof(GPollFD));
   src = aio_get_g_source(qemu_aio_context);
   g_source_set_name(src, "aio-context");
   g_source_attach(src, NULL);
   g_source_unref(src);
   src = iohandler_get_g_source();
   g_source_set_name(src, "io-handler");
   g_source_attach(src, NULL);
   g_source_unref(src);
   return 0;
 }

qemu_init_main_loop函数的主要逻辑如下:

  1. 首先调用qemu_signal_init将一个fd与一组信号关联起来,qemu_signal_init调用qemu_set_fd_handler函数将该signalfd对应的可读回调函数设置为sigfd_handler。

    1. qemu_set_fd_handler在首次调用时会调用iohandler_init创建一个全局的iohandler_ctx事件源,这个事件源的作用是监听QEMU中的各类事件。
    2. 最终qemu_signal_init会在iohandlers_ctx的aio_handlers上挂一个AioHandler节点,其fd为这里的signalfd,其io_read函数为这里的sigfd_handler。
  2. 接着调用aio_context_new创建一个全局的qemu_aio_context事件源,这个事件源主要用于处理BH和块设备层的同步使用。

  3. 最后调用aio_get_g_source和iohandler_get_g_source分别获取qemu_aio_context和iohandler_ctx的GSource,以GSource为参数调用g_source_attach两个AioContext加入到glib的主循环中去。

4.2 signal事件处理主循环

将信号对应的fd加入事件源以及将事件源加入到glib的主循环之后,QEMU就会进入一个while循环中进行事件监听。

当使用kill向QEMU进程发送SIGALARM信号时,signalfd就会有可读信号,从而导致glib的主循环返回调用g_main_context_dispatch进行事件分发,这会调用到aio_ctx_dispatch,最终会调用到qemu_signal_init注册的可读处理函数sigfd_handler。

参考文献

[1] QEMU/KVM源码解析与应用 - 李强

关注微信公众号:Linux内核拾遗

文章来源:mp.weixin.qq.com/s/-fNJNfynm…