Virtio: An I/O virtualization framework for Linux
原文链接:https://developer.ibm.com/articles/l-virtio/
简而言之,virtio 是半虚拟化hypervisor中设备上的抽象层。 virtio 由 Rusty Russell 开发,以支持他自己的称为 lguest 的虚拟化解决方案。本文首先介绍半虚拟化和仿真设备,然后探讨 virtio 的细节。重点是 2.6.30 内核版本中的 virtio 框架。
Linux 是虚拟机hypervisor的游乐场。正如我关于 Linux 作为虚拟机hypervisor的文章所示,Linux 提供了多种具有不同属性和优势的虚拟机hypervisor解决方案。示例包括基于内核的虚拟机 (KVM)、lguest 和用户模式 Linux。在 Linux 上拥有这些不同的hypervisor解决方案可能会根据它们的独立需求对操作系统征税。其中一项税收是设备的虚拟化。 virtio 没有各种设备模拟机制(用于网络、块和其他驱动程序),而是为这些设备模拟提供了一个通用前端,以标准化接口并增加跨平台代码的重用。
Full virtualization vs. paravirtualization
让我们首先快速讨论两种不同类型的虚拟化方案:完全虚拟化和半虚拟化。在完全虚拟化中,Guest操作系统运行在位于裸机上的hypervisor之上。Guest不知道它正在被虚拟化,并且不需要更改即可在此配置中工作。相反,在半虚拟化中,Guest操作系统不仅知道它在虚拟机hypervisor上运行,而且还包含使Guest到虚拟机hypervisor转换更有效的代码(参见图 1)。
在完全虚拟化方案中,hypervisor必须模拟设备硬件,它在对话的最低级别(例如,网络驱动程序)进行模拟。尽管在这种抽象上仿真是干净的,但它也是最低效和最复杂的。在半虚拟化方案中,Guest和hypervisor可以协同工作以使这种仿真高效。半虚拟化方法的缺点是操作系统知道它正在被虚拟化并且需要修改才能工作。
Figure 1. Device emulation in full virtualization and paravirtualization environments

但在传统的全虚拟化环境中,hypervisor必须捕获这些请求,然后模拟真实硬件的行为。 尽管这样做提供了最大的灵活性(即运行未经修改的操作系统),但它确实会导致效率低下(参见图 1 的左侧)。 图 1 的右侧显示了半虚拟化案例。 在这里,Guest操作系统知道它在hypervisor上运行,并包括充当前端的驱动程序。 hypervisor实现特定设备仿真的后端驱动程序。 这些前端和后端驱动程序是 virtio 的用武之地,为开发模拟设备访问提供标准化接口,以传播代码重用并提高效率。
virtio 在这个领域并不孤单。 Xen 提供半虚拟化设备驱动程序,而 VMware 提供了Guest Tools。
An abstraction for Linux guests
从上一节中,您可以看到 virtio 是对半虚拟化hypervisor中一组常见模拟设备的抽象。 这种设计允许hypervisor导出一组通用的仿真设备,并通过通用应用程序编程接口 (API) 使它们可用。 图 2 说明了为什么这很重要。 使用半虚拟化hypervisor,Guest实现了一组通用接口,并在一组后端驱动程序后面使用特定的设备模拟。 后端驱动程序不必通用,只要它们实现前端所需的行为即可。
Figure 2. Driver abstractions with virtio

virtio API 依赖于一个简单的缓冲区抽象来封装客户的命令和数据请求。 让我们看看 virtio API 及其组件的内部结构。
Virtio architecture
除了前端驱动程序(在客户操作系统中实现)和后端驱动程序(在虚拟机hypervisor,即Qemu中实现)之外,virtio 还定义了两个层来支持guest-to-hypervisor的通信。 在顶层(称为 virtio)是虚拟队列(virtual queue)接口,它在概念上将前端驱动程序附加到后端驱动程序。 驱动器可以根据需要使用零个或多个队列。 例如,virtio 网络驱动程序使用两个虚拟队列(一个用于接收,一个用于发送),而 virtio 块驱动程序只使用一个。 虚拟队列是虚拟的,实际上是作为环来实现的,以遍历客户到hypervisor的转换。 但这可以以任何方式实现,只要Guest和hypervisor都以相同的方式实现它。
Figure 3. High-level architecture of the virtio framework

Concept hierarchy
从客户机的角度来看,对象层次结构如图 4 所示。顶部是 virtio_driver,它代表客户机中的前端驱动程序。 与此驱动程序匹配的设备由 virtio_device(Guest设备的表示)封装。 virtio_config_ops 定义了 virtio 设备的操作配置。 virtio_device 由 virtqueue 引用(包括对其服务的 virtio_device 的引用)。 最后,每个 virtqueue 对象引用 virtqueue_ops 对象,该对象定义了用于处理管理程序驱动程序的底层队列操作。
Figure 4. Object hierarchy of the virtio front end

该过程从创建 virtio_driver 开始,随后通过 register_virtio_driver 进行注册。 virtio_driver 结构定义了上层设备驱动程序、驱动程序支持的设备 ID 列表、功能表(取决于设备类型)和回调函数列表。 当hypervisor序识别出与设备列表中的设备 ID 匹配的新设备时,会调用probe函数(在 virtio_driver 对象中提供)以传递 virtio_device 对象。 此对象与设备的管理数据一起缓存(以与驱动程序相关的方式)。 根据驱动程序类型,可以调用 virtio_config_ops 函数来获取或设置特定于设备的选项(例如,获取 virtio_blk 设备的磁盘的读/写状态或设置块设备的块大小)。
请注意,virtio_device 不包含对 virtqueue 的引用(但 virtqueue 确实引用了 virtio_device)。 要识别与此 virtio_device 关联的 virtqueue,您可以使用带有 find_vq 函数的 virtio_config_ops 对象。 此对象返回与此 virtio_device 实例关联的虚拟队列。 find_vq 函数还允许为 virtqueue 指定回调函数(参见图 4 中的 virtqueue 结构),用于通知客户来自hypervisor的响应缓冲区。
virtqueue 是一个简单的结构,它标识一个可选的回调函数(当hypervisor消耗缓冲区时调用它)、对 virtio_device 的引用、对 virtqueue 操作的引用以及引用要使用的底层实现的特殊 priv 引用 . 尽管回调是可选的,但可以动态启用或禁用回调。
但是这个层次结构的核心是 virtqueue_ops,它定义了命令和数据如何在Guest和hypervisor之间移动。 让我们首先探索从 virtqueue 中添加或删除的对象。
Virtio buffers
Guest(前端)驱动程序通过缓冲区与hypervisor(后端)驱动程序进行通信。 对于 I/O,Guest提供一个或多个表示请求的缓冲区。 例如,您可以提供三个缓冲区,第一个表示读取请求,随后的两个缓冲区表示响应数据。 在内部,此配置表示为一个分散-收集列表(列表中的每个条目表示一个地址和一个长度)。
Core API
通过 virtio_device 连接Guest驱动程序和hypervisor驱动程序,最常见的是通过 virtqueues。 virtqueue 支持它自己的由五个函数(见图4 )组成的 API。使用第一个函数 add_buf 向hypervisor发起请求。该请求采用前面讨论的分散-聚集列表的形式。为了 add_buf,guest 提供的参数包括:将请求加入队列的 virtqueue、分散收集列表(地址和长度的数组)、用作 out 条目的缓冲区数量(发往底层hypervisor)以及in 条目的数量(hypervisor将为其存储数据并返回给Guest)。当通过 add_buf 向 hypervisor 发出请求时,guest 可以使用 kick 函数通知 hypervisor 新的请求。为了获得最佳性能,Guest应在通过 kick 通知之前将尽可能多的缓冲区加载到 virtqueue 上。
来自hypervisor响应通过 get_buf 函数发生。Guest可以简单地通过调用此函数进行轮询或通过提供的 virtqueue 回调函数等待通知。当Guest得知缓冲区可用时,对 get_buf 的调用将返回已完成的缓冲区。
virtqueue API 中的最后两个函数是 enable_cb 和 disable_cb。您可以使用这些函数来启用和禁用回调进程(通过 find_vq 函数在 virtqueue 中初始化的回调函数)。请注意,回调函数和hypervisor位于不同的地址空间中,因此调用是通过间接hypervisor调用(例如 kvm_hypercall)发生的。
缓冲区的格式、顺序和内容仅对前端和后端驱动程序有意义。内部传输(当前实现中的环)仅移动缓冲区并且不知道它们的内部表示。