1、QGA 简介
在虚拟化世界中,虚拟机(VM)常被视为一个"黑盒"------宿主机可以分配 CPU、内存和磁盘资源,却难以感知其内部状态:操作系统是否正常运行?文件系统是否已挂载?IP 地址是什么?关键进程是否卡死?
早期方案多依赖 SSH、SNMP 或自研代理,但普遍存在安全风险高、侵入性强、缺乏统一标准等问题。直到 QEMU Guest Agent(QGA) 的出现,才为这一难题提供了一种轻量、安全且高度标准化的解决方案。
QGA 是一个运行在虚拟机内部的轻量级守护进程,通过 virtio-serial 虚拟串口通道与宿主机通信,对外暴露一组基于 JSON-RPC 的标准接口。借助这一机制,外部管理系统无需开放网络端口或依赖复杂认证,即可安全地查询甚至控制 Guest OS 的内部状态。
尽管 QGA 最初作为 QEMU 项目的一部分诞生 ,其协议、守护进程(qemu-ga)及设备模型(virtio-serial)均围绕 QEMU/KVM 生态设计,原生支持也主要集中于 QEMU 及其衍生平台(如 libvirt、OpenStack) ,但其简洁、开放的设计理念使其影响力远超单一虚拟化框架。得益于 virtio-serial 接口的清晰规范,包括 ACRN 在内的多个非 QEMU 虚拟化平台,也能通过实现兼容的后端设备模型,无缝集成 QGA。
正因如此,QGA 不仅是云基础设施中的"幕后功臣",更是一个极具参考价值的跨平台虚拟机通信范式。深入理解 QGA 的设计与实现,有助于我们掌握虚拟机与宿主机之间高效、安全通信的核心原理,也为构建可移植、标准化的虚拟化管理能力提供了宝贵思路。
2、QGA 源码下载/编译
Linux 环境
c
$ cat /proc/version
Linux version 6.8.0-87-generic (buildd@lcy02-amd64-060) (x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04.2) 12.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #88~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Oct 14 14:03:14 UTC 2
下载 qemu 源码
c
# 1. 克隆官方仓库(保留未来升级或查历史的可能)
git clone https://gitlab.com/qemu-project/qemu.git
cd qemu
# 2. 切换到最新版
git checkout v10.1.0
配置 QGA:
c
./configure --enable-guest-agent
配置过程中,可能会报错,根据报错打印,依次安装需要的库即可:
c
sudo apt install -y python3-venv python3-pip
sudo apt install -y python3-tomli
sudo apt install -y ninja-build
sudo apt install -y \
pkg-config \
libglib2.0-dev
编译 QGA:
c
make qemu-ga
编译成功后,会在 /qemu/build/qga 目录下,生成 QGA 的可执行程序 qemu-ga
c
$ ./qemu-ga --version
QEMU Guest Agent 10.1.0
关于 qemu-ga 的使用介绍,可以阅读 QEMU Guest Agent
3、QGA 通信过程概览
宿主机通过 JSON-RPC 格式向 Guest 发送命令,Guest 执行后返回结果。通信通道支持多种后端:
- virtio-serial(默认,用于 QEMU/KVM)
- unix-listen(Unix Domain Socket)
- isa-serial(传统串口,主要用于 Windows)
- vsock-listen(AF_VSOCK 套接字)

通信过程如下(这里只是为了便于理解,并没有精细到虚拟机类型等具体场景,所以不具参考意义):

Guest userspace
write("/dev/virtio-ports/xxx")
↓
Guest kernel
virtio-serial frontend
↓
virtqueue (shared memory)
↓ kick
──────── VM Exit ────────
↓
Host userspace (QEMU)
virtio-serial backend
↓
chardev (socket / pipe)
↓
Host userspace process
3.1 QGA 本地测试(推荐 unix-listen)
在 Ubuntu 主机上测试 QGA 无需虚拟机,使用 unix-listen 模式最为便捷:
- 无需虚拟化环境
- 无需 root 权限
- 通信安全(仅本地进程可访问)
- 易于用 socat 模拟宿主机请求
第一步:启动 qemu-ga(监听 Unix Socket)
c
$ ./qemu-ga -m unix-listen -p /tmp/qga.sock --statedir=/tmp/qga-state
- -m unix-listen:使用 Unix Domain Socket 监听
- -p /tmp/qga.sock:指定 socket 路径
- --statedir=/tmp/qga-state:指定状态存储目录(避免权限问题)
第二步:用 socat 发送 JSON-RPC 命令(模拟 Host)
bash
# 获取主机名
liangjie@liangjie-virtual-machine:/tmp/qga-state$ echo '{"execute":"guest-get-host-name"}' | socat - UNIX-CONNECT:/tmp/qga.sock
{"return": {"host-name": "liangjie-virtual-machine"}}
# 获取操作系统信息
liangjie@liangjie-virtual-machine:/tmp/qga-state$ echo '{"execute":"guest-get-osinfo"}' | socat - UNIX-CONNECT:/tmp/qga.sock
{"return": {"name": "Ubuntu", "kernel-release": "6.8.0-87-generic", "version": "22.04.2 LTS (Jammy Jellyfish)", "pretty-name": "Ubuntu 22.04.2 LTS", "version-id": "22.04", "kernel-version": "#88~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Oct 14 14:03:14 UTC 2", "machine": "x86_64", "id": "ubuntu"}}
可以看到,正确的返回了 host name 以及 osinfo 属性。
关于 JSON-RPC 命令格式,请参考:
QEMU Guest Agent Protocol Reference
4、源码结构详解
QGA 的核心入口位于 qemu/qga/main.c。其 main() 函数负责初始化配置、解析参数、建立通信通道并启动事件循环。关键流程如下:
c
int main(int argc, char **argv)
{
int ret = EXIT_SUCCESS;
GAState *s;
GAConfig *config = g_new0(GAConfig, 1);
int socket_activation;
config->log_level = G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL;
qemu_init_exec_dir(argv[0]);
/* 注册所有 QGA 支持的 JSON-RPC 命令(如 guest-shutdown, guest-fsfreeze-freeze 等)*/
qga_qmp_init_marshal(&ga_commands);
/* 初始化默认路径(如 pidfile、state_dir) */
init_dfl_pathnames();
/* 尝试从配置文件(如 /etc/qemu-ga.conf)加载用户自定义配置 */
config_load(config);
/* 解析命令行参数(如 -m virtio-serial -p /dev/vport0p1 --daemonize 等) ,覆盖配置文件或默认值 */
config_parse(config, argc, argv);
/* 如果未指定 pid 文件路径,则使用默认路径(QGA 启动后,会将自己的进程 ID(PID)写入该文件) */
if (config->pid_filepath == NULL) {
config->pid_filepath = g_strdup(dfl_pathnames.pidfile);
}
/* 如果未指定状态目录,则使用默认路径(QGA 在运行过程中会把一些临时状态信息写入该目录下的文件) */
if (config->state_dir == NULL) {
config->state_dir = g_strdup(dfl_pathnames.state_dir);
}
/* 如果未指定通信方法,默认使用 virtio-serial(最常见于 KVM/QEMU) */
if (config->method == NULL) {
config->method = g_strdup("virtio-serial");
}
/*
* 检查是否由 systemd 通过 socket activation 启动(即环境变量 LISTEN_FDS > 0)。
* 返回值:
* 0 → 未使用 socket activation
* 1 → 使用了一个 socket(合法)
* >1 → 多个 socket(QGA 不支持)
*/
socket_activation = check_socket_activation();
if (socket_activation > 1) {
g_critical("qemu-ga only supports listening on one socket");
ret = EXIT_FAILURE;
goto end;
}
if (socket_activation) {
SocketAddress *addr;
g_free(config->method);
g_free(config->channel_path);
config->method = NULL;
config->channel_path = NULL;
addr = socket_local_address(FIRST_SOCKET_ACTIVATION_FD, NULL);
if (addr) {
if (addr->type == SOCKET_ADDRESS_TYPE_UNIX) {
config->method = g_strdup("unix-listen");
} else if (addr->type == SOCKET_ADDRESS_TYPE_VSOCK) {
config->method = g_strdup("vsock-listen");
}
qapi_free_SocketAddress(addr);
}
if (!config->method) {
g_critical("unsupported listen fd type");
ret = EXIT_FAILURE;
goto end;
}
} else if (config->channel_path == NULL) {
if (strcmp(config->method, "virtio-serial") == 0) {
/* try the default path for the virtio-serial port */
config->channel_path = g_strdup(QGA_VIRTIO_PATH_DEFAULT);
} else if (strcmp(config->method, "isa-serial") == 0) {
/* try the default path for the serial port - COM1 */
config->channel_path = g_strdup(QGA_SERIAL_PATH_DEFAULT);
} else {
g_critical("must specify a path for this channel");
ret = EXIT_FAILURE;
goto end;
}
}
if (config->dumpconf) {
config_dump(config);
goto end;
}
/*
* 核心初始化函数:
* - 创建 GAState 结构体
* - 打开通信通道(virtio-serial 设备 / unix socket / vsock)
* - 初始化事件循环(基于 GLib MainLoop)
* - 注册信号处理(如 SIGTERM)
* - 写入 pid 文件(若 daemonize)
* 返回 NULL 表示初始化失败。
*/
s = initialize_agent(config, socket_activation);
if (!s) {
g_critical("error initializing guest agent");
goto end;
}
#ifdef _WIN32
if (config->daemonize) {
SERVICE_TABLE_ENTRY service_table[] = {
{ (char *)QGA_SERVICE_NAME, service_main }, { NULL, NULL } };
StartServiceCtrlDispatcher(service_table);
} else {
ret = run_agent(s);
}
#else
/*
* run_agent() 启动 GLib 主循环,监听通道数据,
* 收到 JSON-RPC 请求后分发给对应命令处理函数。
*/
ret = run_agent(s);
#endif
cleanup_agent(s);
end:
if (config->daemonize) {
unlink(config->pid_filepath);
}
config_free(config);
return ret;
}
4.1 自动化代码生成机制
QGA 的命令接口通过 QAPI(QEMU API)框架自动生成。定义位于 qemu/qga/qapi-schema.json:
c
##
# @GuestHostName:
#
# @host-name: Fully qualified domain name of the guest OS
#
# Since: 2.10
##
{ 'struct': 'GuestHostName',
'data': { 'host-name': 'str' } }
##
# @guest-get-host-name:
#
# Return a name for the machine.
#
# The returned name is not necessarily a fully-qualified domain name, or even
# present in DNS or some other name service at all. It need not even be unique
# on your local network or site, but usually it is.
#
# Returns: the host name of the machine on success
#
# Since: 2.10
##
{ 'command': 'guest-get-host-name',
'returns': 'GuestHostName' }
该定义会自动生成:
- C 结构体 GuestHostName
c
struct GuestHostName {
char *host_name;
};
- Marshalling 函数(序列化/反序列化 JSON)
- 命令分发桩函数
c
/*
* /qemu/build/qga/qga-qapi-init-commands.c
*/
void qga_qmp_init_marshal(QmpCommandList *cmds)
{
......
/* 注册 RPC 命令 */
qmp_register_command(cmds, "guest-get-host-name",
qmp_marshal_guest_get_host_name, 0, 0);
......
}
/*
* /qemu/build/qga/qga-qapi-commands.c
*/
/* RPC 命令序列化 */
static void qmp_marshal_output_GuestHostName(GuestHostName *ret_in,
QObject **ret_out, Error **errp)
{
Visitor *v;
v = qobject_output_visitor_new_qmp(ret_out);
if (visit_type_GuestHostName(v, "unused", &ret_in, errp)) {
visit_complete(v, ret_out);
}
visit_free(v);
v = qapi_dealloc_visitor_new();
visit_type_GuestHostName(v, "unused", &ret_in, NULL);
visit_free(v);
}
/* RPC 命令实现 */
void qmp_marshal_guest_get_host_name(QDict *args, QObject **ret, Error **errp)
{
Error *err = NULL;
bool ok = false;
Visitor *v;
GuestHostName *retval;
v = qobject_input_visitor_new_qmp(QOBJECT(args));
if (!visit_start_struct(v, NULL, NULL, 0, errp)) {
goto out;
}
ok = visit_check_struct(v, errp);
visit_end_struct(v, NULL);
if (!ok) {
goto out;
}
/* 调用平台相关的具体实现 */
retval = qmp_guest_get_host_name(&err);
if (err) {
error_propagate(errp, err);
goto out;
}
qmp_marshal_output_GuestHostName(retval, ret, errp);
out:
visit_free(v);
v = qapi_dealloc_visitor_new();
visit_start_struct(v, NULL, NULL, 0, NULL);
visit_end_struct(v, NULL);
visit_free(v);
}
最终调用平台相关的实现,例如 guest-get-host-name 在 POSIX 系统的实现:
c
// /qemu/qga/commands-posix.c
char *qga_get_host_name(Error **errp)
{
long len = -1;
g_autofree char *hostname = NULL;
#ifdef _SC_HOST_NAME_MAX
len = sysconf(_SC_HOST_NAME_MAX);
#endif /* _SC_HOST_NAME_MAX */
if (len < 0) {
len = HOST_NAME_MAX;
}
/* Unfortunately, gethostname() below does not guarantee a
* NULL terminated string. Therefore, allocate one byte more
* to be sure. */
hostname = g_new0(char, len + 1);
if (gethostname(hostname, len) < 0) {
error_setg_errno(errp, errno,
"cannot get hostname");
return NULL;
}
return g_steal_pointer(&hostname);
}
// /qemu/qga/commands.c
GuestHostName *qmp_guest_get_host_name(Error **errp)
{
GuestHostName *result = NULL;
g_autofree char *hostname = qga_get_host_name(errp);
/*
* We want to avoid using g_get_host_name() because that
* caches the result and we wouldn't reflect changes in the
* host name.
*/
if (!hostname) {
hostname = g_strdup("localhost");
}
result = g_new0(GuestHostName, 1);
result->host_name = g_steal_pointer(&hostname);
return result;
}
4.2 平台适配层
QGA 通过条件编译支持多平台,源码结构清晰分离:
bash
qemu/qga/
├── commands.c # 通用命令逻辑
├── commands-posix.c # Linux/Unix 实现
├── commands-linux.c # Linux 特有功能
├── commands-win32.c # Windows 实现
├── commands-bsd.c # BSD 实现
└── ...