写点什么

QEMU 事件循环机制

  • 2023-04-08
    江苏
  • 本文字数:8029 字

    阅读完需:约 26 分钟

QEMU事件循环机制

最近在阅读李强编著的《QEMU/KVM 源码解析与应用》这本书来学习 Linux 内核虚拟化相关知识,通过读书笔记的方式来提炼和归纳书中重要的知识点。本文主要内容是关于 QEMU 事件循环机制的介绍。


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

文章来源:https://mp.weixin.qq.com/s/-fNJNfynm7fxrrXYWI3npA


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



  • 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 命令。



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

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

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



  • 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 来开始主循环。



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

// vl.cstatic void main_loop(void) {  ...  do {    ...    last_io = main_loop_wait(nonblocking);    ...  } while (!main_loop_should_exit());}
// main-loop.cvoid 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.cstatic 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.cint qemu_poll_ns(GPollFD* fds, guint nfds, int64_t timeout) {#ifdef CONFIG_POLL  if (timeout < 0) {    return ppoll((struct pollfd*)fds, nfds, NULL, NULL);  } 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.cstatic 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.hstruct 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.cAioContext *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.cvoid 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 链表上了,如下图所示:



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.cstatic 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.cbool 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.cint 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(FALSE, FALSE, 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。

  2. qemu_set_fd_handler 在首次调用时会调用 iohandler_init 创建一个全局的 iohandler_ctx 事件源,这个事件源的作用是监听 QEMU 中的各类事件。

  3. 最终 qemu_signal_init 会在 iohandlers_ctx 的 aio_handlers 上挂一个 AioHandler 节点,其 fd 为这里的 signalfd,其 io_read 函数为这里的 sigfd_handler。

  4. 接着调用 aio_context_new 创建一个全局的 qemu_aio_context 事件源,这个事件源主要用于处理 BH 和块设备层的同步使用。

  5. 最后调用 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 内核拾遗

文章来源:https://mp.weixin.qq.com/s/-fNJNfynm7fxrrXYWI3npA

发布于: 2023-04-08阅读数: 19
用户头像

聚沙成塔 2023-01-12 加入

分享Linux内核开发相关的编程语言、开发调试工具链、计算机组成及操作系统内核知识、Linux社区最新资讯等

评论

发布
暂无评论
QEMU事件循环机制_Linux Kenel_Linux内核拾遗_InfoQ写作社区