QEMU qmp 命令卡住问题分析

 2小时前     20  

文章目录

最近遇到了 dummy qemu 进程 qmp 命令卡住的问题,发现社区 qemu 最新版本(10.x+)也存在该问题,便向社区提交了修复 patch,在此记录下问题分析过程与修复方案。

QEMU 环境如下:

  • Linux 6.6.87.2-microsoft-standard-WSL2 x86_64
  • QEMU emulator version 10.1.91 (v10.2.0-rc1-38-gfb241d0a1f),commit fb241d0a1f

问题现象

libvirt 使用 virsh version 或 virsh capabilities 命令去探测系统信息时,会启动一个 dummy qemu 进程,dummy qemu 启动后会监听 qmp 对应的 unix socket,然后 libvirt 再连接该 unix socket,并发送 qmp 命令获取系统相关信息。

dummy qemu 进程相应的命令行如下所示:

qemu-system-x86_64 -S -no-user-config -nodefaults -nographic -machine none,accel=kvm:tcg -qmp unix:/tmp/qmp-xxx/qmp.monitor,server=on,wait=off

问题现象是执行 virsh version 或 virsh capabilities 命令时卡住,也就是 qmp 命令没有返回,通过 gdb 和 nc 命令模拟复现的结果如下所示。

QEMU qmp 命令卡住问题分析 QEMU qmp 命令卡住问题分析

问题分析

基于对 qemu 的 gdb 调试结果,确认 dummy qemu 卡住的原因为:io 线程初始化 qmp 监听时未及时移除主线程对 qmp fd 的监听,使得主线程和 io 线程同时监听同一个 qmp fd,从而产生竞态,导致 qmp 命令卡住。具体来说,触发 dummy qemu 卡住的基本流程如下:

1、dummy qemu 进程启动

  • 主线程初始化监听 qmp 对应 fd(qemu_chardev_new);
  • 由于默认开启 io 线程,所以 io 线程会处理 qmp 监听任务,并移除主线程对 qmp 的监听(monitor_init_qmpmonitor_qmp_setup_handlers_bh);
  • 由于 io 线程移除主线程对 qmp fd 的监听较晚(qio_net_listener_set_client_func_full),导致存在一个较短的时间窗口【io线程监听到主线程更新监听 fd之间】,主线程和 io 线程都会监听 qmp fd;

2、客户端发起连接

  • dummy qemu 的主线程(或 io 线程)首先监听到 fd 事件并调用 accept4 处理了对应监听的 fd 事件并添加新的监听请求,此时正常返回内容
  • dummy qemu 的 io 线程(或主线程)也监听到 fd 事件,此时再次调用 accept4 时已无可读内容,导致io 线程(或主线程)阻塞
  • 注:主线程先处理则 io 线程卡,io 线程先处理则主线程卡

3、客户端连接成功后发送 qmp 命令

  • 当主线程卡住时, io 线程监听到新的请求后调用 tcp_chr_read 读取相关内容后将实际的处理函数 handle_qmp_command 加到队列中,等待协程唤醒后处理,但此时主线程卡住,协程无法唤醒,导致无法响应请求
  • 当 io 线程卡住时,由于前面主线程处理连接事件时 fd 事件源绑定的 context 已经由主线程变成 io 线程,所以主线程不会监听该事件,由 io 线程监听该事件,但是 io 线程卡住,同样无法响应请求

解决方案

由前面的分析可以发现以下两种处理方法:

1、单主线程和单 io 线程都可以完成 qmp 事件处理,所以最简单的方法是在 dummy qemu 场景下(machine none)不开启 io 线程,让主线程去处理 qmp 事件即可,示例伪代码如下。

# monitor/qmp.c

void monitor_data_init(Monitor *mon, bool is_qmp, bool skip_flush,
                       bool use_io_thread)
{
    if (use_io_thread && !mon_iothread) {
        monitor_iothread_init();
    }
    ...
}

// machine_is_none 初始化为 false,在 qemu_create_machine 中通过判断
// machine_class->name 是否为 none 来更新它的值
bool machine_is_none = False;

void monitor_init_qmp(Chardev *chr, bool pretty, Error **errp)
{
    MonitorQMP *mon = g_new0(MonitorQMP, 1);
    ...
    /* Note: we run QMP monitor in I/O thread when @chr supports that */
    // 添加 machine_is_none 判断,在 machine_is_none 为 false 时开启 io 线程
    // 在 machine_is_none 为 true 时则不开启 io 线程
    monitor_data_init(&mon->common, true, false,
                      qemu_chr_has_feature(chr, QEMU_CHAR_FEATURE_GCONTEXT)
                      && !machine_is_none);
    ...
}

2、问题本质上是由io 线程初始化 qmp 监听时未及时移除主线程对 qmp fd 的监听造成的,所以在 io 线程初始化 qmp 监听前就移除主线程对 qmp fd 的监听可以解决该问题,示例伪代码如下。

void socket_chr_listener_cleanup(Chardev *chr)
{
    SocketChardev *s = SOCKET_CHARDEV(chr);

    if (s->listener) {
        QIONetListener *listener = s->listener;
        size_t i;

        for (i = 0; i < listener->nsioc; i++) {
            if (listener->io_source[i]) {
                g_source_destroy(listener->io_source[i]);
                g_source_unref(listener->io_source[i]);
                listener->io_source[i] = NULL;
            }
        }
    }
}

void monitor_init_qmp(Chardev *chr, bool pretty, Error **errp)
{
    MonitorQMP *mon = g_new0(MonitorQMP, 1);
    ...
    /* Note: we run QMP monitor in I/O thread when @chr supports that */
    monitor_data_init(&mon->common, true, false,
                      qemu_chr_has_feature(chr, QEMU_CHAR_FEATURE_GCONTEXT));
    if (mon->common.use_io_thread) {
        remove_fd_in_watch(chr);

        // 在 io 线程初始化 qmp 监听前添加 socket_chr_listener_cleanup 
        // 移除主线程对 qmp fd 的监听
        socket_chr_listener_cleanup(chr);

        aio_bh_schedule_oneshot(iothread_get_aio_context(mon_iothread),
                                monitor_qmp_setup_handlers_bh, mon);
    } 
}

总的来说,第一种方法虽然可以解决问题,但比较粗暴,适用于 dummy qemu 这种场景,第二种方法则可以从根本上解决问题。

社区提交

在确认社区 qemu 最新版本(10.x+)也存在该问题后,决定向社区提交 patch,采用的是上述第二种方法。

V1、直接移除主线程对 qmp fd 的监听

这一版过于粗暴,就是上述第二种方法的实现。

  • [PATCH] monitor/qmp: cleanup socket listener sources early to avoid fd handling race
  • https://lists.nongnu.org/archive/html/qemu-devel/2025-11/msg01621.html

社区大佬们指出了其中存在的两个关键问题:

  1. Daniel 指出未判断 chr 是否是 SocketChardev 类型,可能导致 crash,另外可以和remove_fd_in_watch(chr)一样考虑更通用的做法,而不只是针对 SocketChardev
  2. Eric 指出代码在 net-listener.c 之外直接访问 listener->nsioc,他提了个新的 patch 禁止这种做法,并提到可以通过使用 qio_net_listener_set_client_func_full 将回调函数设置为 NULL 来移除主线程对 qmp fd 的监听

V2、通过将回调设置为 NULL 来移除监听

这一版根据 Daniel 和 Eric 的意见进行了修改,判断了 chr 的类型并使用 qio_net_listener_set_client_func_full来移除主线程的监听。

  • [PATCH v2] monitor/qmp: cleanup SocketChardev listener sources early to avoid fd handling race
  • https://lists.nongnu.org/archive/html/qemu-devel/2025-11/msg02443.html
@@ -537,6 +539,16 @@ void monitor_init_qmp(Chardev *chr, bool pretty, Error 
**errp)
          * e.g. the chardev is in client mode, with wait=on.
          */
         remove_fd_in_watch(chr);
+        /*
+         * Clean up SocketChardev listener IO sources early to prevent
+         * racy fd handling between the main thread and the I/O thread.
+         */
+        if (object_dynamic_cast(OBJECT(chr), TYPE_CHARDEV_SOCKET)) {
+            SocketChardev *s = SOCKET_CHARDEV(chr);
+            if (s->listener)
+                qio_net_listener_set_client_func_full(s->listener, NULL, NULL,
+                                                      NULL, chr->gcontext);
+        }

虽然比较简洁,但是不够优雅,没有采用通用的做法,这一版在社区无人回复。

V3、chardev 类添加清理 handler

这一版采用了更通用的做法,通过在 Chardev 类添加清理 handler 函数,并在 SocketChardev 实现清理函数来移除监听。

  • [PATCH v3] monitor/qmp: cleanup SocketChardev listener sources early to
  • https://lists.nongnu.org/archive/html/qemu-devel/2025-11/msg03504.html

社区大佬 Marc-André 提到能否分享 qmp 卡住时的调用堆栈(直接邮件回复了),并指出了其中存在的 typo 小问题:

  • 一是 if 单行语句也加上大括号
  • 二是 listener 拼写错误(我写成了 listaner :))

此外,他还提到移除监听是否也放到 remove_fd_in_watch 中,但是这会影响所有调用到它的地方,所以目前还是 nevermind

V4、V3 小问题修改

这一版就是修改了 V3 中 Marc-André 指出的问题。

  • [PATCH v4] monitor/qmp: cleanup SocketChardev listener sources early to
  • https://lists.nongnu.org/archive/html/qemu-devel/2025-11/msg01794.html

目前还在等待回复中。

总结

总之,第一次给 qemu 上游社区做贡献,还是很开心的😊。

此外,由衷感谢社区大佬们的指点,他们能一针见血地指出问题所在并提供可行的建议,让我在这种迭代优化过程中收获很多。

最后,虽然整个周期有些漫长,但还是希望这个 patch 最终能被接受。

版权声明:小傅 发表于 2小时前,共 4970 字。
转载请注明:QEMU qmp 命令卡住问题分析 | 太傅博客

暂无评论

暂无评论...