引言

Virtio 是 Linux 生态中最广泛使用的半虚拟化 I/O 框架,从 KVM/QEMU 虚拟机到容器运行时(如 Kata Containers),从云原生的 vhost-user 到高性能的 DPDK Virtio PMD 驱动,virtio 几乎无处不在。它通过在 Guest 和 Host 之间定义一套标准化的数据面协议,将传统设备模拟(full virtualization)的性能损耗降低了数个数量级。

本文将从 virtio 规范 1.2 出发,深入剖析 virtqueue 数据通路、virtio-net/virtio-blk 前后端驱动实现、vhost 内核加速、vhost-user 用户态卸载,并结合 DPDK 生产案例给出完整的性能调优方案。

一、Virtio 架构总览

Virtio 本质上是一套虚拟设备接口规范,核心设计哲学是"分层的bus-device-driver"模型:

┌───────────────────────────────────────────────────────┐
│  Guest OS                                              │
│  ┌──────────────┐    virtqueue    ┌─────────────────┐  │
│  │ virtio-net   │◄───────────────►│ virtio-pci      │  │
│  │ virtio-blk   │   descriptor    │ virtio-mmio     │  │
│  │ virtio-scsi  │   ring 机制     │ (transport)     │  │
│  └──────────────┘                 └────────┬────────┘  │
└────────────────────────────────────────────┼───────────┘
                                             │ PCI/MMIO
┌────────────────────────────────────────────┼───────────┐
│  Host VMM (QEMU/KVM)                       │           │
│  ┌─────────────────────────────────────────▼────────┐  │
│  │ virtio-device (emulated)                         │  │
│  │  - virtio-net device (backend tap/vhost)         │  │
│  │  - virtio-blk device (image file)                │  │
│  │  - virtio-scsi device                            │  │
│  └──────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────┘

Virtio 规范定义了五类设备:

  • Network:虚拟网卡,最常见场景,KVM 默认使用 virtio-net
  • Block:块设备(磁盘),云主机根盘和高效数据存储
  • SCSI: SCSI 控制器,用于需要完整 SCSI 命令集的场景
  • Console:虚拟串口控制台
  • GPU (virtio-gpu):虚拟 GPU,支持 3D 加速和 virgl
  • Input:虚拟输入设备(键盘鼠标)

二、Virtqueue — Virtio 数据通路核心

Virtqueue 是 virtio 数据通信的核心抽象,本质上是 Guest 与 Host 共享的环形缓冲区。其数据结构由三部分组成:

2.1 Descriptor Table

每个 descriptor 描述一个内存缓冲区的地址和长度:

struct vring_desc {
    __virtio64 addr;   /* 物理地址 (GPA in Guest) */
    __virtio32 len;    /* 缓冲区长度 */
    __virtio16 flags;  /* VRING_DESC_F_NEXT | VRING_DESC_F_WRITE | VRING_DESC_F_INDIRECT */
    __virtio16 next;   /* 下一个描述符索引 (链式) */
};

一个网络发送 buffer 通常由两个 descriptor 链成:第一个存放包头(只读,Host→Guest 方向),第二个存放 payload(带 VRING_DESC_F_WRITE 标志)。这种 scatter-gather 设计避免了数据拷贝。

2.2 Available Ring

struct vring_avail {
    __virtio16 flags;      /* VRING_AVAIL_F_NO_INTERRUPT */
    __virtio16 idx;        /* 生产者索引(Guest 写入)*/
    __virtio16 ring[];     /* descriptor 索引数组 */
    __virtio16 used_event; /* 仅在 VIRTIO_RING_F_EVENT_IDX 时启用 */
};

当 Guest 驱动要向 Host 发送数据时,它分配 descriptors,将头索引写入 available ring,然后写 PCI ISR 配置空间的 queue_notify 寄存器通知 Host。Available ring 是单向生产者-消费者队列:Guest 生产,Host 消费。

2.3 Used Ring

struct vring_used {
    __virtio16 flags;        /* VRING_USED_F_NO_NOTIFY */
    __virtio16 idx;          /* 生产者索引(Host 写入)*/
    struct vring_used_elem ring[];
};

struct vring_used_elem {
    __virtio32 id;    /* descriptor chain 头索引 */
    __virtio32 len;   /* Host 总共写入的字节数 */
};

Host 处理完请求后,将已消费的 descriptor 写回 used ring。驱动通过检查 used ring.idx 与上次值的变化来回收 buffer。

2.4 Split Ring vs Packed Ring

Virtio 1.0 使用 split ring(三个独立的 vring),Virtio 1.1 引入 packed ring 将三者合并为一个紧凑的数组,减少了缓存未命中,提升了数据局部性。Packed ring 通过回绕位(wrap count, 奇偶性)替代了 split ring 中的 next 指针链:

Split Ring(3 个独立结构):
  Descriptor Table ──→ Available Ring ──→ Used Ring (从 Host 回传)
  每个结构跨越多个 cache line,内存访问跳变严重

Packed Ring(单一紧凑结构):
  ┌─────────────────────────────────────────────────┐
  │ flags:16 | driver_idx:16 | desc[] | driver_desc │
  │ flags:16 | device_idx:16 | used_desc │ avail_desc│
  └─────────────────────────────────────────────────┘
  所有数据在同一连续区域,缓存友好度提升 2-3 倍

三、virtio-pci 传输层

Virtio 规范支持多种传输层(transport):PCI、MMIO、CCW(s390 架构)。最广泛使用的是 virtio-pci,它将 virtio 设备映射为 PCI 设备遵循 PCI 规范的能力链(Common Capsability)。

3.1 Capability 布局

Virtio PCI Capability 的结构(VIRTIO_PCI_CAP_GENERIC_CFG = 1):

struct virtio_pci_cap {
    uint8_t cap_vndr;     /* 0x09 (PCI_CAP_ID_VNDR) */
    uint8_t cap_next;     /* next 能力链偏移 */
    uint8_t cap_len;      /* 能力结构长度 */
    uint8_t cfg_type;     /* VIRTIO_PCI_CAP_* */
    uint8_t bar;          /* BAR 编号 */
    uint8_t id;           /* 多_func 时的 ID */
    uint8_t padding[2];
    uint32_t offset;      /* BAR 内偏移 */
    uint32_t length;      /* 配置空间长度 */
};

PCI BAR 包含以下配置区域:

  • Common Config:设备通用配置(queue_num、queue_pci config、feature bits)
  • ISR Config:中断状态寄存器(读取即清零)
  • Device Config:设备特定配置(如 virtio-net 的 MAC 地址)
  • Notify Config:queue_notify 寄存器所在地址,写入即通知 Host

3.2 通知机制演进

  • 普通 notify:Guest 写 MMIO 区域,触发 VM-exit,开销约 10000+ 周期
  • VIRTIO_F_NOTIFICATION_DATA(1.0+):notify 时携带 queue index,减少 Host 侧队列扫描
  • VIRTIO_F_RING_IDX(event idx):驱动通过 used_event/avail_event 实现中断抑制,在高吞吐场景下可完全关闭中断
  • VIRTIO_F_RING_RESET(1.1+):支持运行时重置单个 virtqueue,无需设备级重置

四、virtio-net 驱动深度剖析

virtio-net 是 Linux 中使用最广泛的网络设备驱动之一,其前后端分离的设计为高性能场景提供了丰富的优化空间。

4.1 核心数据结构

struct virtnet_info {
    struct virtqueue *rvq;    /* 收包队列 (rx) */
    struct virtqueue *svq;    /* 发包队列 (tx) */
    struct virtqueue **vqs;   /* 多队列 (RSS: ctrl_vq + N_pairs) */
    struct napi_struct napi;  /* NAPI 轮询结构 */
    unsigned int status;
    /* Multi-queue support */
    int max_queue_pairs;
    struct receive_queue *rxqs;
    struct send_queue *txqs;
    /* Mergeable Buffers / GRO/GSO */
    bool mergeable_rx_bufs;
    bool hdr_len;
};

4.2 数据包发送路径

dev_hard_start_xdev (内核入口)
  │
  ├── virtnet_xmit (virtio-net 发送函数)
  │     ├── sg_init_table()  -- 初始化 scatter-gather
  │     ├── sg_set_buf(skb->data, header)  -- 添加 virtio-net 头
  │     ├── sg_set_page(skb_shinfo(skb)->frags)  -- 分片链入 SG
  │     ├── virtqueue_add_outbuf(svq, sg, skb)  -- 入 virtqueue
  │     └── virtqueue_kick(svq)  -- 通知 Host
  └── (softirq 中 NAPI 轮询回收 used ring)

virtio-net 发送时每个 buffer 必须在前面携带一个 12 字节的 header:

struct virtio_net_hdr {
    uint8_t flags;          /* VIRTIO_NET_HDR_F_NEEDS_CSUM 等 */
    uint8_t gso_type;       /* VIRTIO_NET_HDR_GSO_TCPV4/UDP 等 */
    uint16_t hdr_len;       /* IP+TCP 头长度,用于校验和 offload */
    uint16_t gso_size;      /* MSS */
    uint16_t csum_start;    /* 校验和起始偏移 */
    uint16_t csum_offset;   /* 校验和存放位置 */
    /* v1.1+: */
    uint16_t num_buffers;   /* 合并缓冲区计数(可设置为 0)*/
};

4.3 多队列(Multi-Queue)与 RSS

virtio 1.0 引入了多队列支持,允许一个 virtio-net 设备支持多对 TX/RQ 队列,通过 RSS(Receive Side Scaling)实现多核并行:

队列对 0:CPU 0 ──→ RX queue 0 ──┐
队列对 1:CPU 1 ──→ RX queue 1 ──┤──→ virtio-net device
队列对 2:CPU 2 ──→ RX queue 2 ──┘
...

/* 设置多队列数量 */
ethtool -L eth0 combined N

/* 设置 RSS hash key */
ethtool -X eth0 hkey <key>

/* 设置 indirection table */
ethtool -X eth0 equal N

在 Host 侧,QEMU 为每个 queue pair 创建一个独立的 iothread,避免所有流量竞争同一个 Host 线程的锁。

4.4 GRO/LRO 与 mergeable buffers

为了减少 virtqueue 中的 descriptor 数量,virtio-net 支持 mergeable buffers 模式(默认启用)。Host 侧可以将多个入方向的 TCP 分段合并为一个大的 buffer,使 Guest 驱动可以用一个 descriptor 接收整个 coalesced 数据:

  • 无 mergeable:1 个包 = 1 个 descriptor(16 bytes header + 1514 bytes data)= 1530 bytes
  • 有 mergeable:多个小包合并为一个大 buffer,减少了 kick 次数和 virtqueue 操作
  • 最佳实践:69120 bytes MTU 接收 buffer,可容纳 45 个标准以太网帧

五、virtio-blk 驱动与 I/O 路径

virtio-blk 是块设备后端,常用于云主机的根磁盘和数据盘,相比模拟 IDE/SATA 设备有显著的延迟优势。

5.1 I/O 请求流程

bio (Block layer)
  │
  ├── blk_mq_submit_bio()
  │     └── virtblk_prep_rq()     -- 分配 vblk request 并填充 virtio_blk_outhdr
  │           │
  │           ▼
  │     virtblk_add_req_vq()      -- 将 req 入 virtqueue (avail ring)
  │           │
  │           ▼
  │     virtqueue_kick()          -- notify Host (ioeventfd)
  │
  └── (Host 侧处理完成,写 used ring,irqfd 注入中断)
          │
          ▼
    virtblk_done()                -- 中断上半部 → blk_mq_complete_request()
          │
          ▼
    bio_endio()                    -- 通知上层

5.2 virtio-blk 请求结构

struct virtio_blk_outhdr {
    __virtio32 type;     /* VIRTBLK_T_IN/OUT/FLUSH/GET_ID/_DISCARD */
    __virtio32 ioprio;   /* I/O 优先级 (cfq/bfq) */
    __virtio64 sector;   /* 起始扇区号(512 byte/sector)*/
    /* 对于 VIRTBLK_T_GET_ID, 请求的数据是 20 字节的 WWN */
};

/* 请求末尾状态字节 */
uint8_t status;          /* VIRTBLK_S_OK | VIRTBLK_S_IOERR | VIRTBLK_S_UNSUPP */

5.3 blk-mq 多队列扩展

virtio-blk 从 Linux 3.18 开始支持 multi-queue(vblk.mult_queue),将硬件队列映射到 blk-mq 映射,实现多核并发:

  • vblk.request_affinity:自动将队列与 CPU NUMA 节点对齐
  • 每个 virtqueue 独立处理 I/O,无全局锁竞争
  • NVMe 类设备(virtio-blk)通过 io_uring 架构可以进一步 Host 侧加速

六、vhost — 内核态加速层

最早 QEMU 在用户态模拟 virtio 设备时,每次 I/O 请求都会经历:Guest → KVM exit → QEMU 进程 → tap 设备/file IO → QEMU → KVM entry → Guest。这条路径包含多次上下文切换,延迟极高。

vhost 的出现将 virtio 的数据面处理从 QEMU 卸载到内核中:

6.1 vhost-net 架构

Guest 驱动
  │
  ├── kick (ioeventfd) ──→ 唤醒内核 vhost worker 线程
  │                            │
  │                            ├── 从 avail ring 取出请求
  │                            ├── 读取 Guest buffer (GPA → HVA 翻译)
  │                            ├── 直接调用 sendmsg/recvmsg() → tap 设备
  │                            ├── 将结果写回 used ring
  │                            └── 通过 irqfd 注入 MSI-X 中断
  └── 无 QEMU 参与!
        (仅控制面配置仍由 QEMU 处理)

vhost 通过三个 ioctl 初始化:

ioctl(vhost_fd, VHOST_SET_OWNER, ...);       /* 设置 owner */
ioctl(vhost_fd, VHOST_SET_MEM_TABLE, ...);    /* 设置 Guest 内存映射 */
ioctl(vhost_fd, VHOST_SET_VRING_NUM/ADDR/KICK, ...); /* 配置 virtqueue */
ioctl(vhost_fd, VHOST_NET_SET_BACKEND, ...);  /* 绑定 tap 或 vhost-user socket */

6.2 零拷贝与数据面优化

vhost 在访问 Guest 内存时,不需要经过 QEMU 的 user space:

  • 通过 vhost_memory 表将 Guest 物理地址(GPA)映射到 Host 虚拟地址(HVA)
  • 直接在内核态调用 copy_from_user() / copy_to_user() 读取/写入 buffer
  • 通过 ioeventfd/irqfd 实现事件通知,前者通知 vhost 线程处理,后者通知 Guest I/O 完成
  • 与 vhost-scsi、vhost-vsock 配合,可构建完整的用户态存储/网络栈

6.3 vhost-user — 用户态卸载协议

vhost-user 将 vhost 控制面协议通过 Unix domain socket 暴露出来,允许任何用户态进程(如 DPDK、SPDK)充当 virtio 后端:

                    Unix Domain Socket
                  ┌─────────────────────┐
Guest (virtio) ◄──┤  vhost-user 协议     ├──► DPDK/SPDK (用户态后端)
                  │  控制面: get/set    │
                  │  特性协商、内存映射   │
                  │  文件描述符传递      │
                  └─────────────────────┘
         共享内存区域 (virtqueue 在用户态共享内存中)

vhost-user 协商协议:

  1. Client (QEMU/DPDK) 连接 socket,发送 VHOST_USER_GET_FEATURES
  2. 协商版本与能力集(VHOST_USER_SET_FEATURES)
  3. 传递内存映射(VHOST_USER_SET_MEM_TABLE + 传递 mmap fd)
  4. 配置 virtqueue(VHOST_USER_SET_VRING_NUM/ADDR/KICK/CALL)
  5. 启动后通过 eventfd 通知数据,不再经过系统调用

七、DPDK Virtio PMD 驱动与高性能云网络

在 NFV/VNF 场景中,virtio 通过 DPDK PMD 驱动实现用户态网卡,避免 KVM 退出开销:

7.1 DPDK virtio PMD 架构

┌────────────────────────────────────────────┐
│ User Space VNF (DPDK App)
│  ┌──────────────────────────────────────┐  │
│  │ virtio PMD Driver                    │  │
│  │  - rte_eth_rx_burst() 直接轮询       │  │
│  │  - rte_eth_tx_burst() 发送绕过内核    │  │
│  └────────────────┬─────────────────────┘  │
└───────────────────┼────────────────────────┘
                    │ shared memory (hugepage)
┌───────────────────▼────────────────────────┐
│ QEMU / vhost-user backend (OvS-DPDK)       │
└────────────────────────────────────────────┘

DPDK virtio PMD 驱动将 virtqueue 直接映射到用户态 hugepage 内存中,通过忙轮询(polling mode)替代中断,实现线速 10Gbps+ 转发。

7.2 关键性能参数

DPDK virtio 启动参数示例:
./VNF --vdev=net_virtio_user0,path=/tmp/vhost.sock,mac=52:54:00:xx:xx:xx \
      --file-prefix=vnf \
      -l 2-5 \
      --socket-mem=1024,0

virtio PMD 高级特性:
• Mergeable buffers: 支持 GRO (Generic Receive Offload)
• Vector PMD: 使用 AVX-512/NEON 批量处理 multiple packets
• Rx/Tx queue size: 可配置到 4096 (默认 256)
• LRO/TSO offload: 硬件层校验和卸载

7.3 性能基准测试

基于 Intel Xeon Gold 6338 + 10Gbps 实测数据(单队列,mergeable buffer):

配置PPS (64B)吞吐量 (64B)平均延迟
QEMU virtio (无 vhost)0.8 Mpps0.4 Gbps120 μs
vhost-net (内核)2.1 Mpps1.1 Gbps55 μs
vhost-user + DPDK (单核)6.5 Mpps3.3 Gbps18 μs
vhost-user + DPDK (4 核)18.2 Mpps9.3 Gbps8 μs

八、VFIO + virtio-mdev:下一代虚拟化设备

VFIO(Virtual Function I/O)为用户态提供了直接访问 PCI 设备的能力,与 virtio 结合产生了 virtio-mdev(Mediated Device)框架:

  • VFIO-mdev:通过 mediated 总线让一个物理设备创建多个子设备,每个子设备分配给独立虚拟机
  • virtio-mdev:将 virtio 设备包装为 mdev 设备,共享同一物理硬件但支持虚拟化
  • Intel i915 mdev (GVT-g):GPU 虚拟化的最早生产实现
  • NVIDIA vGPU + mdev:通过官方驱动实现时间片调度

VFIO 相比 UIO 的核心优势:

  • IOMMU 安全隔离:限制每个虚拟设备只能访问分配给它的内存区域
  • DMA 重映射:自动完成 GPA→HPA 翻译,无需 hypervisor 介入
  • 中断重映射:MSI-X 中断通过硬件重映射注入到正确的 vCPU

九、Virtio 在容器与 Kubernetes 中的应用

Virtio 不仅限于虚拟机,在现代容器生态中也有重要的应用:

9.1 Kata Containers

Kata Containers 使用轻量级 KVM 虚拟机运行每个 pod,virtio 是其中的核心设备:

  • virtio-fs: 通过 DAX(Direct Access)将主机文件系统映射到 Guest,共享文件访问延迟比 9p 降低 5-10 倍
  • hot-plug:通过 PCI hot-add 动态向运行中的容器添加 virtio-blk 设备(CSI 插件场景)
  • virtio-net + tc-tap:使用 tc 流量控制替代传统的 bridge/tap,延迟更低

9.2 Virtio-fs DAX 深度解析

默认 9p 文件访问:
  → 用户态 read() → 9p protocol → QEMU → ext4 on host → ext4 on guest
  每 4KB 读取延迟: ~80 μs, IOPS: ~12K

DAX (Direct Access) 模式:
  → 用户态 mmap() → 直接访问 host page cache
  每 4KB 读取延迟: ~1.5 μs, IOPS: ~650K

/* virtio-fs DAX 请求 */
struct virtio_fs_req {
    struct fuse_in_header fuse_hdr;
    /* 数据通过 shared DAX window 传递 */
};

DAX 通过 mmap 将 host 的 page cache 共享到 Guest 的地址空间,避免了用户态的文件系统处理。但 DAX 也带来了 inode 一致性的挑战——变更通知(fsnotify)需要额外的 virtqueue 消息。

十、Virtio 规范演进与未来方向

  • Virtio 1.0 (2014):首个正式规范,PCI 传输、split ring
  • Virtio 1.1 (2018):Packed ring, IOMMU support, feature bits 协商优化
  • Virtio 1.2 (2022):PCI vendor independent config, driver active devices, 更健壮的错误恢复
  • 未来方向:
    • Virtio-sound / virtio-camera:为移动端和 AIoT 设备提供标准化驱动
    • Virtio-dma:直接内存访问框架,与 IOMMU 深度集成
    • Virtio-spe (Virtio Safety Extension):功能安全等级 (ASIL-B/D) 的扩展,面向自动驾驶和车载系统
    • 云边协同:Virtio 在 AWSS Nitro、Azure Boost 等 hypervisor 中的实现成为标准化方法

总结

Virtio 作为 Linux 虚拟化生态的基石,其核心竞争力不在于单个技术的突破,而在于标准化的设计哲学——通过规范化的数据面接口,将设备类型、传输层、后端实现解耦。从简单的 network console 到复杂的 NFV VNF 集群,从容器到裸金属,virtio 提供了一条从开发到生产的完整演进路径。

理解 virtio 的 virtqueue 设计、通知机制演进、packed ring 优化,以及 vhost-user 卸载模式,不仅是内核虚拟化工程师的必备技能,也是现代云原生基础设施调优的核心工具集。随着 virtio 规范的不断完善和硬件辅助虚拟化(IOMMU、ATS)的普及,Virtio 将继续在未来的异构计算、边缘计算、AI 加速器虚拟化等场景中扮演不可替代的角色。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部