QEMU_003

返回主页
返回上一页

Understanding QEMU [L2.01]

本节内容主要包含:QEMU基本组件

本节QEMU源代码版本: qemu-10.0.3。涉及到的代码文件如下所示

参考文献:

  • 《QEMU/KVM源码解析与应用》

@last_update: 2025/09/10

 

"Everying is a file"

 

QEMU事件循环机制

在上一节的末尾,其实出现了包括kvm_fd,vm_fd,vcpu_fd在内的各种fd。”一切皆文件“是UNIX/Linux的著名哲学理念。Linux具体文件、设备、网络socket等都可以抽象为文件。内核通过虚拟文件系统VFS抽象出一个统一的界面。Linux通过fd(文件描述符)来访问一个文件,application可以调用select \ poll \ epoll系统调用监听文件变化。

QEMU程序的运行也基于各类文件fd事件,在运行过程中将感兴趣的文件fd添加到监听列表上并定义相应的处理函数。在QEMU主线程中,有一个循环用来处理这些文件fd的事件(如用户的输入、来自VNC的连接,vNIC对应tap设备的收包...)

 

为什么需要事件循环?

QEMU 本质上是一个用户态的虚拟机监控器(VMM),除了 vCPU 执行指令,还需要处理:

这些事件都是异步发生的,因此 QEMU 需要一个 事件循环机制 来统一管理。

 

glib事件循环机制

QEMU的事件循环机制基于glib (跨平台,C编写的库)。或者说QEMU 的事件循环是基于 glib 提供的抽象,但 QEMU 自己实现了一个轻量级的循环框架。

glib 的 事件循环机制(GMainLoop / GMainContext) 是 QEMU、GTK 等很多项目的基础。它本质上是一个 多路事件分发器,统一处理文件描述符、定时器、空闲任务等事件。

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

补充问题

Q: 什么是Poll ?

  • 狭义的poll

    • 在 Linux/Unix 系统里,poll 是一个系统调用,用来等待 一个或多个文件描述符(fd) 的事件发生(比如可读、可写、异常)。

      • fds:要监听的文件描述符数组,每个 fd 可以指定关心的事件(可读、可写等)。

      • nfds:数组长度。

      • timeout:超时时间(毫秒),-1 表示无限等待。

      • 返回值

        • 大于 0 :有事件发生

        • 等于 0 :超时

        • 小于 0 :出错

    • 比如:

      • 这样就能在一个阻塞点同时等待多个事件。

  • 广义的poll

    • 在很多事件循环文档里,poll 被泛指为 等待事件 的阶段,不一定非得是 poll() 系统调用,也可能是:

      • select()

      • epoll_wait()(Linux 更高效)

      • kqueue()(BSD / macOS)

      • IOCP(Windows)

    • glib 事件循环里的 poll 阶段 就是:主循环根据所有 GSource 的需要,收集 fd 和超时时间 → 调用 poll()epoll_wait() → 等待事件发生或定时器超时。

  • 一句话解释:“poll”就是 挂起当前线程,等待一组事件(I/O、定时器等)发生 的机制;在事件循环中,poll 阶段就是负责阻塞直到事件准备好,然后再进入 dispatch 阶段。dispatch可以调用事件源对应事件的处理函数。

 

QEMU中的事件循环

背景知识:tap设备

Q: 什么是tap设备

TAP 设备是 Linux 内核里提供的一种虚拟网络设备,用于用户态程序和内核网络协议栈之间传递数据,常用于虚拟化(QEMU/KVM、容器)和网络仿真。

一句话理解:TAP 就像一根虚拟网线,一端插在虚拟机(通过 QEMU),另一端插在宿主机内核的网络栈,从而让虚拟机能像真实主机一样收发以太网数据。

Linux 里有两种虚拟网络设备:

  • TUN (network TUNnel)

    • 面向 三层(L3,IP 层),收发的是 IP 包

    • 常用于 VPN、隧道协议(比如 OpenVPN)。

  • TAP (network tap)

    • 面向 二层(L2,以太网层),收发的是 以太网帧

    • 常用于虚拟机、容器虚拟网卡,因为虚拟机需要完整的以太网接口。

TAP 设备在内核里表现为一个 虚拟网卡(比如 tap0),但是它不直接连到物理网卡,而是:

  • 一端在 内核网络栈(表现得像 eth0 一样,可以配置 IP、MAC 地址)。

  • 一端通过 字符设备文件 /dev/net/tun 暴露给用户空间程序。

用户空间程序(比如 QEMU、OpenVPN)可以:

  • 通过 read() 从 TAP 设备拿到 虚拟网卡收到的以太网帧

  • 通过 write()以太网帧写到 TAP 设备,内核网络栈就会认为这是网卡收来的包

TAP在虚拟化里的作用

在 QEMU/KVM 中:

 

背景知识:QMP

QMP 是 QEMU 提供的一种 基于 JSON 的控制协议 (QEMU Monitor Protocol),运行在 管理平面

用来 控制和管理虚拟机,比如:

交互方式

QMP 是管理接口(像命令行 API),用来控制虚拟机。

 

背景知识:VNC

VNC 是一种 远程桌面协议(图形界面访问协议),QEMU 可以内置一个 VNC 服务器。

用来 远程访问虚拟机的显示画面,就像你坐在虚拟机的显示器前一样。

交互方式

例子:

可以把 VNC 理解为“QEMU 提供的远程显示/远程桌面接口”。用来操作虚拟机的屏幕和输入设备。

 

注册和处理

 

如图所示,QEMU在运行过程中会注册一些感兴趣的事件,设置其对应的处理函数

例如,对于VNC来说,会创建一个socket用于监听来自用户的连接,注册其可读事件为vnc_client_io,当VNC有连接到来的时候,glib的框架就会调用vnc_client_io函数。

图中的例子是注册网卡设备的后端tap设备的收包。收到包后QEMU调用tap_send将包路由给虚拟机网卡前端,若虚拟机使用qmp,则在管理界面中当用户发送qmp命令过来之后,glib会调用事先注册的tcp_chr_accept来处理用户的qmp命令。

 

这个命令是启动一个 QEMU 虚拟机 的完整示例,表示启动一个 64 位 QEMU 虚拟机,分配 1GB 内存4 个 vCPU,硬盘镜像是 /home/test/test.img,使用 KVM 硬件加速,并且通过 VNC (5900端口) 输出显示画面。

 

再次强调事件循环的意义:

 

QEMU的main函数早期版本定义在vl.c中,现在(qemu-10.0.3)定义在main.c中,在进行好所有初始化工作后会调用函数main_loop来开始主循环。

具体代码是这样的

这里的这个qemu_init还是定义在vl.c当中的

qemu_default_main()中的qemu_main_loopsystem.h中申明,在runstate.c中实现。

然后这里的`main_loop_wait是实现在main_loop.c

main_loop_wait

QEMU主循环对应的三个函数如图所示,被上述代码中的os_host_main_loop_wait包含:

注: 此处第三步写错了,是glib_pollfds_poll

这段代码也被定义在main-loop.c中。

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

更通俗地描述:在 QEMU 里,主循环 main_loop_wait() 负责处理两类事件(i) I/O事件 (ii)定时器事件(QEMU 设定的虚拟时钟/周期性任务,到达触发时间时需要执行)。

为了避免 CPU 忙等 (轮询),QEMU 的主循环会调用一个阻塞函数 os_host_main_loop_wait(),通常基于 ppoll() / select() / epoll(),让线程“睡眠”直到事件到来。

但问题是:如果只靠 I/O 事件唤醒,定时器就可能错过触发时间。所以 QEMU 需要一个 超时机制 (timeout) 来保证按时处理定时器。

所谓 定时器到期事件,就是某个定时器设定的时间到了,需要执行对应的回调函数。举几个在 QEMU 里的例子:

  • 虚拟时钟 (vm_clock):模拟 CPU 指令执行的时钟,定期触发事件来同步 vCPU 与外设。

  • 周期性任务:比如刷新虚拟机状态、统计性能数据、触发 watchdog、刷新屏幕(VNC/SDL)。

  • 设备模拟定时器:模拟硬件设备的定时器(如网卡需要周期性发送中断、磁盘控制器定时刷新缓存)。

当某个定时器的到期时间 <= 当前虚拟时钟,就会触发“定时器到期事件”,执行对应的 handler。

timeout 的作用 = 限制主循环最多阻塞多久,以便 QEMU 能够及时处理这些定时器事件。

 

QEMU主循环的一个函数是glib_pollfds_fill (定义在main-loop.c中)。

 

此时就完成了上图的第一步,已经有了所有需要监听的fd了,然后会调用qemu_mutex_unlock_iothread释放QEMU大锁(Big QEMU Lock, BQL)。关于BQL将在下一节中介绍。

接着,os_host_main_loop_wait函数会调用qemu_poll_ns

该函数在timer.h中声明,在qemu-timer.c中实现

它接收三个参数

 

判断是否使用 ppoll

 

qemu_poll_ns的调用会阻塞主线程,当该函数返回之后

不管怎么样,这都将进入图中的第三步,也就是调用glib_pollfds_poll进行事件的分发处理

该函数实现于main-loop.c

该函数调用了glib框架中的g_main_context_check检测事件,然后调用g_main_context_dispatch进行了事件的分发。

 

 

QEMU主循环真正的循环

上面的main_loop_wait其实只执行了一次,真正的循环发生在qemu_main_loop里。也即是之前提到的在runstate.c中提到的代码。返回值是程序退出状态(EXIT_SUCCESS 或其他)

 

循环效果