Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing

  VirtIO 是一种 IO 半虚拟化解决方案,它提供 Guest OS 与 Hypervisor 虚拟化设备之间的通信框架和编程接口。其主要的优势是能提高性能且减少跨平台带来的兼容性问题。本文重点结合 VirtIO 规范 1.1 版以及 Linux 中的源码来分析 VirtIO 框架。

本文是我自己学习虚拟化相关的记录,欢迎指正其中的错误及技术交流

虚拟化

  现代计算机系统通常被分成了自下而上的多个层次,每一个层次都向上一层次呈现一个抽象,每一层只需知道下层抽象的接口,而不需要了解其内部运作机制。虚拟化就是由位于下层的模块向上一层模块提供一个与它原先所期待的运行环境完全一致的接口的方法。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第1张图片
  虚拟化技术在云计算领域至关重要,随着云计算的迅速崛起,虚拟化技术也快速发展。Linux 中的虚拟化经历了从 I/O 全虚拟化、I/O 半虚拟化、硬件直通再到 vDPA 加速以及 Vhost-user 技术的演进。详细介绍参见独立博文 Linux Kernel 之十一 Linux 设备虚拟化技术的演进之路。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第2张图片

分类

  虚拟化技术起始于上世纪 70 年代的 IBM System/370 大型计算机,它的操作系统能为运行在同一计算机上的不同程序提供几个完整的虚拟环境。经过五十余年的发展,现在存在诸多实现在不同层次的虚拟化技术。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第3张图片

  • 硬件抽象层上的虚拟化是指通过虚拟硬件抽象层来为客户机操作系统呈现和物理硬件相同或相近的硬件抽象层的虚拟化
    • 也称为指令集级虚拟化(处理敏感指令)、系统级虚拟化、平台级虚拟化、硬件虚拟化
    • 通常是利用硬件(主要是 CPU)辅助处理敏感指令以实现完全虚拟化的功能,因此也称为硬件辅助虚拟化
    • 波佩克与戈德堡虚拟化需求(Popek and Goldberg virtualization requirements)是一组用于验证某一计算机体系结构可否被有效虚拟化的充分条件
    • 硬件抽象层上的虚拟化有许多不同的具体实现方案,按照实现方法的不同,可划分为如下几个类别。
      Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第4张图片
      • 仿真就是将所有指令都采用模拟来实现,就是模拟出每条指令执行的效果。
        • 性能比较低
        • 典型实现是 Bochs、QEMU
      • 完全虚拟化是在客户机和硬件之间加了一个名为 Hypervisor 的软件层
        • 最初的虚拟化技术就是完全虚拟化
        • 客户机操作系统察觉不到是运行在一个虚拟平台上
        • 早期,没有硬件虚拟化技术,则采用纯软件的二进制代码翻译(Binary Translation)法来实现完全虚拟化
        • 后来 CPU 厂家添加了硬件虚拟化,Intel 的 VT-x 和 AMD 的 AMD-V 是这一方向的代表
        • 典型代表 VirtualBox、KVM、VMware Workstation、VMware vSphere、Xen(也支持全虚拟化)。
      • 准虚拟化又称为半虚拟化,它是在全虚拟化的基础上,把客户操作系统进行了修改(实现一个专用的驱动),增加了一些专门的 API,来单独处理一些功能
        • 准虚拟化的出现主要是为了优化全虚拟化的性能不足(目前情况来说,全虚拟化性能(尤其是 x64 平台)并不比准虚拟化差)
        • 需要对操作系统进行或多或少的修改
        • 客户机操作系统知道其运行在虚拟平台上
        • 典型代表 Xen 、微软的 Hyper-V、Linux 中 KVM 的 VirtIO
  • 操作系统层上的虚拟化是指操作系统的内核可以提供多个互相隔离的用户态实例
    • 也叫做轻量级虚拟化,进程虚拟化
    • 用户态实例通常被称为容器
    • 典型代表 docker、Parallels Virtuozzo Containers、OpenVZ、LXC 以及类 Unix 系统上的 chroot,Solaris 上的 Zone,FreeBSD 上的 FreeBSD jail
  • 库函数层上的虚拟化就是通过虚拟化操作系统的应用级库函数的服务接口,使得应用程序不需要修改,就可以在不同的操作系统中无缝运行,从而提高系统间的互操作性
    • 典型代表 Wine、WSL(Windows Subsystem for Linux)
  • 编程语言层上的虚拟机称为语言级虚拟机
    • 典型代表 JVM(Java Virtual Machine)、Microsoft . NET、Parrot

VMM

  在硬件抽象层上的虚拟化中,虚拟环境的提供者被称为 Host(宿主机),使用虚拟的环境的一方则称为 Guest(客户机),而负责管理虚拟环境的这个软件程序被称为虚拟机监控器(Virtual Machine Monitor,VMM)。

VMM 通过陷入再模拟法来模拟执行引起异常的敏感指令

  VMM 也称为 Hypervisor 或虚拟机管理器(Virtual Machine Manager)。 Gerald J. Popek 和 Robert P. Goldberg 在他们 1974 年的文章中将 Hypervisor(在早期计算机界,操作系统被称为 Supervisor,而用来提供虚拟化的这个中间层则被称为 Hypervisor) 分为 Type I 和 Type II 两种类型。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第5张图片

  • Type I: 裸机 Hypervisor,也称为原生 Hypervisor,这些虚拟机管理程序直接运行在宿主机的硬件上来控制硬件和管理客操作系统
    • 此类型的 Hypervisor 可以被看做是一个为虚拟化而设计的完备的操作系统。与 Linux、Windows 等传统不同,它重点是为了提供多种虚拟环境
    • 典型代表 VMware ESX 服务器版本、Xen 3.0 及以后版本、Virtual PC 2005、KVM
  • Type II: 托管 Hypervisor,也称为寄居 Hypervisor,这些虚拟机管理程序运行在传统的操作系统上,就像其他计算机程序那样运行
    • 宿主机操作系统是 Windows 、Linux 等传统操作系统
    • 典型代表 VMware workstation、Xen 3.0 以前版本、Virtual PC 2004

  现在也有些方案将以上两种类型进行融合,Hypervisor 依然位于最底层,拥有所有的物理资源,而 I/O 设备虚拟化由 Hypervisor 和特权操作系统共同完成。采用这种模型的典型是 Xen。

VirtIO

  VirtIO(Virtual I/O)是 Linux Kernel 中的一种 IO 半虚拟化解决方案,它提供 Guest OS 与 Hypervisor 虚拟化设备之间的通信框架和编程接口。其主要的优势是能提高性能且减少跨平台带来的兼容性问题。

  1. VirtIO 是车载行业比较常用的半虚拟化技术

  2008 年,linux 内核社区大佬 Rusty Russell 提出了virtio 的模型和实现:virtio: Towards a De-Facto Standard For Virtual I/O Devices,以此来规范化 Linux Kernel 中越来越多的虚拟化技术。

标准

  现在,VirtIO 是 OASIS Open 下的 VirtIO 技术委员会(OASIS Virtual I/O Device (VIRTIO) Technical Committee)负责维护的开放协议和接口,以使得虚拟机能够以标准化方式访问 IO 设备。VirtIO 于 2016 年 3 月发布 1.0 版本,正式标准化,并于 2019 年 4 月发布 1.1 版本,2022 年 7 月发布 1.2 版,标准文档可以直接从官网免费下载。

  • [VIRTIO-v1.2] Virtual I/O Device (VIRTIO) Version 1.2. Edited by Michael S. Tsirkin and Cornelia Huck. 01 July 2022. OASIS Committee Specification 01. https://docs.oasis-open.org/virtio/virtio/v1.2/cs01/virtio-v1.2-cs01.html. Latest stage: https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html.
  • [VIRTIO-v1.1] Virtual I/O Device (VIRTIO) Version 1.1. Edited by Michael S. Tsirkin and Cornelia Huck. 11 April 2019. OASIS Committee Specification 01. https://docs.oasis-open.org/virtio/virtio/v1.1/cs01/virtio-v1.1-cs01.html. Latest version: https://docs.oasis-open.org/virtio/virtio/v1.1/virtio-v1.1.html.
  • [VIRTIO-v1.0] Virtual I/O Device (VIRTIO) Version 1.0. Edited by Rusty Russell, Michael S. Tsirkin, Cornelia Huck, and Pawel Moll. 03 March 2016. OASIS Committee Specification 04. http://docs.oasis-open.org/virtio/virtio/v1.0/cs04/virtio-v1.0-cs04.html. Version with change bar: http://docs.oasis-open.org/virtio/virtio/v1.0/cs04/virtio-v1.0-cs04-diff.html. Latest version: http://docs.oasis-open.org/virtio/virtio/v1.0/virtio-v1.0.html.

  在 VirtIO 1.0 正式版发布之前,VirtIO 0.95 版草案已经被广泛应用在了 Linux 中,因此,在 VirtIO 1.0 及之后的规范中有很多 Legacy Interface、Transitional Device 等术语用于指示 VirtIO 1.0 之前规范中的描述。对应到 Linux 中分别为 virtio_pci_legacyvirtio_pci_modern。本文主要是依据 VirtIO 1.1 版本,以下是一些支持 VirtIO 的软件 / 系统中对 VirtIO 1.1 的支持情况:

  • linux 4.18 virtio-net driver 部分支持 virtio 1.1,但 vhost-net 不支持 virtio 1.1
  • qemu master 实现了 virtio 1.1
  • dpdk virtio pmd 和 vhost-user 都支持 virtio 1.1

架构

  VirtIO 作为一种通用的虚拟 IO 设备驱动模型,由前端驱动(规范中使用 Driver 这个术语来表示前端部分)、后端设备(规范中使用 Device 这个术语来表示后端部分)、VirtQueue & VRing 、控制字段这四个部分组成。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第6张图片
  VirtIO 的前端驱动与后端设备在 VirtIO 规范中统一使用 Device 这个术语表示,而不显式区分前端驱动和后端设备这两个概念。规范中的 Device 这个术语并不等同于 Linux 内核驱动模型(设备(device),驱动(driver),总线(bus))中的设备。

  VirtQueue & VRing 和控制字段则分别被用于前端驱动与后端设备之间进行数据传递和能力交换协商(包括建立和终止数据通信)。VirtQueue & VRing 需要尽可能高效地快速移动数据包,而控制字段则需要尽可能灵活地支持未来架构中的不同设备和供应商。

  如下是 virtio-net 设备在 QEMU + KVM 虚拟环境下的一个框图,这里先来整体看一下 VirtIO 的基本通信框架,后面我们详细学习一下其中的每一部分的具体框架及功能。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第7张图片

前端驱动

  VirtIO 的前端驱动一般存在于客户机内核中的驱动程序,其核心职责是接收用户进程的 IO 请求,然后将 IO 请求传输到相应的后端设备,以及从对应的后端设备获取完成的请求后发往用户进程。

  VirtIO 前端驱动程序负责对客户机操作系统提供模拟的 VirtIO 设备,VirtIO 规范为每一类设备定义了一个唯一的 ID 号,这个 ID 号会被同时用于前端驱动与后端设备。规范的 5 Device Types 这个章节对每种设备进行了介绍,其中,最为常见的是 virtio-net、virtio-blk、rpmsg 等。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第8张图片
  VirtIO 规范中规定了基于 VirtIO 总线的设备是通过特定于总线的方法被发现和识别的。VirtIO 只是一个半虚拟化标准(协议),它本身实现的载体和总线并无绑定。VirtIO 协议实现过程中,CPU 与外设之间的通知机制以及外设访问内存方式由实际连接 CPU 与外设的总线协议决定。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第9张图片
  VirtIO 规范的 Virtio Transport Options 章节定义了 Virtio Over PCI Bus、Virtio Over MMIO、Virtio Over Channel I/O 这三种方式。Virtio Over Channel I/O 仅用于 S/390 系统,我没接触过。。。

Virtio Over PCI Bus

  VirtIO 设备通常作为 PCI 设备实现,并且可以实现为任何类型的 PCI 设备或 PCI Express 设备。根据 PCI 相关规范,VirtIO 设备的 PCI Vendor ID 为 0x1AF4 并且 PCI Device ID 为 0x1000 ~ 0x107F 的 PCI 设备将被认为是 VirtIO 设备。PCI Device ID 的这个取值范围就对应了不同的 VirtIO 设备。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第10张图片

采用 PCI 总线协议的 virtio 设备叫 virtio-pci 设备

  VirtIO 1.0 规范之前的规范中规定的 VirtIO 设备(VirtIO 1.0 及之后的规范中称为 Transitional Device)的 PCI Device ID 是固定的;VirtIO 1.0 及之后的规范中规定 VirtIO 设备(称为 Non-transitional Device)的 PCI Device ID 必须为 VirtIO Device ID + 0x1040,且 PCI Revision ID 必须为 1 或者更高,PCI Subsystem Device ID 必须为 0x40 或者更高。两种 VirtIO 设备的 PCI Device ID 如下所示。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第11张图片

更详细的介绍参见规范的 4.1 Virtio Over PCI Bus

Virtio Over MMIO

  没有 PCI 支持的虚拟环境可能使用简单的内存映射设备(virtio-mmio)代替 PCI 设备。内存映射 virtio 设备行为基于 PCI 设备规范。因此,包括设备初始化、队列配置和缓冲区传输在内的大多数操作几乎是相同的。

采用 mmio 总线协议的 virito 设备叫 virito-mmio 设备,它完全是针对虚拟机设计的

  与 PCI 不同,MMIO 没有提供通用的设备发现机制。对于每个设备,客户操作系统都需要知道寄存器和中断的位置。通常的做法是在 Linux 设备树中定义一块共享内存。规范中建议在设备树中进行系统绑定:
在这里插入图片描述
  virito-mmio 设备无法像 virtio-pci 设备一样支持动态发现,客户机需要知道 mmio 设备使用的具体内存地址和中断号。规范中为 virito-mmio 设备提供了一组内存映射控制寄存器,后面跟着一个特定于设备的配置空间,其相关定义如下图所示:

更详细的介绍参见规范的 4.2 Virtio Over MMIO

Virtio Over Channel I/O

  基于 S/390 的虚拟机既不支持 PCI 也不支持 MMIO,因此需要一种不同的传输方式。Virtio-ccw 使用基于标准通道 I/O 的机制,用于 S/390 上的大多数设备。具有特殊控制单元类型的虚拟通道设备充当 virtio 设备的代理(类似于 virtio-pci 使用 PCI 设备的方式) ,virtio 设备的配置和操作主要通过通道命令完成。

更详细的介绍参见规范的 4.3 Virtio Over Channel I/O

后端设备

  VirtIO 的后端设备通常就是存在于宿主机(Hypervisor)中的驱动,其核心职责是接收来自相应前端驱动的 IO 请求,处理后将结果返回给前端驱动(这个过程通常被称为某某功能的 offload)。

  Linux Kernel 中提供了完整的 VirtIO 框架以及前端驱动,后端则由不同的虚拟化方案来具体实现,例如,嵌入式常用的 OpenAMP 提供了裸机下的 VirtIO 功能,下面简单介绍几种虚拟化方案中 VirtIO 的应用。

QEMU + KVM

  QEMU(quick emulator)是由 Fabrice Bellard 提出的一个通过仿真来虚拟化的项目,完整地实现了处理器虚拟化、内存虚拟化以及 I/O 设备。但是由于采用了仿真方法,虚拟化的性能比较差。KVM 全称是基于内核的虚拟机(Kernel-based Virtual Machine),它是 Linux 的一个内核模块,该内核模块使得 Linux 变成了一个 Hypervisor(上文说的 Type I)。

  在此虚拟化方案中,KVM 只提供处理器虚拟化和内存虚拟化,不提供 I/O 虚拟化。当虚机进行 I/O 操作时,KVM 就会通过 VirtIO 将操作转交到 QEMU 来负责解析和模拟,从而提高性能。因此,在 QEMU 中就包含了一套 VirtIO 的后端设备的实现代码。

  QEMU(quick emulator)本身并不包含或依赖 KVM 模块,可以在没有 KVM 模块的情况下独立运行。KVM 只是内核模块,用户并没法直接跟内核模块交互,需要借助用户空间的管理工具,而这个工具通常是 QEMU,但并不是唯一选择。

OpenAMP

  OpenAMP 经常被用来在多核架构中的从核中跑裸机或者 RTOS 环境(主核则可以直接运行 Linux)下实现 VirtIO 的后端设备。OpenAMP 提供上层应用的基本框架,而与硬件交互的部分被独立拆分到了 Libmetal 中。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第12张图片

RPMsg

  OpenAMP 提供了 VirtIO 框架的基本实现以及提供了 RPMsg 这一种类型的 VirtIO 设备。其中,源码中即包含前端驱动实现也包含了后端设备的实现。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第13张图片

自定义后端

  我们可以借助于 OpenAMP 来实现自己的后端设备。例如,借助于 RemotePROC 在从核实现一个网卡的后端,其中主核 Linux 则走 virtio-net 以及 Remoteproc 这两套流程。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第14张图片

控制字段

  控制字段是指那些用于前端驱动与后端设备进行同步的方法。例如,在 RemoteProc 中有资源表这个概念,其中就定义了 struct fw_rsc_vdev 这个结构(被称为资源表头),其中的就包含了各种控制字段,具体见如下注释:

struct fw_rsc_vdev {
	u32 id;					/* VirtIO 设备类型 */
	u32 notifyid;			/* 这个就是不同类型的通知唯一的 ID,用于区分不同的通知类型 */
	u32 dfeatures;			/* 后端设备填写的自身支持的特性,取决于设备类型 */
	u32 gfeatures;			/* 前端驱动回写的特性,取决于设备类型 */
	u32 config_len;			/* 这个表示 Device Configuration space 的长度,如果不用则填 0 */
	u8 status;				/* 这个用来同步前端驱动与后端设备的状态 */
	u8 num_of_vrings;		/* vring 的个数 */
	u8 reserved[2];
	struct fw_rsc_vdev_vring vring[0];	/* 后续是多个缓冲区 */
	/* 紧跟在后面的 就是 Device Configuration space */
} __packed;

Device status field

  Device status field 用来同步前端驱动与后端设备的之前的状态。后端设备需要先将 Device status field 字段设置为 0,然后等待。前端驱动会根据初始化情况修改 Device status field 字段。规范中定义的前端驱动修改如下:
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第15张图片

详细介绍见规范 2.1 Device Status Field

Feature bits

  每个后端设备都需要通过 Feature bits 提供它所能理解的所有特性。在设备初始化期间,前端驱动会读取 Feature bits,并且会根据实际情况重新修改 Feature bits,进而完成同步设备的特性。重新协商的唯一方法就是重置设备,运行期间无法动态变更。

  • 0 ~ 23: 由具体的设备来定义的。规范的 5 Device Types 章节介绍的每个设备中都有 Feature bits 的相关定义
  • 24 ~ 37: 用于前端驱动与后端设备协商 Virtqueue 相关的特性
  • 38 以上: 保留为以后扩展。目前,从 VirtIO 1.0 ➔ VirtIO 1.1 ➔ VirtIO 1.2 规范已经逐步扩展了一些,在 6 Reserved Feature Bits 章节有详细的说明

注意,不同版本的规范,以上范围是由变化的

  需要注意,Feature bits 通常实现是两个独立按位使用的变量,后端设备负责填写一个,供前端驱动读取;前端驱动会修改另一个,供后端设备读取。Feature bits 的实际内容取决于具体的设备,规范的 5 Device Types 中定义的每类设备都包含独立的章节来介绍其对应的 Feature bits。

详细介绍见规范 2.2 Feature Bits 和 5 Device Types 和 6 Reserved Feature Bits

Notifications

  当前端驱动或者后端设备在有特定变动时,都需要通过通知来告诉另一端。通知的操作方式是特定于传输的,规范中定义了如下三种类型的通知(缓冲区通知与 VirtQueue 密切相关,详见后文):

  • 配置变更通知:由后端设备发送,前端驱动接收使用。这种类型的通知用于告诉前端驱动后端设备配置空间发生变化
  • 可用的缓冲区通知:由前端驱动发送,后端设备接收使用。这种类型的通知用于告诉后端设备缓冲区已经在通知指定的 virtqueue 上可用
  • 已用缓冲区通知:由后端设备发送,前端驱动接收使用。这种类型的通知用于告诉前端驱动缓冲区已经在通知指定的 virtqueue 上可用

  大多数传输使用中断实现由设备发送给驱动程序的通知。因此,在 VirtIO 规范的早期版本中,这些通知也被称为中断。在 1.1版本规范中定义的一些名称仍然保留了这个中断术语。有时也使用事件这个术语指通知或通知的接受。

详细介绍见规范 2.3 Notifications

Device Configuration space

  设备配置空间通常用于很少更改或初始化时的参数,前端驱动会读取其中的内容然后根据里面的值进行初始化。当配置字段是可选的时,它们的存在由功能位指示。规范的未来版本可能会通过在尾部添加额外的字段来扩展设备配置空间。

  1. 字节序采用小端模式
  2. 详细介绍见规范 2.4 Device Configuration Space

  这里还是以 RemoteProc 为例,struct fw_rsc_vdev 被称为资源表头,紧跟在这个头后面的就是 Device Configuration space 了。之所以称为资源表头,是因为其中的 vring 是变长的,没法在一个结构体中同时定义 Device Configuration space,实际使用时通常是在封装一层,如下所示的 virtio-net 这种设备的资源表定义。

struct shared_resource_table {
    unsigned int version;
    unsigned int num;
    unsigned int reserved[2];
    unsigned int offset[NUM_RESOURCE_ENTRIES];
    struct fw_rsc_vdev mac0_vdev;
    struct fw_rsc_vdev_vring mac0_vring_tx;
    struct fw_rsc_vdev_vring mac0_vring_rx;
    struct fw_rsc_vdev_vring mac0_vring_ctrl;
    struct virtio_net_config mac0_ctrl;
};

VirtQueue & VRing

  VirtQueue & VRing 是 VirtIO 提供的用于前端驱动与后端设备进行数据交互的标准方法。VirtQueues 是一种帮助后端设备和前端驱动执行各种 VRing 操作的数据结构,而 VRing 则是底层核心数据结构,被实现为环形的缓冲区。

virtqueue 存放于前端驱动与后端设备的共享内存中。

  从 1.1 版标准开始,VirtIO 规范提供了 Split Virtqueue 和 Packed Virtqueue 这两种类型的 Virtqueues,Packed Virtqueue仅仅是将 Split Virtqueue 中的各种 RING 进行了打包,其根本框架还是一样的,下面分别来学习一下。

Split Virtqueue

  在 VirtIO 协议 1.0 及之前版本中,Split Virtqueue 是唯一支持的 virtqueue 格式。Split Virtqueue 格式将 virtqueue 分成了描述符表(Descriptor Table)、可用描述符(Avaliable Ring)和已用描述符(Used Ring)三个部分,对于每个部分只能由驱动或者设备写入,而不支持同时写入。

通信流程

  virtqueue 是一个典型的生产者与消费者的模型,前端驱动属于生产者,后端设备属于消费者,且是一个环形的使用方法。一种类型的设备可能需要多个 virtqueue(例如,virtio-net 最少需要两个 virtqueue,分别用于 tx 和 rx )。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第16张图片

  • 发送数据
      前端驱动首先获取一个(也可以一次用多个)空闲的描述符并填充(buffer 地址、数据长度等),然后将描述符的基本情况(描述符索引)填写到 Available Ring 中的一个空闲位置中,最后通知后端设备;

      后端设备则从 Available Ring 中获取到需要发送的描述符索引,进而获取到描述符中的数据(buffer 地址、数据长度等)并发送。发送完成后,后端设备需要将已用完的描述符信息(描述符索引)填充到 Used Ring 中一个空闲位置,然后通知前端驱动

  • 接收数据
      前端驱动需要先将空的 buff 填充(buff 地址、buff 长度等)到空闲描述符中,然后将可以用的描述符基本情况(描述符索引)填写到 Available Ring 中的一个(通常是一次填写多个)空闲位置中,最后通知后端设备

      当后端设备接收到数据后,需要从 Available Ring 中获取一个可用描述符索引,进而获取到描述符中的缓冲区(buffer 地址、buffer 长度等)并将数据放到缓冲区中,然后将描述符信息(描述符索引,数据长度)填写到 Used Ring 中一个空闲位置,然后通知前端驱动

Descriptor Table

  Descriptor Table 中存放了前端驱动与后端设备进行数据交互的描述符,描述符中存放了数据的物理地址和长度以及描述符本身的标志位。实际代码实现中就是一块内存(数组),其中存放了多个 VRing 描述符。

  • 从 1.1 版规范开始,使用 Descriptor Area 这个术语
  • 占用的总字节数 = 16 * 队列大小;其中 16 = sizeof(vring_desc)
  • 最小 16 字节对齐
  • 所有的描述符通过 .next 串联起来,形成描述符链
  • VIRTIO_F_IN_ORDER 特性表示描述符必须从 0 ~ N 顺序使用(最后一个转圈到 0),没有该特性则可以随便使用哪个描述符

  每个描述符都包含一个后端设备只读或者只写的缓冲区,但是一个描述符链可以同时包含这两种缓冲区,而且前端驱动必须确保将任何设备可写的描述符元素放在任何设备可读的描述符元素之后。

  一个 IO 请求可能一次使用多个描述符(用一个描述符链来传递一个完整数据)。通过描述符中的 flags 中是否设置 VRING_DESC_F_INDIRECT 标志位,一个 IO 请求可以选择 Chained Descriptor 或者 Indirector Descriptor 方式进行组织。

  • Chained Descriptor:在申请描述符空间之后,必须将描述符通过 .next 串联起来。初始状态为连续(0 ➔1 ➔ 2 ➔ N),但是一旦用起来之后就不是连续了,因为,一个数据帧可能占用多个描述符,第一次使用释放后第二次用几个不确定。
    Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第17张图片
    需要注意的是,.next 串联的描述符链是为了使用时方便前端驱动和后端设备一次使用多个描述符,实际通信时采用的 Chained Descriptor 是通过描述符中 flag 中的 VRING_DESC_F_NEXT 标志位来判断的。

    通信时使用的描述符链的最后一个描述符的 flag 中没有 VRING_DESC_F_NEXT,但是其 .next 仍然会指向下一个描述符

  • Indirect Descriptor:IO 请求使用的描述符(描述符的 flag 中有 VIRTQ_DESC_F_INDIRECT 标志)指向的是一个包含多个 virtq_desc 描述符的间接描述符表,并由间接描述表中的描述符描述所有的内存区域。
    Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第18张图片

  此外,我们需要注意,传递数据时通常会包含一个额外的数据头。这个数据头可以单独存放在一个描述符中,也可能会与数据组合后放到一个描述符中,这个取决于具体的 VirtIO 设备的驱动实现。如下是默认情况下的 virtio-net 设备收发数据的描述符使用示例:
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第19张图片

Available Ring

  Available Ring 用于前端驱动提供给后端设备的可用描述符(前端驱动只写,后端设备只读)。实际代码实现中就是一个块内存(数组),其中存放了 Descriptor Table 中描述符的索引(注意,这里不是存放的描述符)

  • 从 1.1 版规范开始,使用 Driver Area 这个术语
  • 占用的总字节数 = 6 + 2 * 队列大小;其中 6 = sizeof(vring_avail),2 = sizeof(uint16)
  • 最小 2 字节对齐
  • 前端驱动首先填写 vring_desc[vq_desc_idx] 的内容并将 vq_desc_idx 加 1,然后修改 .avali->ring[.avali->idx]vq_desc_idx 加 1 之前的值,最后将 .avali->idx 加 1
    • 注意,如果一次填写了多个描述符(vq_desc_idx + n),.avali->ring[.avali->idx] 需要为第一个描述符的索引。前端驱动会在从第一个描述符到倒数第二个描述符的 .flag 中添加 VRING_DESC_F_NEXT,后端驱动通过此标志一次处理这多个描述符
  • 后端设备通过检查 .avali->idxvq_avaliable_idx 的关系判断 .avali->ring[] 是否有需要处理的描述符索引,如果有,则根据 .avali->ring[vq_avaliable_idx] 中记录的描述符索引找到 vring_desc[.avali->ring[vq_avaliable_idx]] 这个描述符进行处理,然后会立刻将 vq_avaliable_idx 加 1(这里不是处理完成后增加)
  • 在 Linux 及某些源码实现中,Available Ring 也被称为 IN 缓冲区

Used Ring

  Used Ring 用于后端设备提供给前端驱动的可用描述符(后端设备只写,前端驱动只读)。实际代码实现中就是一个块内存(数组),其中存放了 Descriptor Table 中描述符的索引(注意,这里不是存放的描述符)

  • 从 1.1 版规范开始,使用 Device Area 这个术语
  • 占用的总字节数 = 6 + 8 * 队列大小;其中 6 = sizeof(vring_used),8 = sizeof(vring_used_elem)
  • 最小 4 字节对齐
  • 后端设备处理完从 .avali 中获取的描述符后,需要修改 .used->ring[.used->idx] 为获取的 .avali->ring[vq_avaliable_idx 增加之前] 的值后将 .used->idx 加 1
    • 如果处理的是一个描述符链,则仅返回描述符链中的第一个描述符索引
  • 前端驱动通过检查 .used->idxvq_used_idx 的关系判断 .used->ring[] 是否有需要处理的描述符索引,如果有,则根据 .used->ring[vq_used_idx] 中记录的描述符索引找到 vring_desc[.used->ring[vq_used_idx]] 这个描述符进行处理,处理完成后将 vq_used_idx 加 1
  • 在 Linux 及某些源码实现中,Available Ring 也被称为 OUT 缓冲区

队列大小

  队列大小就是描述符表中描述符的个数,通常取值为 2n,最大 65535。不同 VirtIO 设备对于队列的个数需求也不一样,例如,virtio-net 最少使用两个队列,且后续扩展仅支持偶数个队列(不能搞 3 个这种基数个队列)。

Packed Virtqueue

  Packed Virtqueue 是从 1.1 版引入的一种使用主机和客户机可以同时读写的内存的紧凑 virtqueue 布局。前端驱动与后端设备通过协商 VIRTIO_F_RING_PACKED 特性从而使用 Packed virtqueue。Packed Virtqueue 将 Split Virtqueue 中的三个 RING 合并为了一个。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第20张图片
  由于前端驱动和后端设备可能运行在不同的 cpu,且 Virtqueue 本身存在于他们的共享内存中,因此,存在不同 cpu 之间互相通知进行 cache 刷新的问题。Split Virtqueue 分成三个部分,三部分分布在不同的 cacheline 上就需要多次 cache 刷新,Packed Virtqueue 中的三个部分合并成一个,这样大大减少了cache 刷新的次数。

通信流程

  Packed Virtqueue 把三个 ring 进行了整合,但 virtio 的本质思想并没有变化,整个数据传输还是 avail 和 used 共同作用完成的。三 ring 合一仅仅是形式的变化,avail ring 和 used ring 并没有消失。规范中定义了 Driver Ring Wrap Counter 和 Device Ring Wrap Counter 这两个字段来辅助实现(通常实现为两个布尔类型,每次转圈进行反转)原来的通信方式。

  • .flagsVRING_DESC_F_AVAIL 的设置和 Driver Ring Wrap Counter 同步,且 VRING_DESC_F_USED 的设置和 Driver Ring Wrap Counter 相反时,表示 desc 为 avail desc。例如,Driver Ring Wrap Counter 为 1 时,.flags 应该设置VRING_DESC_F_AVAIL|~VRING_DESC_F_USED;当 Driver Ring Wrap Counter 为 0 时,.flags 应该设置~VRING_DESC_F_AVAIL|VRING_DESC_F_USED
  • .flagsVRING_DESC_F_USED 的设置和 Device Ring Wrap Counter 同步,且 VRING_DESC_F_AVAIL 的设置也和 Device Ring Wrap Counter 同步时,表示 desc 为 used desc。例如,Device Ring Wrap Counter 为 1 时,.flags 应该设置VRING_DESC_F_AVAIL|VRING_DESC_F_USED;当 Device Ring Wrap Counter 为 0 时,.flags 应该设置 ~VRING_DESC_F_AVAIL|~VRING_DESC_F_USED
  • 引入 Wrap Counter 的作用主要是为了解决描述符的回绕问题

Descriptor Ring

  前端驱动与后端设备交互使用的描述符数组,描述符最多支持 215 个。其中的 .addr.len 名字和含义与 Split Virtqueue 中的相同,flag 中则添加了 avaliused 标志位。其中的 .id 字段比较特殊,它是 buffer id,不是 desc 的下标 idx。与 Split Virtqueue 一样,一次 I/O 请求可以一次使用多个描述符。
Linux Kernel 之十 虚拟化、VirtIO 架构及规范、VirtQueue & VRing_第21张图片

  • 占用的总字节数 = 16 * 队列大小;其中 16 = sizeof(vring_desc)
  • 最小 16 字节对齐
  • VIRTQ_DESC_F_WRITE 标志位
    • 对于 avail desc 这个 flag 用来标记其关联的 buffer 是只读的还是只写的;
    • 对于 used desc 这个 flag 用来表示去关联的 buffer 是否有被后端(device)写入数据;
  • desc 中的 len
    • 对于 avail desc,len 表示 desc 关联的 buffer 中被写入的数据长度;
    • 对于 uesd desc,当 VIRTQ_DESC_F_WRITE 被设置时,len 表示后端(device)写入数据的长度,当 VIRTQ_DESC_F_WRITE 没有被设置时,len 没有意义;
  • Descriptor Chain
    • buffer id 包含在 desc chain 的最后一个 desc 中,另外,VIRTQ_DESC_F_NEXT 在 used desc 中是没有意义的。

  Packed Virtqueue 的描述符中去掉了原来的 .next 字段,主要就是因为在 Packed Virtqueue 中描述符链的描述符必须是连续的(依旧通过 .flag 中的 next 标志位来判断一次数据的多个描述符)。

Driver Event Suppression

  前端(driver)驱动只写,后端设备(device)只读,用于前端驱动主动控制后端向前端发送的的通知数量。

  • 占用的总字节数 = 4
  • 最小 4 字节对齐

Device Event Suppression

  前端驱动(driver)只读,后端设备(device)只写,用于后端设备主动控制前端驱动向后端设备发送的通知数量。

  • 占用的总字节数 = 4
  • 最小 4 字节对齐

队列大小

  队列大小就是描述符表中描述符的个数,通常取值为 2n,最大 215。不同 VirtIO 设备对于队列的个数需求也不一样,例如,virtio-net 最少使用两个队列,且后续扩展仅支持偶数个队列(不能搞 3 个这种基数个队列)。

参考

  1. https://zhuanlan.zhihu.com/p/102809005
  2. https://zh.wikipedia.org/wiki/%E8%99%9B%E6%93%AC%E5%8C%96
  3. https://www.redhat.com/en/blog/virtio-devices-and-drivers-overview-headjack-and-phone
  4. https://developer.ibm.com/articles/l-virtio/?mhsrc=ibmsearch_a&mhq=virtio
  5. https://www.cnblogs.com/sammyliu/p/4543110.html
  6. https://blog.csdn.net/dillanzhou/article/details/120339795?ydreferer=aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS8%3D
  7. https://blogs.oracle.com/linux/post/introduction-to-virtio
  8. https://blog.csdn.net/anyegongjuezjd/article/details/128500870
  9. https://baijiahao.baidu.com/s?id=1751021676584440574&wfr=spider&for=pc
  10. https://zhuanlan.zhihu.com/p/100526650
  11. http://blog.chinaunix.net/uid-28541347-id-5819237.html

你可能感兴趣的:(Linux,Kernel,linux,virtio,虚拟化,kernel,virtqueue,vring)