设备层虚拟化抽象之Virtio记录(202x)
2022-10-19 20:21:13 阿炯

本文用于收录Virtio的相关方面,截止到2030年之前。

Virtio 是对半虚拟化 Hypervisor 中的一组通用模拟设备的抽象。由 Rusty Russell 开发,当时的目的是支持其虚拟化解决方案 lguest。该设置还允许 hypervisor 导出一组通用的模拟设备,并通过一个通用的应用编程接口(API)让它们变得可用。有了半虚拟化 hypervisor 之后,来宾操作系统能够实现一组通用的接口,在一组后端驱动程序之后采用特定的设备模拟。后端驱动程序不需要是通用的,因为它们只实现前端所需的行为。virtio 是一种 I/O 半虚拟化解决方案,是一套通用 I/O 设备虚拟化的程序,是对半虚拟化 Hypervisor 中的一组通用 I/O 设备的抽象。提供了一套上层应用与各 Hypervisor 虚拟化设备(KVM、Xen、VMware等)之间的通信框架和编程接口,减少跨平台所带来的兼容性问题,大大提高驱动程序开发效率。

除了前端驱动程序(在来宾操作系统中实现)和后端驱动程序(在 hypervisor 中实现)之外,virtio 还定义了两个层来支持来宾操作系统到 hypervisor 的通信。在顶级(称为 virtio)的是虚拟队列接口,它在概念上将前端驱动程序附加到后端驱动程序。驱动程序可以使用 0 个或多个队列,具体数量取决于需求。例如,virtio 网络驱动程序使用两个虚拟队列(一个用于接收,另一个用于发送),而 virtio 块驱动程序仅使用一个虚拟队列。虚拟队列实际上被实现为跨越来宾操作系统和 hypervisor 的衔接点。但这可以通过任意方式实现,前提是来宾操作系统和 hypervisor 以相同的方式实现它。

Linux提供了许多具有不同特性和优势的虚拟化解决方案。例如KVM(Kernel-based Virtual Machine),lguest、和用户态的Linux。在Linux使用这些虚拟化解决方案给操作系统造成了重担,因为它们各自都有独立的需求。其中一个问题就是设备的虚拟化。virtio为各种各样设备(如:网络设备、块设备等等)的虚拟提供了通用的标准化前端接口,增加了各个虚拟化平台的代码复用。而不是各自为政般的使用繁杂的设备虚拟机制。


全虚拟化 vs 半虚拟化

先来讨论两种完全不同的虚拟化方案:全虚拟化和半虚拟化。在全虚拟化中,客户操作系统在hypervisor上运行,相当于运行于裸机一般。客户机不知道它在虚拟机还是物理机中运行,不需要修改操作系统就可以直接运行。与此相反的是,在半虚拟化中,客户机操作系统不仅能够知道其运行于虚拟机之上,也必须包含与hypervisor进行交互的代码。但是能够在客户机和hypervisor的切换中,带来更高的效率(图1)。

译注:客户机与hypervisor的切换举例:客户机请求I/O,需要hypervisor中所虚拟的设备来响应请求,此时就会发生切换。

在全虚拟化中,hypervisor必须仿真设备硬件,也就是说模仿硬件最底层的会话(如:网卡驱动)。尽管这种仿真看起来方便,但代价是极低的效率和高度的复杂性。在半虚拟化中,客户机与hypervisor可以共同合作让这种仿真具有更高的效率。半虚拟化不足就是客户机操作系统会意识到它运行于虚拟机之中,而且需要对客户机操作系统做出一定的修改。

由于不同 guest 前端设备其工作逻辑大同小异(如块设备、网络设备、PCI设备、balloon驱动等),单独为每个设备定义一套接口实属没有必要,而且还要考虑扩平台的兼容性问题,另外,不同后端 Hypervisor 的实现方式也大同小异(如KVM、Xen等),这个时候,就需要一套通用框架和标准接口(协议)来完成两者之间的交互过程,virtio 就是这样一套标准,它极大地解决了这些不通用的问题。

图1. 全虚拟化和半虚拟化中的设备仿真



硬件也随着虚拟化不断地发展着。新处理器加入了高级指令,使客户机操作系统和hypervisor的切换更加高效。硬件也随着I/O虚拟化不断地发生改变(参照Resource了解PCI passthrough和单/多根I/O虚拟化)。

在传统的全虚拟化环境中,hypervisor必须陷入(trap)请求,然后模仿真实硬件的行为。尽管这样提供了很大的灵活性(指可以运行不必修改的操作系统),但却造成了低效率(图1左侧)。图1右侧展示了半虚拟化。客户机操作系统知道运行于虚拟机之中,加入了驱动作为前端。hypervisor为特定设备仿真实现了后端驱动。这里的前端后端就是virtio的构件,提供了标准化接口,提高了设备仿真开发的代码复用程度和仿真设备运行的效率。

译注:virtio并不是半虚拟化领域的唯一存在。Xen提供了半虚拟化的设备驱动,VMware也提供了名为Guest Tools的半虚拟化支持。


Linux客户机中的一种抽象

如前节所述,virtio为半虚拟化提供了一系列通用设备仿真的接口。这种设计允许hypervisor导出一套通用的设备仿真操作,只要使用这一套接口就能够工作。图2解释了为什么这很重要。通过半虚拟化,客户机实现了通用的接口,同时虚拟化管理程序提供设备仿真的后端驱动。后端驱动并不一定要一致,只要它实现了前端所需的各种操作就可以。

图2. 使用virtio的驱动抽象



注意:在实际中,使用用户空间的QEMU程序来进行设备仿真,所以后端驱动通过QEMU的I/O来与用户空间的hypervisor通信。QEMU是系统模拟器,包括提供客户机操作系统虚拟化平台,提供整个系统的仿真(PCI host controller, disk, network, video hardware, USB controller等)。

virtio依靠于简单的缓存管理,用来存储客户机的命令与客户机所需的数据。继续来看virtio的API及其构件。

 
Virtio 架构

除了前端驱动(在客户机操作系统中实现)和后端驱动(在hypervisor中实现)之外,virtio还定义了两层来支持客户机与hypervisor进行通讯。虚拟队列(Virtual Queue)接口将前端驱动和后端驱动结合在一起。驱动可以有0个或多个队列,依赖于它们的需要。例如,virtio网络驱动使用了两个虚拟队列(一个用于接收一个用于发送),而virtio块设备驱动只需要一个。虚拟队列,通常使用环形缓冲,在客户机与虚拟机管理器之间传输。可以使用任意方式实现,只要客户机与虚拟机管理器相统一。

图3. virtio框架的架构



如图3,包含了五种前端驱动:块设备(如硬盘)、网络设备、PCI仿真、balloon驱动(用于动态的管理客户机内存使用)和一个终端驱动。每一个前端驱动,在hypervisor中都有一个相匹配的后端驱动。

从总体上看,virtio 可以分为四层,包括前端 guest 中各种驱动程序模块,后端 Hypervisor (实现在Qemu上)上的处理程序模块,中间用于前后端通信的 virtio 层和 virtio-ring 层,virtio 这一层实现的是虚拟队列接口,算是前后端通信的桥梁,而 virtio-ring 则是该桥梁的具体实现,它实现了两个环形缓冲区,分别用于保存前端驱动程序和后端处理程序执行的信息。


严格来说,virtio 和 virtio-ring 可以看做是一层,virtio-ring 实现了 virtio 的具体通信机制和数据流程。或者这么理解可能更好,virtio 层属于控制层,负责前后端之间的通知机制(kick,notify)和控制流程,而 virtio-vring 则负责具体数据流转发。


概念层级

在客户机的视角来看,对象层级如图4所示。顶端是virtio_driver,表示客户机中的前端驱动。与驱动相匹配的设备被封装在virtio_device(在客户机中表示设备),其中有成员config指向virtio_config_ops结构(其中定义了配置virtio设备的操作)。virtqueue中有成员vdev指向virtio_device(也就是指向它所服务的某一设备virtio_device)。最下面,每个virtio_queue中有个类型为virtqueue_ops的对象,其中定义了与hypervisor交互的虚拟队列操作。

图4. virtio前端的对象层级



这一过程起始于virtio_driver的创建和后续的使用register_virtio_driver将驱动进行注册。virtio_driver结构定义了设备驱动的上层结构,包含了它所支持的设备的设备ID,特性表(根据设备的类型有所不同),和一系列回调函数。当hypervisor发现新设备,并且匹配到了设备ID,就会以virtio_device为参数调用probe函数(于virtio_driver中提供)。这一结构与管理数据一起被缓存(以独立于驱动的方式)。根据设备的类型,virtio_config_ops中的可能会被调用,以获取或设置设备相关的选项(例如,获取硬盘块设备的读/写状态或者设置块设备的块大小)。

注意,virtio_device中没有包含指向所对应virtqueue的成员(virtqueue有指向virtio_device的成员)。为了得到与virtio_device相关联的virtqueue,需要使用virtio_config_ops结构中的find_vq函数。这个函数返回与该virtqueue相关联的设备实例。find_vq还允许为virtqueue指定回调函数,用于在hypervisor准备好数据时,通知客户机。

virtqueue结构包含可选的回调函数(用于在hypervisor填充缓冲后,通知客户机)、一个指向virtio_device、一个指向virtqueue操作和一个特别的priv用于底层实现使用。callback是可选的,也可以动态的启用或禁用。

这个层级的核心是virtqueue_ops,其中定义了如何在客户机和hypervisor之间传输命令与数据。我们先来探索virtqueue中对象的添加和删除操作。


Virtio数据流交互机制

vring 主要通过两个环形缓冲区来完成数据流的转发,如下图所示。


vring 包含三个部分,描述符数组 desc,可用的 available ring 和使用过的 used ring。

desc 用于存储一些关联的描述符,每个描述符记录一个对 buffer 的描述,available ring 则用于 guest 端表示当前有哪些描述符是可用的,而 used ring 则表示 host 端哪些描述符已经被使用。

Virtio 使用 virtqueue 来实现 I/O 机制,每个 virtqueue 就是一个承载大量数据的队列,具体使用多少个队列取决于需求,例如,virtio 网络驱动程序(virtio-net)使用两个队列(一个用于接受,另一个用于发送),而 virtio 块驱动程序(virtio-blk)仅使用一个队列。

具体的,假设 guest 要向 host 发送数据,首先,guest 通过函数 virtqueue_add_buf 将存有数据的 buffer 添加到 virtqueue 中,然后调用 virtqueue_kick 函数,virtqueue_kick 调用 virtqueue_notify 函数,通过写入寄存器的方式来通知到 host。host 调用 virtqueue_get_buf 来获取 virtqueue 中收到的数据。



Virtio缓冲

客户机驱动(前端)与hypervisor(后端)通过缓冲区进行通信。对于一次I/O,客户机提供一个或多个缓冲区表示请求。例如,你可以使用三个缓冲区,其中一个用来存储读请求,其他两个用来存储回复数据。内部这个配置被表示为分散/聚集(scatter-gather)列表(列表中的每个元素存储有缓冲区地址与长度)。


核心API

将客户机驱动与hypervisor驱动链接起来,偶尔是通过virtio_device,大多数情况下都是通过virtqueue。virtqueue支持五个API函数。使用第一个函数add_buf向hypervisor添加请求,这种请求以分散/聚集列表的形式,正如先前讨论的。为了提交请求,客户机必须提供请求命令,分散/聚集列表(以缓冲区地址和长度为元素的数组),向外提供请求的缓冲区的数量(也就是发送请求信息给hypervisor),向内传递数据的缓冲区的数量(hypervisor用来填充数据,返回给客户机)。当客户机通过add_buf向hypervisor提交一条请求后,客户机就可以使用kick通知hypervisor新请求已递送。但为了更好地性能,客户机应该在kick通知hypervisor之前,提交尽可能多的请求。

客户机使用get_buf接收从hypervisor中返回的数据。客户机可以简单地使用get_buf轮询或者等待由virtqueue callback函数的通知。当客户机知道了缓冲区数据可用,就会使用get_buf获取数据。

最后两个virtqueue的API是enable_cb和disable_cb,可用使用这两个函数启用和禁用回调函数(callback函数使用find_vq初始化设置)。注意回调函数与hypervisor在不同的地址空间,所以调用需要间接调用(indirect hypervisor call)(例如:kvm_hypercall)。

缓冲区的格式、顺序与内容支队前端和后端驱动有意义。内部传送(现在使用环形缓冲区实现)只传输缓冲区,并不知道内部表达的意义。


Virtio驱动例子

对于各种各样前端驱动,可以在Linux内核源码的./drivers子目录下找到。virtio网络驱动在./driver/net/virtio_net.c,virtio块驱动在./driver/block/virtio_blk.c。./driver/virtio子目录下提供了virtio接口的实现(virtio设备、驱动、virtqueue和环形缓冲区)。virtio也被用在了高性能计算(High-Performance Computing, HPC)研究之中,使用共享内存传递内部虚拟机的信息。特别的,这使用了virtio来实现虚拟化PCI接口。可以再resources中找到相关工作。

可以在Linux内核中练习半虚拟化基础工作。所需要的就是一个作为hypervisor的内核,客户机内核和用来仿真设备的QEMU。可以使用KVM(一个存在于宿主机内核中的模块)或者Rusty Russell的lguest(一个修改过的Linux内核)。两种方案都支持virtio(配合以QEMU进行系统模拟和libvirt进行虚拟化管理)。

Rusty的成果是简化了半虚拟化驱动的开发,并且设备仿真性能更高。最重要的还是,virtio能够提供更好地性能(两三倍的网络I/O)比现有的商业解决方案;虽说有一定的代价,但如果Hypervisor和客户机系统是Linux,还是非常值得的。


小结

尽管阁下可以永远不会为virtio开发前端或者后端驱动,但它实现了一个有趣的架构,值得更加细致的理解它。与先前的Xen相比,virtio为了半虚拟化提高性能提供了新的可能。在作为投入使用的hypervisor和新虚拟技术的实验平台中,Linux不断地证明了它自己。virtio再一次证明了Linux作为hypervisor的优势和开放性。

virtio 是 guest 与 host 之间通信的润滑剂,提供了一套通用框架和标准接口或协议来完成两者之间的交互过程,极大地解决了各种驱动程序和不同虚拟化解决方案之间的适配问题。virtio 抽象了一套 vring 接口来完成 guest 和 host 之间的数据收发过程,结构新颖,接口清晰。


参考来源:

Virtio简介

Virtio一种Linux I/O虚拟化框架


VirtIO-GPU 硬件视频加速获国际社区认可

Virtualization SIG 负责构建 openKylin 社区系统虚拟化技术,打造面向端、边、云的全场景虚拟化解决方案。2022年10月中旬,SIG 小组在虚拟 GPU 优化方向取得了不错成果,率先实现 VirtIO-GPU 支持硬件视频加速技术,解放系统 CPU 占用。目前该技术相关补丁已经合入到 mesa、virglrenderer 上游开源项目,填补了上游国际开源社区虚拟显卡视频硬件解码领域技术空白,并获得国际开源社区认可。

技术介绍

openKylin Virtualization SIG 目前使用的虚拟 GPU 优化方案采用了基于 API 转发的 VirtIO-GPU 虚拟化技术,并借助 virglrenderer 组件实现了 3D 硬件加速,大幅的提升了虚拟机的图形性能(可达 60% 以上),也大幅提高了用户在网页浏览、文件办公和游戏等场景下使用体验。虽然 3D 性能得到了显著优化,但是虚拟 GPU 不支持硬件解码的弊端却导致软件解码 CPU 占用率过高、画面不流畅,甚至丢帧等现象。

所以针对此类问题,openKylin Virtualization SIG 为 VirtIO-GPU 创建了一套采用前后端架构的硬件视频加速机制,为其增加硬件编解码功能。其前端为 “VirtIO-GPU 视频驱动”,后端为 “VirtIO-GPU 视频服务程序”。前后端之间采用 “VirtIO-GPU 视频协议” 进行通信,该协议主要定义了编解码相关的一些命令,如创建编解码器、创建视频缓冲区、解码比特流等。

VirtIO-GPU 硬件视频加速机制整体框架


效果展示

在虚拟机内使用 MPV 播放器分别播放不同编码标准和清晰度的视频文件,可以发现,使用 VirtIO-GPU 硬件解码时的 CPU 占有率明显低于软件编解码,在 X86_64 架构上降幅超过 70%,在 ARM 架构上降幅则超过 90%!大幅提高了用户的使用体验。


另外,使用 Firefox 浏览器进行 4K 在线视频的播放测试,结果表明使用 VirtIO-GPU 硬件解码时 Firefox 解码进程的 CPU 占用率相较于软件解码降低了 95% 以上,同时画面清晰流畅。


目前 VirtIO-GPU 硬件视频加速机制已经实现了 H.264 和 H.265 的硬件解码功能,后续将逐步支持其它视频规范及编码功能,待 openKylin 新的版本发布后,大家即可体验。同时上游合入后 Virtualization SIG 也同 AMD 开源团队开展了相关技术交流探讨后续开发计划,欢迎有志伙伴加入 openKylin 社区 Virtualization SIG,与大家一起交流、共同开发、共同演进,构建 openKylin 社区系统虚拟化技术。

openKylin 社区 Virtualization SIG