virtio —— 一种 Linux I/O 半虚拟化框架 [译]

本文主要从以下几个方面 virtio:

1、前言

2、全虚拟化 vs 半虚拟化

3、Linux 客户机的一种抽象

4、virtio 架构

5、概念层级

6、virtio buffer

7、核心 API

8、virtio 驱动示例

9、关于未来

1、前言

图片

简言之,virtio 是对于半虚拟化管理程序(para-virtualized hypervisor)中设备的一个抽象层。virtio 是 Rusty Russell 为了支持他自己的虚拟化方案 lguest 而开发的。

这篇文章先对半虚拟化和设备模拟技术进行介绍,然后探寻 virtio 技术实现中的一些细节。

本文基于 kernel 2.6.30 版本的 virtio 框架进行讲解。

Linux 是 hypervisor 的“试验场”。Linux 提供了许多具有不同特点和优势的虚拟化解决方案。例如 KVM(Kernel-based Virtual Machine),lguest、和用户态的 Linux。因为这些虚拟化解决方案各自都有不同的需求,因此在 Linux 实现这些虚拟化解决方案给操作系统造成了不小的重担,其中一个负担就是设备虚拟。

virtio 没有为不同类型的设备(如:网络设备、块设备等)提供不同的设备模拟机制,而是通过标准化的接口为这些设备模拟提供了一个通用的前端,从而增加了代码在跨平台时的重用性。

2、全虚拟化 vs 半虚拟化

我们先来快速讨论两种完全不同的虚拟化方案:全虚拟化和半虚拟化。

在全虚拟化中,客户机操作系统运行在 hypervisor 之上,而 hypervisor 运行在裸机之上。客户机不知道它是在虚拟机还是物理机中运行,在全虚拟化中客户机不需要修改操作系统就可以直接运行。

与此相反的是,在半虚拟化中,客户机操作系统不仅需要感知其运行于 hypervisor 之上,还必须包含与 hypervisor 进行交互能够带来更高效率的代码。(如图 1 所示)。

在全虚拟化中,hypervisor 必须进行设备硬件的模拟,也就是需要模拟最底层的通信机制(如:网卡驱动)。尽管这种抽象模拟机制很干净,但这样做同时也是最低效率和最复杂的。

在半虚拟化中,客户机与 hypervisor 可以协作工作以提高模拟效率。半虚拟化的缺点就是客户机操作系统必须感知到它运行于虚拟机之中,而且需要对客户机操作系统进行修改才能工作。

图 1 :全虚拟化与半虚拟化环境中的设备模拟

image-20230714103511179

硬件辅助虚拟化也在不断发展。新处理器增加高级指令,使客户机操作系统和 hypervisor 之间的的转换更有效率。硬件 I/O 虚拟化同样也在不断发展(例如 SR-IOV、MR-IOV)。

(如图1 左侧所示)在传统的全虚拟化环境中,hypervisor 必须捕获 I/O 请求,然后模拟出真实硬件的行为。尽管这样提供了很大的灵活性(可以运行不用进行修改的操作系统),但这样却会造成效率低下。

(如图1 右侧所示)在半虚拟化环境中。客户机操作系统知道其运行在虚拟机之中,并且加入了作为前端的驱动程序。hypervisor 为特定设备模拟实现后端驱动程序。这里的前后端驱动就是 virtio 的架构的组成部分,为模拟设备的访问的开发提供标准化接口,以提高代码重用率及增加转换效率。

Virtio 替代品:

virtio 并不是半虚拟化领域的唯一形式,Xen 也提供了类似的半虚拟化设备驱动,VMware 也提供了名为 Guest Tools 的半虚拟化架构。

3、Linux 客户机的一种抽象

图片

如前节所述,virtio 是半虚拟化 hypervisor 中一组通用模拟设备的抽象。

这种设计允许 hypervisor 输出一组通用的模拟设备,并通过通用的应用程序编程接口 (API) 使它们可用。

如图 2 所示,使用半虚拟化 hypervisor,客户机实现了一组通用的接口,并在一组后端驱动后面进行特定设备模拟。后端驱动并不需要做到通用,只要它能实现前端所需的各种行为即可。

图 2 :使用 virtio 进行驱动程序抽象

image-20230714103549259

在实际实现中,使用用户空间的 QEMU 程序来进行设备模拟,所以后端驱动通过与用户空间的 hypervisor 进行通信,以便通过 QEMU 进行 I/O 操作。QEMU 是系统模拟器,除了提供客户机操作系统虚拟化平台外,还提供整个系统的设备模拟(PCI host 控制器、磁盘、网络、视频硬件、USB 控制器和其他硬件设备)。

virtio API 依赖于一个简单的缓冲区抽象来封装客户机的命令和数据请求。让我们看看 virtio API 及其组件的内部结构。

4、virtio 架构

图片

除了前端驱动(在客户机操作系统中实现)和后端驱动(在虚拟机 hypervisor 中实现)之外,virtio 还定义了 2 个层次来支持客户机到 hypervisor 的通信。

在顶层(称为 virtio 层)是虚拟队列接口,它在概念上将前端驱动程序附加到后端驱动,驱动可以根据需要使用零个或多个队列。例如,virtio 网络驱动使用两个虚拟队列(一个用于接收,一个用于发送),而 virtio 块驱动只使用一个队列。虚拟队列是虚拟的,它通过 ring 实现,以用于遍历客户机到 hypervisor 的信息。这可以以任何方式来实现,只要 Guest 和 hypervisor 通过相同的方式来实现。

图 3 :virtio 架构

image-20230714103621997

如图 3 所示,列出了五个前端驱动程序,分别用于块设备(例如磁盘)、网络设备、PCI 模拟、balloon 驱动(用于动态管理客户机内存使用)和控制台驱动。每个前端驱动在 hypervisor 中都有一个对应的后端驱动。

5、概念层级

图片

从客户机的角度来看,对象层次结构的定义如图 4 所示。

最顶层是 virtio_driver,它代表客户机中的前端驱动。与此驱动程序所匹配的设备通过 virtio_device 结构体进行封装(Guest 设备的表示形式)。virtio_config_ops 结构体定义了对 virtio 设备进行配置的操作。virtio_device 被 virtqueue 所引用(包括被 virtqueue 所使用的 virtio_device 的引用)。最后,每个 virtqueue 对象都引用 virtqueue_ops 对象,该对象定义了用于处理 hypervisor 驱动程序的底层队列操作。

虽然队列操作是 virtio API 的核心,但我们只会对其进行简要讨论,然后更详细地探讨 virtqueue_ops 操作。

图 4 :virtio 前端对象层次结构

image-20230714103653708

该过程从创建 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)。您可以使用带有 find_vq 函数的 virtio_config_ops 对象来识别与此 virtio_device 关联的 virtqueue。此对象返回与此 virtio_device 实例关联的虚拟队列。find_vq 函数还允许为 virtqueue 指定回调函数(参见图 4 中的 virtqueue 结构),用于通过 hypervisor 的响应缓冲区来通知客户机。

virtqueue 是一个简单的结构,它包含一个可选的回调函数(当 hypervisor 消耗缓冲区时调用它),对 virtio_device 的引用,对 virtqueue 操作的引用以及对特殊 priv 的引用(指的是使用的底层实现)。尽管回调是可选的,但可以动态启用或禁用回调函数。

但是这个层次结构的核心是 virtqueue_ops,它定义了命令和数据如何在 Guest 和 hypervisor 之间移动的。让我们首先从探讨在 virtqueue 中添加或删除的对象开始。

6、virtio buffer

图片

Guest(前端)驱动程序通过缓冲区与 hypervisor(后端)驱动程序进行通信。对于 I/O 操作来说,Guest 提供一个或多个表示请求的缓冲区。

例如,您可以提供三个缓冲区,第一个缓冲区表示读取请求,随后的两个缓冲区表示响应数据。在内部,此配置表示为一个分散-收集列表(列表中的每个条目被表示为一个地址和一个长度)。

7、核心 API

图片

通过 virtio_device 连接 Guest 驱动程序和 hypervisor 驱动程序,最常见的是通过 virtqueues。virtqueue 支持它自己的由五个函数(见图4 )组成的 API。

使用第一个函数 add_buf 向 hypervisor 发起请求。该请求采用前面讨论的分散-聚集列表的形式表示。为了实现 add_buf 操作,guest 将进行将 request 在 virtqueue 中进行入队、分散收集列表(地址和长度的数组)、配置输出条目的缓冲区数量(用于底层的 hypervisor)以及配置输入条目的数量(hypervisor 将为其存储数据并返回给 Guest)的一系列操作。

当通过 add_buf 在将向 hypervisor 的请求消息入队时,guest 可以使用 kick 函数将新请求通知给 hypervisor。为了获得最佳性能,Guest 应在通过 kick 通知之前将尽可能多的缓冲区放置到 virtqueue 上。

来自 hypervisor 响应消息可以通过 get_buf 获得。Guest 可以简单地通过调用此函数进行轮询或通过 virtqueue 提供的 callback 函数异步的方式等待响应消息通知。当 Guest 得知缓冲区可用时,调用 get_buf 返回已完成的缓冲区。

virtqueue API 中的最后两个函数是 enable_cb 和 disable_cb。您可以使用这些函数来启用和禁用 callback 处理(在 virtqueue 中通过 find_vq 函数初始化的 callback 函数)。请注意,callback 函数和 hypervisor 位于不同的地址空间中,因此是通过间接对 hypervisor 的调用进行的(例如 kvm_hypercall)。

缓冲区的格式、顺序和内容仅对前端和后端驱动程序有意义。内部传输(当前是通过 ring 实现的)仅移动缓冲区并且不知道缓冲区的内部表示形式。

8、virtio 驱动示例

图片

您可以在 Linux 内核的 ./drivers 子目录下找到各种前端驱动代码。virtio 网络驱动在可以在 ./driver/net/virtio_net.c 中找到,virtio 块驱动可以在 ./driver/block/virtio_blk.c 中找到。

./driver/virtio 子目录下提供了 virtio 接口实现(virtio 设备、驱动程序、virtqueue 和 ring)。virtio 也被用在了高性能计算(HPC)研究之中,使用共享内存在虚拟机之间进行通信。具体来说,这是通过使用 virtio PCI 驱动程序的虚拟 PCI 接口实现的。

您现在可以在 Linux 内核中使用这种半虚拟化基础设施。你所需要的就是一个用作 hypervisor 的内核,guest 内核和用来进行设备模拟的 QEMU。你可以使用 KVM(一个存在于宿主机内核中的模块)或者 Rusty Russell 的 lguest(一个修改过的 Linux 内核)。两种方案都支持 virtio(以及用于进行系统模拟的 QEMU 和 用于虚拟化管理的 libvirt)。

Rusty 的工作成果是一个更简单的半虚拟化驱动程序代码库和更快的虚拟设备模拟,但更重要的是,人们发现 virtio 可以提供比当前商业解决方案更好的性能(网络 I/O 的 2-3 倍)。这种性能提升是有代价的,但如果使用 Linux 作为您的 hypervisor 和 guest,那将是非常值得的。

9、关于未来

图片

尽管您可能永远不会为 virtio 开发前端或后端驱动,但 virtio 整个架构,值得更详细地了解。

virtio 提高了半虚拟化 I/O 环境中的效率,virtio 是基于之前在 Xen 中的工作进行构建的。Linux 继续证明自己是新虚拟化技术的研究平台。virtio 是将 Linux 作为 hypervisor 比使用其他类型的 hypervisor 具有竞争力的另一个例子。

posted @ 2023-07-14 10:39  寻梦99  阅读(178)  评论(0编辑  收藏  举报