2. vhost 协议基础:vhost-user 协议规范、控制通道与数据通道分离、消息类型与交互流程
vhost-user 协议是 DPDK 实现高性能虚拟化 I/O 的核心协议。它基于 Unix Domain Socket 实现控制面通信,并通过共享内存直接传递数据面报文,从而在虚拟机与宿主机之间实现接近原生硬件的 I/O 性能。本章将深入剖析 vhost-user 协议的规范、通道分离机制、消息类型定义以及完整的交互流程。
2.1 vhost-user 协议规范概述
vhost-user 协议定义了一套在用户态 VMM(如 QEMU)与用户态 vhost 后端(如 DPDK vhost)之间进行控制交互的消息集合。其核心设计思想是将 virtio 设备的控制面与数据面完全分离:控制面通过 Unix Domain Socket 传递消息,数据面则通过共享内存(SHM)直接访问 virtqueue 和描述符表。
- 控制面与数据面分离:控制消息走 Socket,数据报文走共享内存,避免控制操作干扰数据路径。
- 零拷贝数据路径:后端直接操作 Guest 物理内存映射,无需数据复制。
- 事件驱动:通过 VHOST_USER_SET_VRING_KICK 和 VHOST_USER_SET_VRING_CALL 实现异步通知。
vhost-user 协议规范经历了多个版本的演进,当前主流版本为 vhost-user 协议规范 0.1(基于 virtio 1.0 标准)。下表总结了协议的核心版本特性:
| 协议版本 | 基础标准 | 关键特性 | 应用场景 |
|---|---|---|---|
| vhost-user 0.1 | virtio 1.0 | 基础消息集、单队列、共享内存映射 | 早期 DPDK 集成 |
| vhost-user 0.2 | virtio 1.0 | 多队列支持、协议特性协商 | 多核虚拟化场景 |
| vhost-user 0.3 | virtio 1.1 | 间接描述符、事件索引、写透缓存 | 高性能网络、存储 |
| vhost-user 0.4+ | virtio 1.2+ | 共享内存区域、VHOST_USER_GET_SHARED_OBJECT | 跨进程共享、GPU 虚拟化 |
2.2 控制通道与数据通道分离
vhost-user 协议最核心的架构设计是控制通道与数据通道的完全分离。这种分离设计使得控制面的复杂协商不会影响数据面的高速转发。
下图展示了 vhost-user 控制通道与数据通道的分离架构:
从架构图可以看出,控制通道与数据通道在物理和逻辑上完全独立:
- 控制通道:通过 Unix Domain Socket 传递 vhost-user 消息,负责设备初始化、特性协商、内存映射、队列配置等控制操作。
- 数据通道:通过共享内存直接访问 virtqueue 的描述符表、可用环和已用环,实现零拷贝数据收发。
2.3 消息类型与分类
vhost-user 协议定义了丰富的消息类型,按照功能可以分为以下几大类:
| 消息类别 | 消息名称 | 方向 | 功能描述 |
|---|---|---|---|
| 设备初始化 | VHOST_USER_GET_FEATURES | 后端 → 前端 | 获取后端支持的特性位图 |
| VHOST_USER_SET_FEATURES | 前端 → 后端 | 设置协商后的特性位图 | |
| VHOST_USER_SET_OWNER | 前端 → 后端 | 声明前端为设备所有者 | |
| 内存管理 | VHOST_USER_SET_MEM_TABLE | 前端 → 后端 | 传递 Guest 内存区域映射表 |
| VHOST_USER_ADD_MEM_REG | 前端 → 后端 | 动态添加内存区域(协议 0.4+) | |
| VHOST_USER_REM_MEM_REG | 前端 → 后端 | 动态移除内存区域(协议 0.4+) | |
| 队列配置 | VHOST_USER_SET_VRING_NUM | 前端 → 后端 | 设置 virtqueue 深度 |
| VHOST_USER_SET_VRING_ADDR | 前端 → 后端 | 设置 virtqueue 描述符表、可用环、已用环地址 | |
| VHOST_USER_SET_VRING_BASE | 前端 → 后端 | 设置 virtqueue 当前索引 | |
| VHOST_USER_GET_VRING_BASE | 后端 → 前端 | 获取 virtqueue 当前索引(用于设备保存/恢复) | |
| 通知机制 | VHOST_USER_SET_VRING_KICK | 前端 → 后端 | 设置 kick 事件文件描述符(Guest 通知后端) |
| VHOST_USER_SET_VRING_CALL | 前端 → 后端 | 设置 call 事件文件描述符(后端通知 Guest) | |
| VHOST_USER_SET_VRING_ERR | 前端 → 后端 | 设置错误事件文件描述符 | |
| 设备控制 | VHOST_USER_SET_BACKEND_REQ_FD | 前端 → 后端 | 设置后端请求文件描述符(协议 0.3+) |
| VHOST_USER_RESET_DEVICE | 前端 → 后端 | 重置设备状态 |
2.4 消息交互流程
vhost-user 协议的交互流程可以分为三个阶段:设备初始化阶段、运行阶段、设备销毁阶段。下面通过流程图展示完整的交互过程:
2.5 关键消息详解
以下对几个关键消息进行详细说明:
2.5.1 VHOST_USER_SET_MEM_TABLE
该消息是 vhost-user 协议中最核心的消息之一。QEMU 通过该消息将 Guest 的内存区域信息传递给 vhost 后端,后端据此建立 GPA(Guest Physical Address)到 HVA(Host Virtual Address)的映射表。
/* vhost-user 消息头结构 */
struct vhost_user_msg {
uint32_t request; /* 消息类型 */
uint32_t flags; /* 标志位 */
uint32_t size; /* 负载大小 */
/* 负载数据 */
union {
struct vhost_user_mem_regions regions[0];
uint64_t u64;
/* ... 其他类型 */
};
};
/* 内存区域描述结构 */
struct vhost_user_mem_region {
uint64_t guest_phys_addr; /* Guest 物理地址起始 */
uint64_t memory_size; /* 内存区域大小 */
uint64_t userspace_addr; /* 用户空间虚拟地址 */
uint64_t mmap_offset; /* mmap 偏移 */
};
2.5.2 VHOST_USER_SET_VRING_ADDR
该消息用于配置 virtqueue 的三个核心数据结构在共享内存中的位置:
- 描述符表(Descriptor Table):存储描述符数组,每个描述符指向一个数据缓冲区。
- 可用环(Available Ring):前端写入待处理描述符索引,后端读取。
- 已用环(Used Ring):后端写入已处理描述符索引,前端读取。
struct vhost_user_vring_addr {
uint32_t index; /* virtqueue 索引 */
uint32_t flags; /* 标志位 */
uint64_t desc_user_addr; /* 描述符表地址(GPA) */
uint64_t avail_user_addr; /* 可用环地址(GPA) */
uint64_t used_user_addr; /* 已用环地址(GPA) */
};
2.6 协议交互中的关键机制
- VIRTIO_F_VERSION_1 (bit 32):表示支持 virtio 1.0 标准。
- VIRTIO_F_INDIRECT_DESC (bit 28):支持间接描述符,减少描述符表占用。
- VIRTIO_F_EVENT_IDX (bit 29):支持事件索引,减少通知次数。
- VHOST_USER_F_PROTOCOL_FEATURES (bit 30):支持 vhost-user 协议特性协商。
文件描述符传递:vhost-user 协议利用 Unix Domain Socket 的辅助数据(cmsg)传递文件描述符。SET_VRING_KICK 和 SET_VRING_CALL 消息分别传递 eventfd 文件描述符,用于实现前后端之间的异步通知机制。这种设计避免了轮询带来的 CPU 开销。
内存映射与地址转换:后端在收到 SET_MEM_TABLE 消息后,会通过 mmap 系统调用将 Guest 物理内存映射到自己的地址空间。映射完成后,后端维护一张 GPA→HVA 的转换表。在数据路径中,后端通过该转换表将描述符中的 GPA 转换为本地可访问的 HVA,从而实现零拷贝数据访问。