转载自https://blog.csdn.net/ZCShouCSDN/article/details/132361829
1. 基于virtio的半虚拟化概述
1.1 virtio运行结构
- virtio 表示虚拟化 IO,用于实现设备半虚拟化,即虚拟机中运行的操作系统需要加载特殊的驱动(e.g. virtio-net)且虚拟机知道自己是虚拟机
相较于基于完全模拟的全虚拟化,基于virtio的半虚拟化可以提升设备访问性能
- 运行在虚拟机中的部分称为前端驱动,负责对虚拟机提供统一的接口
- 运行在宿主机中的部分称为后端驱动,负责适配不同的物理硬件设备
1.2 virtio架构层次
1.2.1 virtio前端驱动
- 运行在虚拟机中
- 针对不同类型的设备有不同的驱动程序,但是与后端驱动交互的接口都是统一的
- 本文分析 virtio-net 模块,源码位于 drivers/net/virtio_net.c
1.2.2 virtio层
- virtio 层实现虚拟队列接口,作为前后端通信的桥梁
- 不同类型的设备使用的虚拟队列数量不同,比如virtio-net使用两个队列,一个用于接收,另一个用于发送
- 源码位于 drivers/virtio/virtio.c
1.2.3 virtio-ring层
- virtio-ring 层是虚拟队列的具体实现
- 源码位于 driver/virtio/virtio_ring.c
1.2.4 virtio后端驱动
- 运行在宿主机中
- 实现 virtio 后端的逻辑,主要是操作硬件设备,比如向内核协议栈发送一个网络包完成虚拟机对于网络的操作
- 在 Qemu + KVM 虚拟化环境中,源码位于 Qemu 源码中(尚未详细分析)。后续将分析 seL4 中的后端实现
2. Linux virtio核心数据结构
2.1 virtio_bus结构
struct bus_type 是基于总线驱动模型的公共数据结构,定义新的 bus,就是填充该结构。virtio_bus 定义在 drivers/virtio/virtio.c 中,具体如下,
说明1:注册 virtio_bus
virtio_bus 以 core_initcall 的方式被注册,该方式注册的启动顺序优先级很高(作为对比,module_init 最终对应的是 device_initcall),在使用中要注意不同组件的启动顺序
说明2:virtio_dev_match 函数
virtio 驱动的 match 涉及到 virtio_device_id 结构,在 virtio_device 结构中包含该结构;在 virtio_driver 中则是包含该驱动支持的virtio_device_id 列表
具体的 match 流程如下,
可见 virtio 驱动的 match 函数先匹配 device 字段,后匹配 vendor 字段,二者都满足条件时 match 成功
补充:从 virtio_dev_match 的流程可以看出,virtio_driver 中的 id_table 必须以 id->device = 0 结尾,以便结束循环
2.2 virtio_device结构
struct virtio_device 定义在 include/linux/virtio.h 中,具体如下
struct virtio_device_id id
其中的 device 成员标识了当前 virtio_device 的用途,virtio-net 是其中的一种,
const struct virtio_config_ops *config
virtio_config_ops 操作集中的函数主要与 virtio_device 的配置相关,主要有如下 2 类操作,
- 实例化 / 反实例化 virtqueue,其中要特别注意 find_vqs 函数,该函数用于实例化 virtio_device 所持有的 virtqueue
- 获取 / 设置 virtio_device 的属性与状态,相关属性均在虚拟机虚拟出的 PCI 配置空间
struct list_head vqs
virtio_device 持有的 virtqueue 链表,virtio-net 中建立了 2 条 virtqueue(虚拟队列)
u64 features
virtio_driver & virtio_device 同时支持的通信特性,也就是前后端最终协商的通信特性
2.3 virtio_driver结构
struct virtio_driver 定义在 include/linux/virtio.h 中,具体如下,
const struct virtio_device_id *id_table
对应 virtio_device 结构中的 id 成员,virtio_device 中标识的是当前 device 的 id 属性;而 virtio_driver 中的 id_table 则是当前 driver 支持的所有 id 列表
const unsigned int *feature_table & unsigned int feature_table_size
feature 列表包含了当前 driver 支持的所有 virtio 传输属性,feature_table_size 则说明属性数组的元素个数
probe 函数
virtio_driver 层面注册的 probe 函数,如上文所述,virtio_bus 层面也注册了 probe 函数,在 Linux 总线驱动框架 & virtio 核心层中,当virtio_device & virtio_driver匹配成功后,先调用 bus 层面的 probe 函数,在 virtio_bus 层面的 probe 函数中,又调用了 virtio_driver 层面的probe 函数
2.4 virtqueue结构
struct virtqueue 定义在 include/linux/virtio.h 中,具体如下,
2.5 vring结构
struct vring 定义在 include/uapi/linux/virtio_ring.h 中,具体如下,
2021 / 08 / 06补充:一定要结合一个后端驱动进行分析,可以对照 rpmsg-lite 分析。可以就分析 rpmsg-lite 对 virtqueue 的操作部分,不用上升到 rpmsg 协议的部分
说明1:vring的三个构成区域
- Destcriptor Table:描述内存 buffer,主要包括 addr & len 等信息
- Avail Ring:用于前端驱动(Guest)通知后端驱动(Host)有可用的描述符。e.g. 前端驱动有一个报文需要发送,需要将其加入 Avail Ring,之后通知后端驱动读取
- Used Ring:用于后端驱动(Host)通知前端驱动(Guest)有可用的描述符,或者是后端驱动已将前端驱动提供的描述符使用完毕。e.g. 后端驱动有一个报文需要发送,需要将其加入Used Ring,之后通知前端驱动读取
可见 avail & used 的命名都是站在 Host 的角度进行的
说明2:vring的存储
vring 结构只是用于描述 vring 在内存中的布局(因此包含的都是指针变量),实际用于通信的 vring 是存储在内存中。上文提到的 vring 的三个区域是在内存中连续存储的,而且是存储在 Guest & Host 共享的一片连续内存中。我们可以通过 vring_init 函数理解 vring 存储结构的布局
实际 vring 的内存布局如下图所示,
在计算 used ring 的起始地址时,在 avail->ring[num] 的地址之后又加了 sizeof(__virtio16)
,也就是增加了 2B,是为了容纳 avail ring 末尾的used_event,该机制详见下文(是一种控制中断触发频率的机制)
说明3:实际 vring 的大小
实际 vring 的大小可以通过 vring_size 函数获得
- 计算 avail ring 时加 3,分别为 flags、idx 和 used_event
- 计算 used ring 时加3,分别为 flags、idx 和 avail_event
- 计算过程中,包含了为满足对齐要求 padding 的空间
说明4:used_event 与 avail_event 机制概述
这 2 个字段均与 virtio 设备的 VIRTIO_RING_F_EVENT_IDX 特性有关,由于 virtio 驱动触发对方中断将导致 CPU 反复进出虚拟机 & 宿主机模式,从而降低性能,因此需要控制触发中断频率的机制
- avail ring 中的 used_event
- 由前端驱动(Geust)设置,标识希望后端驱动(Host)触发中断的阈值
- 后端驱动(Host)在向 Used Ring 加入 buffer 后,检查 Used Ring 中的 idx 字段,只有达到阈值才触发中断
- used_ring 中的 avail_event
- 由后端驱动(Host)设置,标识希望前端驱动(Guest)触发中断的阈值
- 前端驱动(Guest)在向 Avail Ring 加入 buffer 后,检查 Avail Ring 的 idx 字段,只有达到阈值才触发中断
综上所属,vring 结构的构成如下图所示,
2.6 vring_virtqueue结构
vring_virtqueue 结构用于描述前端驱动(Guest)中的一条虚拟队列
总结:virtio_device / vring_virtqueue / virtqueue / vring结构之间的关系
3. virtio操作
如上文所述,virtio 框架向虚拟机中的前端驱动提供了统一的 IO 操作接口,我们下面就分析这些操作。在理解了 virtio 的操作之后,配合不同虚拟设备的属性,就比较容易理解虚拟设备前端驱动的实现。比如 virtio-net 就是网卡驱动 + virtio 操作
3.1 创建virtqueue
3.1.1 核心流程梳理(重点)
virtio_pci_probe阶段
在 virtio 框架中,首先向虚拟机注册的是一个 pci_driver,后续所有 vitio 设备均是以虚拟 pci 设备的形式注册(转载注,这样说应该是不对的,并不是所有的virtio设备都是以pci设备的形式注册的),因此第 1 步流程即是运行virtio_pci_probe 函数
virtio_pci_modern_probe 阶段
在 virtio_pci_probe 函数中,会调用 virtio_pci_modern_probe 函数,进行进一步初始化
在 virtio_pci_modern_probe 函数中会进行 2 步非常重要的操作,
- 设置 virtio_device 中的 virtio_config_ops 回调函数集合,其中就包括了最重要的 find_vqs 回调函数
- 设置 setup_vq 回调函数,该回调函数会在 find_vqs 回调函数中被调用
所以在初始化 virtqueue 的过程中,是先调用 find_vqs 回调函数,后调用 setup_vq 回调函数
find_vqs 回调函数阶段
在 virtio_pci_probe 的最后阶段,会注册 virtio_device,该操作会触发 virtio 驱动的 probe 函数被调用,在该函数中,会触发 find_vqs 回调函数被调用。下面我们以 virtio-net 前端驱动为例,说明创建 virtqueue 的流程
virtio_pci_probe
--> virtio_pci_modern_probe
// 设置find_vqs回调函数
// 设置setup_vq回调函数
--> register_virtio_device // 触发virtio probe函数被调用
// virtio probe函数阶段
virtio_dev_probe
// 调用virtio_driver注册的probe函数
--> virtnet_probe
--> init_vqs
// 分配virtqueue结构(send_queue & recv_queue)
--> virtnet_alloc_queues
--> virtnent_find_vqs
--> find_vqs回调函数(vp_modern_find_vqs)
--> vp_find_vqs
--> vp_find_vqs_msix // 还注册了vring_interrupt
--> vp_setup_vq
--> setup_vq回调函数(setup_vq)
--> vring_create_virtqueue
// 分配存储vring的连续物理内存
--> vring_alloc_queue
// 生成vring_virtqueue结构,并初始化
--> __vring_new_virtqueue
下面我们就分析最核心的几个函数
3.1.2 setup_vq 函数分析
setup_vq 函数有如下 3 个核心步骤,
1. 检查 virtqueue 配置参数
前端驱动读取 PCI 配置空间中的参数,判断其合法性。其中要注意 virtqueue 的长度(queue_size)必须是 2 的幂次,因为后续需要通过简单的位与运算实现绕回
2. 实际生成virtqueue
实际生成 virtqueue 通过 vring_create_virtqueue 函数实现,此处需要注意如下 3 点,
- 对齐要求。如上文所述,vring 结构在内存布局上有对齐要求,该要求在创建 virtqueue 时传递,就是此处的 SMP_CACHE_BYTES 宏
- notify hook 函数。用于前端驱动(Guest)触发后端驱动(Host)中断,通知其有数据需要处理
这里的 notification register 也在 PCI 配置空间中,该地址在 setup_vq 函数中指定
- callback hook 函数。callback hook函数在 virtqueue 被触发时调用,以 virtio-net 驱动为例,callback hoot 函数在 virtnet_find_vqs 函数中指定
3. 同步 GPA 到宿主机
virtqueue 作为前端驱动与后端驱动的交互媒介,需要在虚拟机和宿主机中同步这段共享内存的地址。调用 vring_create_vritqueue 函数生成的 virtqueue,分配的内存为 GPA,需要将其同步到宿主机,宿主机才能将其转换为 HVA 使用(因为虚拟机的 GPA 就是宿主机分配的)
3.1.3 vring_create_virtqueue函数分析
可见 vring 的内存被分配在连续的 1 个或多个 page 中,而且如果内存条件不满足,会动态调整 vring 的长度(num 变量)
3.1.4 _vring_new_virtqueue函数分析
__vring_new_virtqueue
函数的实现注释已经比较清楚了,需要说明的是,该函数返回的是 virtqueue 结构。在 Linux 的 virtio 层实现中,代码会根据需要在 virtqueue 与 vring_virtqueue 结构间进行转换
3.2 前端驱动发送数据
3.2.1 流程概要
- 从 descriptor table 中取出描述符
- 根据要发送的数据填充并组织描述符
- 将组织好的描述符加入 avail ring
- 触发后端驱动中断,通知其处理数据
说明:vring 的描述符结构与 scatterlist 结构是绝配
评论 (0)