Skip to the content.

QEMU AioContext 与 GLib 连接机制

本文记录三个问题:

  1. AioContext 的 fd 是怎么注册进 GSource
  2. aio_notify() 为什么能唤醒 glib loop
  3. 主线程为什么不用 g_main_loop_run(),而要自己 poll

1. AioContext 的 fd 是怎么注册进 GSource

先说结论:AioContext 里维护了一组 AioHandler,每个 handler 带一个 GPollFD pfd。这些 GPollFD 会被加到 ctx->source 这个 GSource 上,所以 glib 在 poll 这个 source 时,实际上也在 poll 这些 fd。

1.1 AioContext 本身就是 GSource

AioContext 不是“额外包了一层 glib 对象”,而是直接用 g_source_new() 分配出来的:

因此 AioContext 头部就是 GSource,后续 aio_get_g_source() 直接返回 &ctx->source

1.2 fd handler 的注册入口

某个 fd 要进入 AioContext,通常走:

这段代码在 util/aio-posix.c。它会:

  1. 查找或创建一个 AioHandler
  2. 填写 new_node->pfd.fd
  3. 按读写回调设置 new_node->pfd.events
  4. 调用 ctx->fdmon_ops->update(ctx, old_node, new_node)

最关键的是最后一步。fdmon_ops 决定“这个 fd 如何挂到 GSource 上”。

1.3 默认 poll backend 的做法

POSIX 下 aio_context_setup() 默认先用 poll backend:

如果当前 backend 是 poll,则 update() 会走 util/fdmon-poll.c 中的 fdmon_poll_update()

这就是“fd 直接注册进 GSource”的实际实现。

所以在 poll backend 下:

1.4 epoll backend 的做法

当 fd 数量变多时,QEMU 会尝试从 poll 升级到 epoll:

升级后,glib 不再直接盯每个业务 fd,而是盯一个 epollfd

util/fdmon-epoll.c 中:

也就是说:

1.5 iouring backend

也是类似,通过 iouring 也是将 ring_fd 注册到 glib 中:

bool fdmon_io_uring_setup(AioContext *ctx, Error **errp)
{
    int ret;

    ctx->io_uring_fd_tag = NULL;

    ret = io_uring_queue_init(FDMON_IO_URING_ENTRIES, &ctx->fdmon_io_uring, 0);
    if (ret != 0) {
        error_setg_errno(errp, -ret, "Failed to initialize io_uring");
        return false;
    }

    QSLIST_INIT(&ctx->submit_list);
    QSIMPLEQ_INIT(&ctx->cqe_handler_ready_list);
    ctx->fdmon_ops = &fdmon_io_uring_ops;
    ctx->io_uring_fd_tag = g_source_add_unix_fd(&ctx->source,
            ctx->fdmon_io_uring.ring_fd, G_IO_IN);
    return true;
}

其实都是类似的,也就是注册一个总的 fd ,然后接受到事件之后,逐个处理。

3. 主线程为什么不用 g_main_loop_run(),而要自己 poll

其实就是将 glib 需要 poll 的 fd 拿出来直接调用 ppoll

3.1 主线程的实际做法

主线程关键路径在 util/main-loop.cos_host_main_loop_wait()

它做的不是直接:

而是下面这套流程:

  1. g_main_context_acquire(context)
  2. g_main_context_prepare(context, &max_priority)
  3. g_main_context_query(...) 取出 glib 需要 poll 的 GPollFD[]
  4. 把这些 fd 放入 QEMU 自己的 gpollfds
  5. 解锁 BQLreplay_mutex
  6. qemu_poll_ns(...)
  7. 重新加锁
  8. g_main_context_check(...)
  9. 如果 ready,再 g_main_context_dispatch(...)

也就是说,主线程模式下:

3.2 为什么不能直接 g_main_loop_run()

原因一:QEMU 要统一 timeout 决策

glib 只知道 glib 自己的 source timeout。

但 QEMU 主循环还要考虑:

所以 glib_pollfds_fill() 里会把 glib 给出的 timeout 和 QEMU 自己的 timeout 做合并,取最早那个:

如果直接 g_main_loop_run(),这个“统一裁决 timeout”的能力就弱很多。

原因二:QEMU 要精确控制锁边界

在真正阻塞前,主线程会:

阻塞返回后再:

这段边界非常关键,代码就在 os_host_main_loop_wait()

如果直接进入 g_main_loop_run(),QEMU 很难精确规定“在哪个阻塞点前释放 BQL,在哪个点后重新拿回来”。

原因三:QEMU 主线程本身就是总调度器

主线程除了 glib source,还要处理:

因此主线程不是把 glib 当总事件循环,而是把 glib 当作:

最终总控权仍然在 QEMU 自己的 main loop 手里。

为什么 main 中需要集成两个

/*
 * Functions to operate on the I/O handler AioContext.
 * This context runs on top of main loop. We can't reuse qemu_aio_context
 * because iohandlers mustn't be polled by aio_poll(qemu_aio_context).
 */
static AioContext *iohandler_ctx;

aio_ctx_dispatch 里的 ctx 指针是谁:

  - ctx == qemu_get_aio_context() -> qemu_aio_context
  - ctx == iohandler_get_aio_context() -> iohandler_ctx
  - 否则大概率是某个 IOThread 的 AioContext

原来是可以通过这个来研究的:

qemu handlers iohandler_ctx
qemu handlers qemu_aio_context

但是对比了一下,只能说,这个也完全看不懂这个差不啊

qemu handlers iohandler_ctx

  io_read = 0x556abfc364b0 <virtio_queue_host_notifier_read>, * 71
  io_read = 0x556abfc3ec50 <vhost_virtqueue_error_notifier>, * 4
  io_read = 0x556ac004e240 <sigfd_handler>,
  io_read = 0x556ac004d060 <aio_context_notifier_cb>,

qemu handlers qemu_aio_context

  io_read = 0x556abfc364b0 <virtio_queue_host_notifier_read>, * 65
  io_read = 0x556abff62b70 <qemu_laio_completion_cb>,
  io_read = 0x556ac004d060 <aio_context_notifier_cb>,

aio_poll 是可以嵌套调用的

nbd_server_free 中就是一个经典案例:

首先发起请求,然后希望等所有的 callbakc 完成,直到 server->connections > 0

    QLIST_FOREACH_SAFE(conn, &server->conns, next, tmp) {
        qio_channel_shutdown(QIO_CHANNEL(conn->cioc), QIO_CHANNEL_SHUTDOWN_BOTH,
                             NULL);
    }

    AIO_WAIT_WHILE_UNLOCKED(NULL, server->connections > 0);

为什么 iothread 中需要 glib

也不是什么大问题了,应该是很小的点了:

static void *iothread_run(void *opaque)
{
    // ...
    while (iothread->running) {
        /*
         * Note: from functional-wise the g_main_loop_run() below can
         * already cover the aio_poll() events, but we can't run the
         * main loop unconditionally because explicit aio_poll() here
         * is faster than g_main_loop_run() when we do not need the
         * gcontext at all (e.g., pure block layer iothreads).  In
         * other words, when we want to run the gcontext with the
         * iothread we need to pay some performance for functionality.
         */
        aio_poll(iothread->ctx, true);

        /*
         * We must check the running state again in case it was
         * changed in previous aio_poll()
         */
        if (iothread->running && qatomic_read(&iothread->run_gcontext)) {
            g_main_loop_run(iothread->main_loop);
        }
    }

本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。