Skip to content

VirtIO

VirtIO 是一套实现虚拟化设备的通用协议,它是 hypervisor 和操作系统驱动之间沟通的桥梁。只要遵循 VirtIO 协议进行设备模拟,就能坐享海量生态资源,实现虚拟设备自由。

架构概述

VirtIO 采用前后端分离、数据面和管控面分离的架构。

前后端分离架构

VirtIO 采用前后端分离的设计模式,将设备功能分为两个独立的部分:

  • 前端(Frontend)运行在 Guest OS 中的驱动程序,负责与应用程序交互,处理 I/O 请求,并管理 virtqueue 和内存映射。
  • 后端(Backend)运行在 Hypervisor 中的设备模拟实现,负责实际的硬件操作和设备模拟,处理来自前端的请求并返回结果。
  • 前后端通过 virtqueue 实现数据交换,采用共享内存和环形缓冲区机制,支持异步通知和批量处理。

生态优势:Linux 默认自带 VirtIO 协议族驱动,QEMU 支持大量 VirtIO 设备,使用 VirtIO 模型可以复用大量现有代码。

数据面与控制面分离

数据面(Data Plane)负责实际的数据传输和处理,通过 virtqueue 进行批量数据传输,采用零拷贝技术减少内存拷贝开销,使用内存映射提供高效的内存访问机制,并通过批量处理、异步通知和多队列并行等优化手段提升性能。

控制面(Control Plane)负责设备的管理和控制,包括状态监控、配置更新、错误处理等设备管理功能,前后端特性协商和协议版本管理等特性协商功能,以及支持协议扩展和新功能的扩展支持。

Virtqueue 和 Vring

VirtIO 通过 virtqueue 数据结构进行前后端的交互,其核心机制是内部的 vring 数据结构。一个 virtqueue 对应一个 vring,一个 vring 是一个数组,被分成三个独立部分:

  1. Descriptor Table(描述符表)

    • 数据准备区
    • 只能由前端写入交互信息
    • 后端只读
    • 包含数据缓冲区的地址和长度信息
  2. Available Ring(可用环)

    • 前端通知区
    • 前端可写,后端只读
    • 用于前端通知后端有新的请求待处理
  3. Used Ring(已用环)

    • 后端通知区
    • 后端可写,前端只读
    • 用于后端通知前端请求处理完成

环形缓冲区机制

环形缓冲区通过索引管理确保前后端数据安全。Available Ring 和 Used Ring 各自维护一个 idx 索引,这些索引像指针一样标记环形缓冲区的写入位置,分别由前端和后端独立维护,确保双方不会覆盖彼此的数据。

环形设计使得前后端可以在各自的环中独立推进。Available Ring 和 Used Ring 都采用环形结构,前端可以在 Available Ring 中填入新请求,后端在 Used Ring 中标记已完成的任务,双方互不干扰地并行工作。

通知机制采用异步方式提升效率。前端通过"kick"操作通知后端有新请求待处理,后端通过中断机制通知前端任务完成,这种异步通知避免了忙等待,显著提升了并发处理效率。

多队列支持

一个设备可以创建多个队列,每个队列彼此独立,由前后端在通信时并行使用,从而提高设备的吞吐效率。

VirtIO 设备支持多种队列类型以满足不同性能需求:单队列适用于简单设备的基本需求,一个 virtqueue 是半双工的;双队列通过分别设置 tx_queue(发送队列)和 rx_queue(接收队列)实现双向全双工数据传输,提高传输效率;多队列采用负载均衡分发策略,显著提升设备和驱动间的吞吐能力,特别适合高性能网络设备场景。

注意:对于网络设备,传输速度取决于整条链路上的瓶颈点,单一一处的吞吐量大不能保证网速的提升。

交互流程

设备初始化

  1. 设备发现
    • Guest OS 通过 PCI 或 MMIO 发现 VirtIO 设备
    • 读取设备配置空间获取设备信息
  2. 设备配置
    • 协商特性(Feature bits)
    • 设置设备参数
    • 分配 virtqueue
  3. 驱动加载
    • 加载对应的 VirtIO 驱动
    • 初始化驱动数据结构
    • 建立与设备的通信通道
  4. 设备就绪
    • 完成所有初始化步骤
    • 设备进入工作状态
    • 可以开始处理 I/O 请求

前后端通信流程

  1. 数据发送流程
    • 前端准备数据缓冲区
    • 将缓冲区信息写入 Descriptor Table
    • 更新 Available Ring 的索引
    • 发送 kick 通知后端
  2. 数据接收流程
    • 后端处理请求
    • 将结果写入 Used Ring
    • 发送中断通知前端
    • 前端处理完成通知

VirtIO 设备类型

  1. 网络设备(VirtIO-net)
    • 虚拟网卡
    • 支持多队列
    • 支持 TSO/GSO
  2. 块设备(VirtIO-blk)
    • 虚拟磁盘
    • 支持多队列
    • 支持 DISCARD/WRITE_ZEROES
  3. 控制台设备(VirtIO-console)
    • 虚拟串口
    • 支持多端口
    • 支持流控制
  4. 输入设备(VirtIO-input)
    • 虚拟键盘/鼠标
    • 支持事件上报
    • 支持多点触控
  5. GPU 设备(VirtIO-gpu)
    • 虚拟显卡
    • 支持 2D/3D 加速
    • 支持显示输出

性能优化

  • 批处理技术通过合并多个 I/O 请求来减少前后端交互次数,从而显著提高系统吞吐量。当多个小请求同时到达时,系统可以将它们打包成一个批量请求进行处理,避免了频繁的上下文切换和通信开销。
  • 零拷贝技术通过避免数据在内存中的复制操作来降低 CPU 开销。VirtIO 利用共享内存机制,让前后端直接访问同一块内存区域,数据无需在用户空间和内核空间之间来回拷贝,大大提升了数据传输效率。
  • 中断合并机制将多个中断事件合并为一个中断进行处理,有效减少了中断处理的开销。当短时间内有多个 I/O 完成事件时,系统可以等待一段时间后将它们合并发送,避免频繁的中断处理,提高系统的整体响应性。
  • 轮询模式在特定场景下替代中断机制,通过主动查询设备状态来减少中断开销。对于低延迟要求的场景,轮询模式可以避免中断处理的延迟,提供更可预测的响应时间,特别适合对实时性要求较高的应用场景。

安全考虑

  • 内存隔离:前后端内存空间隔离,防止越界访问,保护敏感数据
  • 权限控制:设备访问权限管理,资源配额限制,防止资源耗尽
  • 数据加密:敏感数据传输加密,密钥管理,安全协议支持

VirtIO 虚拟化设备协议

VirtIO 是一种虚拟化环境中 I/O 设备的标准化接口协议。与传统的设备虚拟化方案不同,VirtIO 将设备的功能抽象为一套标准化的接口,使得虚拟机可以使用统一的驱动程序访问不同虚拟化平台提供的虚拟设备。VirtIO 最初由 Rusty Russell 为 KVM (Kernel-based Virtual Machine) 开发,现在已成为虚拟化领域的事实标准。

传统虚拟化的困境

在没有 VirtIO 的时代,虚拟化 I/O 设备主要通过全模拟或半虚拟化两种方式实现。全模拟方式由虚拟化模拟完整的硬件设备,如 E1000 网卡、IDE 磁盘控制器,虚拟机使用普通的硬件驱动程序访问这些设备。这种方式的优势是虚拟机无需修改,但性能较差,因为每次 I/O 操作都需要陷入虚拟机监控器,由软件模拟硬件行为。

半虚拟化方式通过修改虚拟机操作系统,使用优化的接口与虚拟机监控器通信。这种方式可以显著提升性能,但每个虚拟化平台都需要开发自己的驱动程序,而且虚拟机需要针对不同平台编译不同的版本。这种方式在 Xen 等虚拟化平台中广泛使用,但缺乏通用性。

VirtIO 的出现解决了这个问题。它定义了一套标准的虚拟设备接口,虚拟机监控器按照这个标准实现设备后端,虚拟机使用统一的 VirtIO 驱动访问设备。这种方式既保持了半虚拟化的高性能,又实现了跨平台的兼容性。

VirtIO 架构

VirtIO 采用前后端分离的架构。前端是虚拟机中的 VirtIO 驱动,后端是虚拟机监控器中的设备模拟实现。前后端通过 VirtQueue (虚拟队列) 进行通信,VirtQueue 是 VirtIO 的核心抽象。

VirtQueue 本质上是一个共享内存环形队列 (vring),前端和后端都可以访问这段共享内存。前端将请求描述符放入队列,后端从队列取出请求处理,处理完成后将响应放入队列。整个过程中数据的拷贝通过共享内存实现,避免了频繁的陷入和退出。

VirtQueue 使用描述符表、可用环和已用环三个数据结构。描述符表包含了指向数据缓冲区的指针、数据长度和标志位。可用环由前端填充,指向待处理的描述符。已用环由后端填充,指向已完成的描述符。这种设计使得前端和后端可以并发工作,提高了吞吐量。

VirtIO 定义了多种设备类型,包括网络设备 (virtio-net)、块设备 (virtio-blk)、控制台设备 (virtio-console)、随机数生成器 (virtio-rng) 等。每种设备类型都有标准化的配置空间和队列布局,确保了不同实现之间的兼容性。

VirtIO 传输机制

VirtQueue 的实现依赖于共享内存,而共享内存在虚拟化环境中需要特殊的处理机制。在基于软件的虚拟化平台如 KVM 中,VirtQueue 通过影子页表或 EPT (Extended Page Table) 映射到虚拟机的物理地址空间。前端和后端通过内存映射访问同一个物理页面,实现零拷贝的数据传输。

通知机制是 VirtIO 的另一个关键问题。前端需要通知后端有新的请求放入队列,后端需要通知前端请求已处理完成。VirtIO 定义了多种通知机制,包括 virtio 现代接口使用的 MSI/MSI-X 中断、传统的 I/O 端口写入、以及基于 eventfd 的通知机制。

在 KVM 环境中,virtio-net 使用 vhost-net 内核模块加速数据平面。vhost-net 运行在内核空间,直接处理网络报文的收发,避免了用户空间和内核空间之间的上下文切换。这种设计使得 virtio-net 的性能可以接近物理网卡。

VirtIO 在云原生中的应用

VirtIO 不仅用于传统虚拟机,也是容器虚拟化的重要技术。Kata Containers、Firecracker 等安全容器方案使用轻量级虚拟机提供强隔离,同时使用 VirtIO 提供高效的 I/O 性能。这些方案使得容器可以获得接近虚拟机的安全性,同时保持接近容器的启动速度和资源效率。

在云环境中,VirtIO 使得虚拟机的迁移变得更加简单。由于 VirtIO 设备接口是标准化的,迁移源和迁移目标只需要实现相同的 VirtIO 后端,虚拟机的 VirtIO 驱动无需任何修改就可以在新平台上运行。这种可移植性大大简化了云平台的运维工作。

VirtIO 的性能优势

从性能角度看,VirtIO 相比全模拟设备有显著优势。首先,VirtQueue 的共享内存机制避免了频繁的数据拷贝,减少了内存带宽消耗。其次,VirtIO 的批量处理机制可以合并多个 I/O 请求,减少陷入次数。最后,vhost 等加速技术将数据平面放到内核空间,进一步降低了延迟。

实际测试表明,virtio-blk 的顺序读性能可以达到全模拟 IDE 设备的数倍,virtio-net 的网络吞吐量也显著高于模拟网卡。这种性能优势使得 VirtIO 成为云计算环境中虚拟机的标准 I/O 设备接口。

Linux 对 VirtIO 的支持

Linux 内核是 VirtIO 的主要支持者,包含完整的 VirtIO 前端驱动实现。从内核 2.6.25 开始,Linux 就加入了 VirtIO 驱动,现在 VirtIO 已成为 Linux 虚拟机的默认 I/O 设备。主流 Linux 发行版都预装了 VirtIO 驱动,这使得使用 VirtIO 不需要额外的配置。

Linux 的 VirtIO 驱动位于 drivers/virtio 目录下,包括核心框架和各设备类型的驱动。开发者可以参考这些驱动实现新的 VirtIO 设备类型,或者在自己的虚拟机监控器中实现 VirtIO 后端。VirtIO 的标准化设计使得这种扩展变得相对简单。