深入 QEMU Guest Agent:虚拟机内外通信的隐形纽带

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 实现
└── ...
相关推荐
崇山峻岭之间2 小时前
Matlab学习记录31
开发语言·学习·matlab
石像鬼₧魂石2 小时前
22端口(OpenSSH 4.7p1)渗透测试完整复习流程(含实战排错)
大数据·网络·学习·安全·ubuntu
njsgcs2 小时前
SIMA2 论文阅读 Google 任务设定器、智能体、奖励模型
人工智能·笔记
你怎么知道我是队长3 小时前
C语言---输入和输出
c语言·开发语言
net3m333 小时前
单片机屏幕多级菜单系统之当前屏幕号+屏幕菜单当前深度 机制
c语言·c++·算法
你怎么知道我是队长3 小时前
C语言---文件读写
java·c语言·开发语言
云半S一3 小时前
pytest的学习过程
经验分享·笔记·学习·pytest
AI视觉网奇3 小时前
ue5.7 配置 audio2face
笔记·ue5
微露清风4 小时前
系统性学习C++-第二十讲-哈希表实现
c++·学习·散列表