设备层虚拟化抽象之Virtio记录(202x)


本文用于收录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
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