最近遇到了 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 的 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_qmp→monitor_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
社区大佬们指出了其中存在的两个关键问题:
- Daniel 指出未判断 chr 是否是 SocketChardev 类型,可能导致 crash,另外可以和
remove_fd_in_watch(chr)一样考虑更通用的做法,而不只是针对 SocketChardev - 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 最终能被接受。