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 能够达到接近线速的转发性能。

下图展示了 vhost-user 控制通道与数据通道的分离架构:

vhost-user 控制通道与数据通道分离架构 vhost-user 控制通道与数据通道分离架构 Guest VM virtio-net 驱动 (前端驱动) virtqueue 对 (描述符表 + 可用环 + 已用环) Guest 物理内存 共享内存区域 (SHM) 数据通道:直接读写 virtqueue (零拷贝数据路径) 控制通道:Unix Domain Socket (消息传递) vhost 后端 DPDK vhost (后端实现) virtqueue 处理 (描述符解析) 内存映射表 数据流 控制流

从架构图可以看出,控制通道与数据通道在物理和逻辑上完全独立:

  • 控制通道:通过 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 协议的交互流程可以分为三个阶段:设备初始化阶段、运行阶段、设备销毁阶段。下面通过流程图展示完整的交互过程:

vhost-user 消息交互流程 vhost-user 消息交互流程 QEMU(前端) DPDK vhost(后端) 阶段1:设备初始化 VHOST_USER_SET_OWNER VHOST_USER_GET_FEATURES → 返回特性位图 VHOST_USER_SET_FEATURES(协商后特性) 阶段2:内存映射 VHOST_USER_SET_MEM_TABLE(传递内存映射表) 阶段3:队列配置 VHOST_USER_SET_VRING_NUM(队列深度) VHOST_USER_SET_VRING_ADDR(队列地址) VHOST_USER_SET_VRING_BASE(初始索引) 阶段4:通知机制设置 VHOST_USER_SET_VRING_KICK(kick fd) VHOST_USER_SET_VRING_CALL(call fd) 阶段5:运行阶段(数据通道)
注意事项:在阶段3(队列配置)中,VHOST_USER_SET_VRING_ADDR 消息携带的地址是 Guest 物理地址(GPA),后端需要根据阶段2建立的内存映射表将其转换为本地虚拟地址。如果映射表建立失败或地址转换错误,将导致数据路径异常。

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 协议交互中的关键机制

特性协商机制:vhost-user 协议通过位图方式进行特性协商。前端和后端各自维护一个 64 位特性位图,通过 GET_FEATURES/SET_FEATURES 消息进行协商。只有双方都支持的特性才能被启用。常见特性包括:
  • 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,从而实现零拷贝数据访问。

性能关键点:地址转换表的查找效率直接影响数据路径性能。DPDK vhost 实现中采用红黑树(rbtree)或哈希表来加速 GPA→HVA 的转换,确保每次数据包处理时的地址转换开销在纳秒级别。