一、虚拟化背景
virtio 由 Rusty Russell 开发,原是为支持 lguest 虚拟化解决方案,目前已成为 linux 标准的 I/O 虚拟化框架.本文重点分享 virtio 框架及学习心得,错漏之处望大家指正.先简单介绍下有关虚拟化的背景及基本术语.
虚拟化是云计算的基础架构,通过软件接口化及标准化设计,在计算机硬件上创建一个抽象层,支持将单物理机的硬件元素(处理器、内存、存储等)分成多个虚拟计算机,每个虚拟机都会运行自己的操作系统,其行为就像一台独立的计算机.虚拟化技术在提高资源利用率,软件规模化部署,可移植性,灵活性,安全性等方面具有显著优势.当前虚拟化已成为企业 IT 架构的标准实践.它也是推动云计算经济发展的核心技术.
在智能终端领域,Google 推出 cuttlefish 虚拟化方案,提供操作系统级虚拟化能力,支持将安卓设备部署在云端.除此之外,在工程效能领域也有重要价值,包括以下方面.
1.使平台和应用开发者不再依赖于物理硬件来开发和验证代码更改.
2.通过与核心框架保持高度一致,以高保真度为重点来复制真实设备的基于框架的行为.
3.在各个 API 级别达到一致的功能水平,与物理硬件上的行为保持一致.
4.实现规模化,能够并行运行多台设备,能够并发执行测试,实现高保真度且入门成本较低.
5.提供可配置的设备,能够调整外形规格、RAM、CPU 、I/O 设备,方便自动化测试及参数调优.
二、虚拟机管理器类型
从软件技术层面看,常见的有操作系统级虚拟化,应用级虚拟化(如 arm64 上运行 arm32 应用转义),语言级虚拟化(如 java 虚拟机,python 虚拟机).
虚拟机管理器 (简称 vmm,也称 hypervisor),为 guest os(指 vmm 运行的 os)分配资源,提供运行所需的环境及必要的硬件模拟,也可以看作是虚拟环境中的元操作系统,主要有两种类型.
类型 1 虚拟机管理器直接在物理硬件(通常是服务器)上运行,作用是支持运行多个 OS.通常情况下,在虚拟机管理器上创建和运行虚拟机。有些管理器如 VMware vSphere 支持用户选择要在虚拟机中安装 guest 操作系统.
类型 2 虚拟机管理器作为应用在主机操作系统中运行,使用类型 2 虚拟机管理器,一般需要创建虚拟机, 然后在其中安装 guest os.以 android 系统为例,cuttlefish 虚拟机管理程序 crosvm 就是属于类型 2 的虚拟机管理器.
三、全虚拟化与半虚拟化
不管哪一种 vmm,除了提供 CPU,内存虚拟化之外,还需要提供 I/O 虚拟化能力.从技术实现来看,虚拟化实现分为全虚拟化和半虚拟化两种方式.在全虚拟化中,guest os 在位于裸机上的虚拟机管理程序之上运行. guest os 不知道它正在被虚拟化,并且无需更改即可在此配置中工作.而在半虚拟化中, guest os 不仅知道自己正在 vmm 上运行,而且还包含使 guest os 到 vmm 的转换更加高效的代码.
在完全虚拟化方案中,管理程序必须模拟设备硬件,该设备硬件在会话的最低级别进行模拟.尽管这种抽象模拟很干净,但它也是效率最低且高度复杂的.在半虚拟化方案中,客户和 vmm 可以协同工作以使模拟高效,半虚拟化方法的缺点是操作系统知道它正在被虚拟化并且需要修改才能工作.硬件随着虚拟化不断变化,新处理器结合了高级指令,使 guest os 和 vmm 转换更加高效,I/O 虚拟化的硬件也在不断变化,但在传统的全虚拟化环境中,vmm 必须捕获这些请求,然后模拟真实硬件的行为,尽管这样做提供了最大的灵活性,但它导致效率低下.对于半虚拟化,guest os 包含前端的驱动程序,vmm 实现特定设备模拟的后端驱动程序.这些前端和后端驱动程序是半虚拟化主要特点,它们为模拟设备访问的开发提供标准化接口,且代码高度复用.
四、Virtio 半虚拟化架构
Linux 提供了多种具有不同特性的虚拟机解决方案,包括基于内核的虚拟机 (KVM)、 lguest 和用户模式 Linux,以及设备半虚拟化 virtio 方案. virtio 是半虚拟化管理程序中一组常见模拟设备的抽象,这种设计允许虚拟机管理程序导出一组通用的模拟设备并通过通用的应用程序编程接口(API)访问设备。通过半虚拟化管理程序,guest os 可以实现通用的接口,并在后端驱动程序进行特定的设备模拟.后端驱动程序不必是通用的,只要它们实现前端所需的行为即可. virtio 并不提供各种设备模拟机制(用于网络、块和其他驱动程序),而是为这些设备模拟提供公共前端,以标准化接口并提高跨平台代码的重用.
以图 1 为例说明 virtio 实现原理.在 qemu-kvm 虚拟化方案中,virtio 提供设备虚拟化的能力.qemu 是类型 2 虚拟机管理器,运行在 linux 用户空间中.guest os 是 qemu 创建的运行实例,在 guest os 中包含了 virtio 前端驱动,qemu 实现了 virtio 后端设备,提供设备模拟或通过 host kernel 转接物理设备.guest os 中的 virtio 驱动和 qemu 中的 virtio 设备通过 virtqueue 通信. virtio 定义了控制平面和数据平面的分离架构,两者的侧重各有不同,控制平面追求尽可能的灵活以兼容不同的设备和厂商,而数据平面则追求更高的转发效率以便快速的交换数据包.
五、Virtio 规范
由于 virtio 前端驱动处于 guest os 中,os 种类及厂家各不相同.后端设备模拟处于虚拟机管理器中,且不限于特定的虚拟机管理器,软件的实现由不同厂家独立完成,因此前段驱动和后端设备的交互必须基于开放的标准完成.virtio 规范制定了 virtio 设备的标准,定义了设备状态,功能,通知,复位,配置空间,virtqueue,共享内存,设备操作与设备发现,设备初始化等一些列标准,旨在为 virtio 设备提供直接、高效、标准和可扩展的机制, 而不是针对每个环境或每个操作系统设计的精品机制,任何基于该规范开发的驱动都能支持其匹配的标准虚拟化设备.目前官网已公布了最新 VIRTIO 版本 1.3. 规范由 OASIS 委员会负责制定和发布.
六、virtio 驱动实例分析
前端驱动
结合以 Android 发布的 kernel 6.6 虚拟声卡驱动,分析下 virtio 的实现机制.
首先看声卡驱动的注册,在 virtio_card.c 中定义了声卡驱动.驱动只定义了几个函数和 id table. pm sleep 相关的我们不必关注,id table 定义了虚拟声卡的 device id 和 vendor id,用于匹配虚拟总线上的设备用的,如果匹配到 id table 中的设备,就执行 probe 函数,进行设备初始化操作. remove 函数执行设备卸载的操作.validate 是验证功能和配置空间的函数.前端驱动实现之所以这么简单,在于 virtio 框架实现了虚拟驱动的主要功能.
注册函数 module_virtio_driver 是 module_driver 的宏封装,module_driver 最终定义了驱动的 init 函数和 exit 函数,分别调用 register_virtio_driver 和 unregister_virtio_driver 进行注册和卸载.
register_virtio_driver,其定义在 virtio.c 中,调用了 driver_register 进行驱动注册.可以看到实现和普通驱动没有什么不同,唯一需要注意的是 driver 的 bus 成员赋值 virtio_bus.这真正决定了它是一个 virtio 类型的驱动.
虚拟总线
虚拟总线的注册,和其它总线完全一样,直接调用 bus_register 注册 virtio bus 结构体.
对于 virtio bus,我们重点关注它的 match 函数及 probe 函数.match 函数赋值 virtio_dev_match,这个函数就是执行前面提到的驱动的 id table 和虚拟总线上挂载的设备匹配检查,如果 device id 和 vendor id 匹配成功,先执行 bus 的 probe 函数,主要是完成 vritio 规范中定义的设备状态设置,设备 feature 获取,设备配置等操作.然后就执行 driver 的 probe 函数,进入 driver 的初始化.
不难看出所有 virtio 设备都会挂载到 virtio 总线上,并由总线匹配与其绑定的设备和驱动.这是 linux 驱动开发人员很熟悉的驱动模型,自此并没有看到明显有别于其它类型驱动的地方.
如下图所示 virtio 总线,设备,驱动结构,通过 virtio 总线进行统一管理.
图 2.虚拟设备的总线结构
注册过程及框架层次
再回到虚拟声卡驱动,看看 virtsnd_probe 函数片段.结构体 snd 是 virtio_snd 类型指针,在这之前分配了内存,并赋值 snd->dev=vdev.这个 vdev 是 virtsnd_probe 传递的 virtio_device 类型参数,也是从 virtio bus 的 probe 函数中传递过来的,稍后展开分析.
首先是 spin lock 的初始化, snd 的每个 virtqueue 都有一个 spin lock.接下来 virtio_find_vqs 函数主要是获取 virtqueque. virtqueue 是一个简单的结构,起到 driver 和 device 通信纽带作用.它标识一个可选的回调函数(当虚拟机管理程序使用缓冲区时调用)、对 virtio_device 的引用、对 virtqueue 操作的引用以及引用要使用的底层实现的特殊 priv 引用.虽然回调是可选的,但可以动态启用或禁用回调.
需要注意的是,virtqueue 并不是在 snd 虚拟前端驱动中创建的,而是从 virtio_device 中通过 virtio_config_ops 获取的. virtio_device 不包含对 virtqueue 的引用,要识别与此 virtio_device 关联的 virtqueue,使用 virtio_config_ops 结构和 find_vq 函数,此函数返回与此 virtio_device 实例关联的虚拟队列.find_vq 函数还允许为 virtqueue 指定一个回调函数,该函数用于通知 guest os 来自虚拟机管理程序的缓冲区.
图 3. virtio 前端对象层次结构
virtio_device_ready 语句的作用是使能 virtqueue,建立了虚拟声卡设备的信道.调用了 virtio_config_ops 结构的 set_status 函数.
virtsnd_build_devs 函数真正创建声卡,包括为声卡申请内存及初始化,配置和创建 jack,pcm,chmap 等音频设备,最后调用 snd_card_register 完成声卡注册.从用户空间来看,虚拟声卡和物理的声卡设备的接口完全一致,前端驱动及框架封装了虚拟化的底层逻辑,对于用户层面是无感知的,应用程序不需要任何修改即能兼容虚拟化设备.
通过以上分析,不难看出,驱动的核心操作是通过 virtio_deivce 结构的引用及相关回调实现的,virtio 框架实现了设备识别,配置,初始化等高效复用的代码.一个重要的结果是保证了前端驱动的简单化,标准化,易扩展的特征.virtio 设计原则是既要解决传统 I/O 全虚拟化方案的低效问题,也要 guest os 修改最小且保证兼容性及扩展性,这是架构设计重点考虑的问题.
虚拟设备
在总线和驱动的操作中,都涉及一个核心的结构 virtio_device,包含设备信息及操作.这是虚拟化设备的表示,它封装了 virtqueue 及 vring 重要操作和设备信息.应注意到,前端驱动及 virtio 框架(guest)并不包含此结构的创建,甚至也不能确定虚拟设备以什么方式连接到 guest 系统中,它是由总线和驱动的 probe 函数直接或间接的传递来的,事实上从 guest 的代码中是看不出此设备在哪里完成初始化的.
回到图一 virtio 框架示例中,virt_device 是在 vmm 中实现的,而 guest os 中的 virtio_device 对象可以看做作设备在 guest 侧的表示,或看作是远程对象的引用.
在 virtio 1.3 规范第 2 章 Basic Facilities of a Virtio Device 中定义了虚拟设备的功能标准,在第 4 章 virtio transport options 中定义了设备接入方式,包括 PCI, MMIO, Channel IO 三种方式,virtio 可以使用不同的总线接入,最常用的是 PCI. virtio 设备可以实现任何类型的 PCI 设备,包括传统 PCI 设备或 PCI Express 设备.使用 virtio over PCI 总线的 virtio 设备必须向 guest os 公开一个满足相应 PCI 规范(PCI 或 PCIe)要求的接口.
对于使用 PCI 作为虚拟 I/O 的虚拟机管理器来说,只需要通过 PCI 设备模拟就可以实现和 guest os 以 PCI 协议的方式通信了.对于 guest os 来说,虚拟设备首先是一个 PCI 设备,通过 vmm 分配的 PCI 地址空间发现 PCI 设备,并通过 PCI PID/VID 匹配和 virtio_pci_id_table 中支持的列表做匹配,如果匹配到,则绑定 virtio_pci_driver 并执行 virtio_pci_probe 函数, virtio_pci_probe 除了完成 PCI 协议规定的操作外,还完成 virtio_device 的创建和初始化,最后调用 register_virtio_device 注册到虚拟总线(注册函数中 virtio_device 绑定了 virtio_bus),至此完成了虚拟设备的注册,并调用了 virtio bus 的 probe 函数,如前章节所述,virtio bus 在 probe 函数中匹配前端驱动.
注意这里的 PCI device id 和 vendor id 和虚拟声卡设备中的 id 是不同的概念,不过存在一个对应关系.任何 PCI 供应商 ID 为 0x1AF4 且 PCI 设备 ID 为 0x1000 到 0x107F 的 PCI 设备都是 virtio 设备.非过渡设备必须具有通过将 0x1040 加上 virtio 设备 ID 计算得出的 PCI 设备 ID.
七、小结
综上所述,virtio 框架实现 virtio 规范要求,设备通过 PCI 等成熟协议接入,创建虚拟总线框架以便管理驱动及设备,除此之外并不需要创造或依赖新的技术,新的通信协议或新的软件概念.从 PCI 角度看,虚拟设备就是普通的 PCI 设备,从前端设备驱动来看,虚拟设备就是挂载在虚拟总线上的普通设备.如此最大化减少了方案推广的成本,为 guest 端提供简单,通用,易扩展的解决方案。
参考文献:
https://developer.ibm.com/art...
https://www.linux-kvm.org/pag...
https://docs.oasis-open.org/v...
https://blogs.oracle.com/linu...
END
作者:Jacky
来源:OPPO内核工匠
推荐阅读
- MCU 无感 OTA 升级功能
- 嵌入式开发之 D-Bus 通信机制
- 零内存泄漏!2KB高效实现事件驱动框架,推荐这款开源事件管理器——LwEVT
- 别再用传统方法解析串口协议啦!单片机高效开发妙招,自定义协议处理效率翻倍(附源码)
欢迎大家点赞留言,更多 Arm 技术文章动态请关注极术社区嵌入式客栈专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。