本文为 “Virtio” 相关文章合辑。
略作重排,如有内容异常,请看原文。
Virtio 简介(一)—— 框架分析
posted @ 2021-04-21 10:14 Edver
1. 概述
在传统设备模拟中,虚拟机内部设备驱动完全不知自身处于虚拟化环境,因此 I/O 操作需完整经过“虚拟机内核栈 → QEMU → 宿主机内核栈”,产生大量 VM Exit 和 VM Entry,导致性能较差。Virtio 方案旨在提升 I/O 性能。在此方案中,虚拟机能够感知自身处于虚拟化环境,并加载相应的 virtio 总线驱动和 virtio 设备驱动,按其定义的协议进行数据传输,减少 VM Exit 和 VM Entry 操作。
2. 架构
VirtIO 由 Rusty Russell 开发,针对虚拟化 hypervisor 中的一组通用模拟设备 I/O 的抽象。Virtio 采用前后端架构,包括前端驱动(Guest 内部)、后端设备(QEMU 设备)和传输协议(vring)。框架如下图所示:
前端驱动:虚拟机内部的 virtio 模拟设备对应的驱动。其作用为接收用户态的请求,按传输协议对请求进行封装,再执行 I/O 操作,并发送通知至 QEMU 后端设备。
后端设备:在 QEMU 中创建,用于接收前端驱动发送的 I/O 请求,按传输协议进行解析,再对物理设备进行操作,之后通过终端机制通知前端设备。
传输协议:使用 virtio 队列(virtio queue,virtqueue)完成。设备有若干个队列,每个队列处理不同的数据传输(如 virtio-balloon 包含 ivq、dvq、svq 三个)。
virtqueue 通过 vring 实现。Vring 是虚拟机和 QEMU 之间共享的一段环形缓冲区,QEMU 和前端设备均可从 vring 中读取数据和放入数据。
3. 原理
3.1 整体流程
从代码层面来看,virtio 的代码主要分为 QEMU 和内核驱动程序两部分。Virtio 设备的模拟由 QEMU 完成,QEMU 在虚拟机启动之前创建虚拟设备。虚拟机启动后检测到设备,调用内核的 virtio 设备驱动程序来加载该 virtio 设备。
对于 KVM 虚拟机,其均通过 QEMU 这一用户空间程序创建,每个 KVM 虚拟机对应一个 QEMU 进程,虚拟机的 virtio 设备由 QEMU 进程模拟,虚拟机的内存也从 QEMU 进程的地址空间内分配。
VRING 是由虚拟机 virtio 设备驱动创建的用于数据传输的共享内存,QEMU 进程通过这块共享内存获取前端设备递交的 I/O 请求。整个虚拟机 I/O 请求的流程如下图所示:
- 虚拟机产生的 I/O 请求被前端的 virtio 设备接收,并存放在 virtio 设备散列表 scatterlist 中;
- Virtio 设备的 virtqueue 提供 add_buf 将散列表中的数据映射至前后端数据共享区域 Vring 中;
- Virtqueue 通过 kick 函数通知后端 qemu 进程。Kick 通过写 pci 配置空间的寄存器产生 kvm_exit;
- Qemu 端注册 ioport_write/read 函数监听 PCI 配置空间的改变,获取前端的通知消息;
- Qemu 端维护的 virtqueue 队列从数据共享区 vring 中获取数据;
- Qemu 将数据封装成 virtioreq;
- Qemu 进程将请求发送至硬件层。
前后端主要通过 PCI 配置空间的寄存器完成前后端的通信,而 I/O 请求的数据地址则存在 vring 中,并通过共享 vring 这一区域实现 I/O 请求数据的共享。
从上图可以看出,Virtio 设备的驱动分为前端与后端:前端是虚拟机的设备驱动程序,后端是 host 上的 QEMU 用户态程序。为实现虚拟机中的 I/O 请求从前端设备驱动传递到后端 QEMU 进程中,Virtio 框架提供了两个核心机制:前后端消息通知机制和数据共享机制。
消息通知机制:前端驱动设备产生 I/O 请求后,可通知后端 QEMU 进程去获取这些 I/O 请求,并递交给硬件。
数据共享机制:前端驱动设备在虚拟机内申请一块内存区域,将该内存区域共享给后端 QEMU 进程,前端的 I/O 请求数据放入这块共享内存区域,QEMU 接收到通知消息后,直接从共享内存取数据。由于 KVM 虚拟机就是一个 QEMU 进程,虚拟机的内存均由 QEMU 申请和分配,属于 QEMU 进程的线性地址的一部分,因此虚拟机只需将这块内存共享区域的地址传递给 QEMU 进程,QEMU 即能直接从共享区域存取数据。
3.2 PCI 配置空间
由整体流程图可知,guest 和 host 交互传送信息的两个重要结构分别为 PCI config 和 vring,本节重点分析实现消息通知机制的 PCI 配置空间。
3.2.1 虚拟机如何获取 PCI 配置空间
首先,为虚拟机创建的 virtio 设备均为 PCI 设备,它们挂在 PCI 总线上,遵循通用 PCI 设备的发现、挂载等机制。
当虚拟机启动发现 virtio PCI 设备时,只有配置空间可被访问,配置空间内保存着该设备工作所需的信息,如厂家、功能、资源要求等。通过对该空间信息的读取,可完成对 PCI 设备的配置。同时,配置空间上有一块存储器空间,包含了一些寄存器和 I/O 空间。
前后端的通知消息即写在这些存储空间的寄存器中,virtio 会为其 PCI 设备注册一个 PCI BAR 来访问该寄存器空间。配置空间如下图所示:
虚拟机系统在启动过程中在 PCI 总线上发现 virtio-pci 设备后,会调用 virtio-pci 的 probe 函数。该函数将 PCI 配置空间上的寄存器映射到内存空间,并将该地址赋值给 virtio_pci_device 的 ioaddr 变量。之后,对 PCI 配置空间上的寄存器操作时,只需使用 ioaddr 加偏移量即可。
pci_iomap 函数完成 PCI BAR 的映射,第一个参数是 pci 设备的指针,第二个参数指定要映射的是 0 号 BAR,第三个参数确定要映射的 BAR 空间大小。当第三个参数为 0 时,将整个 BAR 空间均映射到内存空间上。VirtioPCI 设备的 0 号 BAR 指向的就是配置空间的寄存器空间,即配置空间上用于消息通知的寄存器。
通过 pci_iomap 之后,即可像操作普通内存一样(调用 ioread 和 iowrite)来读写 pci 硬件设备上的寄存器。
3.2.2 虚拟机如何操作该配置空间
- kick
当前端设备的驱动程序需通知后端 QEMU 程序执行某些操作时,会调用 kick 函数,触发读写 PCI 配置空间寄存器的动作。
- 读写 PCI 寄存器
ioread/iowrite 实现了对配置空间寄存器的读写,例如:
vp_dev->ioaddr + VIRTIO_PCI_QUEUE_NOTIFY 表示写 notify 这个寄存器,位置如图 2-1 所示。
ioread 用于读取 QEMU 端在配置空间寄存器上写下的值。
在读写 PCI 设备配置空间的操作中,均通过 ioaddr + 偏移来指向某个寄存器,ioaddr 这一变量是在 Virtio-pci 设备初始化时对其赋值,并指向配置空间寄存器的首地址位置。
3.2.3 QEMU 如何感知虚拟机的操作
虚拟机内调用 kick 函数实现通知后,会产生 KVM_EXIT。Host 端的 kvm 模块捕获到该 EXIT 后,根据其退出原因进行处理。若为 IO_EXIT,kvm 会将该退出交给用户态的 QEMU 程序来完成 I/O 操作。
QEMU 为 kvm 虚拟机模拟了 virtio 设备,因此后端的 virtio-pci 设备也是在 QEMU 进程中模拟生成的。QEMU 对模拟的 PCI 设备的配置空间注册了回调函数,当虚拟机产生 IO_EXIT 时,即调用这些函数来处理事件。
此处仅分析 legacy 模式,实际上在初始化阶段 guest 会判断设备是否支持 modern 模式,若支持,回调函数会有所不同。后续有时间会补充相关内容。
- 监听 PCI 寄存器
virtio_ioport_write/read 即为 QEMU 进程监听 PCI 配置空间上寄存器消息的函数,针对前端 iowrite/ioread 读写了哪个 PCI 寄存器,来决定下一步操作:
- 监听函数的注册
PCI 寄存器的这些监听函数均在 QEMU 为虚拟机创建虚拟设备时注册。
QEMU 先为虚拟机的 virtio-pci 设备创建 PCI 配置空间,配置空间内包含设备的一些基本信息;在配置空间的存储空间位置注册了一个 PCI BAR,并为该 BAR 注册了回调函数以监听寄存器的改变。
以下代码为初始化配置空间的基本信息:
为 PCI 设备注册 PCI BAR,指定起始地址为 PCI_BASE_ADDRESS_SPACE_IO(即 PCI 配置空间中存储空间到配置空间首地址的偏移值);指定该 BAR 的大小为 size,回调函数为 virtio_pci_config_ops 中的读写函数。
这里的 read/write 最终均会调用 virtio_ioport_write(virtio_ioport_write 处理前端写寄存器时触发的事件,virtio_ioport_read 处理前端要读寄存器时触发的事件)来进行统一管理。
3.3 前后端数据共享
上一节分析了消息通知机制,消息通知之后数据如何传送呢?在整体流程图中,我们其实已经画出——vring。
3.3.1 Vring 数据结构
struct vring {unsigned int num;struct vring_desc *desc;struct vring_avail *avail;struct vring_used *used;
};
VRING 共享区域总共包含三个表:
vring_desc 表,存放虚拟机产生的 I/O 请求的地址;
vring_avail 表,指明 vring_desc 中哪些项是可用的;
vring_used 表,指明 vring_desc 中哪些项已经被递交到硬件。
如此,我们可将 I/O 请求存入 vring_desc 表中,通过 vring_avail 表告知 QEMU 进程 vring_desc 表中哪些项是可用的,QEMU 将 I/O 请求递交到硬件执行后,通过 vring_used 表告知前端 vring_desc 表中哪些项已被递交,可释放这些项。
- vring_desc
/* Virtio ring descriptors: 16 bytes. These can chain together via "next". */
struct vring_desc {/* Address (guest-physical). */__virtio64 addr;/* Length. */__virtio32 len;/* The flags as indicated above. */__virtio16 flags;/* We chain unused descriptors via this, too */__virtio16 next;
};
用于存储虚拟机产生的 I/O 请求在内存中的地址(GPA 地址),该表中每一行包含四个字段,如下所示:
- addr:存储 I/O 请求在虚拟机内的内存地址,是一个 GPA 值;
- len:表示该 I/O 请求在内存中的长度;
- flags:指示该行数据是可读、可写(VRING_DESC_F_WRITE),是否是一个请求的最后一项(VRING_DESC_F_NEXT);
- next:每个 I/O 请求都可能包含 vring_desc 表中的多行,next 域指明该请求的下一项在哪一行。
实际上,通过 next 域,我们将一个 I/O 请求在 vring_desc 中存储的多行连接成一个链表,当 flag = ~VRING_DESC_F_NEXT 时,表示该链表到达末尾。
如下图所示,desc 表中有两个 I/O 请求,分别通过 next 域组成了链表:
- vring_avail
存储每个 I/O 请求在 vring_desc 中连接成的链表的表头位置。数据结构如下所示:
struct vring_avail {__virtio16 flags;__virtio16 idx;__virtio16 ring[];
};
在 vring_desc 表中:
- ring[]:通过 next 域连接起来的链表的表头在 vring_desc 表中的位置;
- idx:指向 ring 数组中下一个可用的空闲位置;
- flags:是一个标志域。
如下图所示,vring_avail 表指明 vring_desc 表中有两个 I/O 请求组成的链表是最近更新可用的,它们分别从 0 号位置和 3 号位置开始。
- vring_used
struct vring_used_elem {/* Index of start of used descriptor chain. */__virtio32 id;/* Total length of the descriptor chain which was used (written to) */__virtio32 len;
};struct vring_used {__virtio16 flags;__virtio16 idx;struct vring_used_elem ring[];
};
vring_used 中 ring[] 数组包含两个成员:
- id:表示处理完成的 I/O request 在 vring_desc 表中组成的链表的头结点位置;
- len:表示链表的长度;
- idx:指向 ring 数组中下一个可用的位置;
- flags:是标记位。
如下图所示,vring_used 表表示 vring_desc 表中从 0 号位置开始的 I/O 请求已被递交给硬件,前端可释放 vring_desc 表中的相应项。
3.3.2 对 Vring 进行操作
Vring 的操作分为两部分:在前端虚拟机内,通过 virtqueue_add_buf 将 I/O 请求的内存地址存入 vring_desc 表中,同时更新 vring_avail 表;在后端 QEMU 进程内,根据 vring_avail 表的内容,通过 virtqueue_get_buf 从 vring_desc 表中取出数据,同时更新 vring_used 表。
-
virtqueue_add_buf
- 将 I/O 请求的地址存入当前空闲的 vring_desc 表中的 addr 字段(若无空闲表项,则通知后端完成读写请求,释放空间);
- 设置 flags 字段,若本次 I/O 请求尚未完成,则为 VRING_DESC_F_NEXT,并转下一步;若本次 I/O 请求的地址已全部存入 vring_desc 表中,则为 ~VRING_DESC_F_NEXT;
- 根据 next 字段,找到下一个空闲的 vring_desc 表项,返回第一步;
- 本次 I/O 请求已全部存入 vring_desc 表中,并通过 next 字段连接成一个链表,将该链表的头结点在 vring_desc 表中的位置写入 vring_avail->ring[idx],并将 idx 加 1。
在前端虚拟机内通过上述步骤将 I/O 请求地址存入 vring_desc 表中,并通过 kick 函数通知后端来读取数据。
-
virtqueue_get_buf
- 从 vring_avail 表中取出数据,直到取到 idx 位置为止;
- 根据 vring_avail 表中取出的值,从 vring_desc 表中取出链表的头结点,并根据 next 字段依次找到其余结点;
- 将链表头结点的位置值存入 vring_used->ring[idx].id。
如下图所示,在 QEMU 进行操作之前,vring_avail 表显示 vring_desc 表中有两个新的 I/O 请求。从 vring_avail 表中取出第一个 I/O 请求的位置(vring_desc 表的第 0 行),从 vring_desc 表的第 0 行开始获取 I/O 请求,若 flags 为 NEXT,则根据 next 继续往下寻找;若 flags 为 ~NEXT,则表示该 I/O 请求已结束。
3.3.3 前端对 vring 的管理
该部分较为复杂,后续会逐步完善。Vring 属于 vring_virtqueue,同时 vring_virtqueue 包含 virtqueue。二者分工明确:vring 负责数据面,virtqueue 负责控制面。
以 virtio-balloon. 为例,分析前后端数据共享的源头:GUEST 内部如何管理 vring,以及 vring 的创建过程。
3.3.3.1 结构体
/* virtio_balloon 驱动结构 */
struct virtio_balloon {struct virtio_device *vdev;/* balloon 包含三个 virtqueue */struct virtqueue *inflate_vq, *deflate_vq, *stats_vq;...
};struct virtqueue {struct list_head list;void (*callback)(struct virtqueue *vq);const char *name;struct virtio_device *vdev;unsigned int index;unsigned int num_free;void *priv;
};struct vring_virtqueue {struct virtqueue vq;/* Actual memory layout for this queue */struct vring vring;...
};
3.3.3.2 数据共享区创建
由 Linux 驱动模型可知,驱动入口函数为 virtballoon_probe
,我们由此来分析数据共享区创建过程,整体调用逻辑如下:
设备驱动层:
virtballoon_probe ->
init_vqs ->
virtio_find_vqs ->
vdev->config->find_vqs
PCI 设备层:
(vdev->config->find_vqs) vp_modern_find_vqs ->
vp_modern_find_vqs ->
vp_find_vqs ->
vp_find_vqs_intx/msix ->
vp_setup_vq -> // 实现 pci 设备中 virtqueue 的赋值
vp_dev->setup_vq // 真正创建 virtqueue
virtqueue 创建:
setup_vq ->
// 1. 获取设备注册的 virtqueue 大小等信息
vp_ioread16
// 2. 创建 vring
vring_create_virtqueue ->
__vring_new_virtqueue
// 3. 共享内存地址通知 qemu 侧模拟的设备
vp_iowrite16
// 4. 更新 notify 消息发送的地址
vq->priv update
Virtio 简介(二)—— Virtio-balloon Guest 侧驱动
posted @ 2022-01-25 10:29 Edver
1. 概述
在后端模拟出 balloon 设备后,guest OS 在启动时会扫描到该设备,并遵循 Linux 设备模型进行初始化工作。Virtio-balloon 属于 virtio 体系,其细节需结合 virtio 的工作流程进行分析。本章主要分析 balloon 的行为,涉及 virtio 的部分后续会补充。
balloon 执行流程如下:
2. 驱动创建
2.1 驱动注册
在 Linux 设备驱动模型中,各驱动可按总线类别进行划分,每个总线类别下可挂载“驱动”和“设备”两类对象。内核维护了一张“总线”到“驱动和设备”的映射表。当一个新驱动加入内核时,内核会扫描该驱动所挂载总线上的所有设备,并通过比对驱动中的 id_table 字段和设备配置空间中的 Device ID 来判断是否匹配。若匹配,则内核会针对该设备调用总线的 probe 函数(若总线无 probe 函数,则调用驱动的 probe 函数)。
Linux 内核中前端代码及 virtio-balloon-pci 设备初始化过程
在 Linux 内核中,前端代码主要包含 driver/virtio
目录下的相关文件以及 driver/virtio_balloon.c
。最终生成的内核模块有 virtio.ko
、virtio_ring.ko
、virtio_pci.ko
和 virtio_balloon.ko
。
在 Linux 内核设备驱动体系中,virtio-balloon-pci
设备隶属于 virtio-pci
设备类别,而 virtio-pci
设备本质上属于 PCI 设备家族。基于此层级关系,virtio-pci
设备驱动遵循 PCI 设备驱动模型,将其注册至 PCI 总线。以下详述该设备驱动的完整初始化流程:
-
内核模块加载与总线注册阶段:
- 内核依据驱动模块的依赖关系,优先定位并加载
virito-pci.ko
驱动模块。在此过程中,系统顺序加载virtio.ko
、virtio-ring.ko
以及virtio_pci.ko
模块。其中,virtio_pci.ko
模块依赖于前两者提供的基础功能。 virtio.ko
模块执行初始化函数时,会在系统中创建并注册名为virtio
的新总线类型,为后续相关设备的接入提供统一管理机制。同时,virtio_pci.ko
模块的初始化函数触发已注册的virtio_pci_probe
函数执行,为设备注册做准备。
- 内核依据驱动模块的依赖关系,优先定位并加载
-
设备注册阶段:
virtio_pci_probe
函数通过调用register_virtio_device
函数,在virtio
总线架构下完成一个新的virtio
设备实例的注册操作,构建起设备与总线的关联关系。
-
次级驱动搜索与加载阶段:
- 内核在完成上述设备注册后,依据设备属性与驱动匹配规则,再次执行驱动模块的搜索流程。最终定位并加载
virtio_balloon.ko
模块,随即调用其初始化函数。
- 内核在完成上述设备注册后,依据设备属性与驱动匹配规则,再次执行驱动模块的搜索流程。最终定位并加载
-
总线驱动注册与优先级调度阶段:
virtio_balloon
模块的初始化函数将virtio_balloon
驱动注册至virtio
总线,并触发总线的probe
函数(即virtio_dev_probe
)执行。需注意,在 Linux 设备驱动模型中,总线的probe
函数具备高于总线上设备probe
函数的执行优先级,以确保总线层面的配置与设备初始化的有序性。
-
最终初始化完成阶段:
virtio_dev_probe
函数通过调用virtballoon_probe
函数,完成设备与驱动的深度绑定、资源分配及功能配置等核心初始化任务,至此virtio-balloon-pci
设备驱动初始化流程全部完成 。
从 linux 设备初始化开始到调用 virtballoon_probe 的过程简化如下,仅供参考:
驱动可执行的动作包含在 virtio_balloon_driver
定义的结构体中。先来看下这个结构体的内容,文件位置为 driver/virtio/virtio_balloon.c
。
static unsigned int features[] = {VIRTIO_BALLOON_F_MUST_TELL_HOST,VIRTIO_BALLOON_F_STATS_VQ,VIRTIO_BALLOON_F_DEFLATE_ON_OOM,
};static struct virtio_driver virtio_balloon_driver = {.feature_table = features,.feature_table_size = ARRAY_SIZE(features),.driver.name = KBUILD_MODNAME,.driver.owner = THIS_MODULE,.id_table = id_table,.probe = virtballoon_probe,.remove = virtballoon_remove,.config_changed = virtballoon_changed,
#ifdef CONFIG_PM_SLEEP.freeze = virtballoon_freeze,.restore = virtballoon_restore,
#endif
};
module_virtio_driver(virtio_balloon_driver);
可以看到,注册的 driver
中注册了 feature
属性、driver
的名称和 owner
,以及驱动加载的 probe
、卸载的 remove
、感知变化的 config_changed
。这三个函数做了主要的工作。
- feature_table:定义了 virtio-balloon 支持的特性列表,如 VIRTIO_BALLOON_F_MUST_TELL_HOST、VIRTIO_BALLOON_F_STATS_VQ 和 VIRTIO_BALLOON_F_DEFLATE_ON_OOM 等。
- id_table:用于匹配 virtio-balloon 设备的设备 ID 表,确保驱动能够识别并绑定到正确的设备。
- probe:当发现匹配的设备时,调用此函数完成设备初始化。
- remove:当设备被移除时,调用此函数进行清理工作。
- config_changed:当设备配置发生变化时,调用此函数进行处理。
先来看下加载做了什么工作。
static int virtballoon_probe(struct virtio_device *vdev)
{struct virtio_balloon *vb;int err;// device 的 get 回调函数,用来获取 qemu 侧模拟的设备的 config 数据// 回调在 virtio_pci_modern.c 中注册,原型为 vp_getif (!vdev->config->get) {dev_err(&vdev->dev, "%s failure: config access disabled\n", __func__);return -EINVAL;}// 申请一个 virtio_balloon 结构vdev->priv = vb = vb_dev = kmalloc(sizeof(*vb), GFP_KERNEL);if (!vb) {err = -ENOMEM;goto out;}// 需要释放的页面默认为 0,即 gust 默认保留全部页面,不使用 balloon 释放vb->num_pages = 0;mutex_init(&vb->balloon_lock);// 初始化了两个工作队列,用于通知对应工作队列有消息到达,需要被唤醒init_waitqueue_head(&vb->config_change);init_waitqueue_head(&vb->acked);vb->vdev = vdev;vb->need_stats_update = 0;// 尝试申请用于 balloon 的页面,如果失败一次则增加一// 用来记录失败次数,如果短时间失败过多表明 gust 无多余内存可提供给 balloonvb->alloc_page_tried = 0;// 是否停止 balloon,如 gustos 发生了 lowmemkiller 即内存不够 gust 使用,则停止 balloonatomic_set(&vb->stop_balloon, 0);balloon_devinfo_init(&vb->vb_dev_info);
#ifdef CONFIG_BALLOON_COMPACTIONvb->vb_dev_info.migratepage = virtballoon_migratepage;
#endif// 初始化 virtqueue,用于和后端设备进行通信// 创建了 3 个 queue 用于 ivq/dvq/svq 时间的信息传输// 同时注册了三个 callback 函数,用来唤醒上面写的两个工作队列err = init_vqs(vb);if (err)goto out_free_vb;// 向 oom 的 notify 链表中添加处理回调函数,在 out_of_memory 函数中会调用vb->nb.notifier_call = virtballoon_oom_notify;vb->nb.priority = VIRTBALLOON_OOM_NOTIFY_PRIORITY;err = register_oom_notifier(&vb->nb);if (err < 0)goto out_oom_notify;// 读取设备侧 config 的 status,检查 VIRTIO_CONFIG_S_DRIVER_OK 是否置位// 若已置位说明设备侧已经可用virtio_device_ready(vdev);// 启动 vballoon 线程,balloon 主要操作在这里完成vb->thread = kthread_run(balloon, vb, "vballoon");if (IS_ERR(vb->thread)) {err = PTR_ERR(vb->thread);goto out_del_vqs;}return 0;out_del_vqs:unregister_oom_notifier(&vb->nb);
out_oom_notify:vdev->config->del_vqs(vdev);
out_free_vb:kfree(vb);
out:return err;
}
在 virtballoon_probe 函数中,主要完成以下工作:
- 初始化工作队列:通过 init_waitqueue_head 初始化两个工作队列,用于接收 QEMU 发来的通知。
- 初始化 virtqueue:调用 virtio_find_vqs 函数,根据设备支持的特性初始化三个 virtqueue(ivq、dvq 和 svq),分别用于处理不同的任务。
- 启动内核线程:创建并启动一个内核线程,用于执行 virtio-balloon 的主要逻辑,如内存的充气和放气操作。
2.2 vballoon 如何运作
static int balloon(void *_vballoon)
{struct virtio_balloon *vb = _vballoon;// 注册工作队列的唤醒函数DEFINE_WAIT_FUNC(wait, woken_wake_function);set_freezable();while (!kthread_should_stop()) {s64 diff;try_to_freeze();// 将 wait 添加到 config_change 的队列,等待唤醒// 唤醒操作需要 virtballoon_changed 处理,其注册到了驱动的 config_changed// qemu 执行 virtio_notify_config 发送 notify 时会被调用/* gust 侧唤醒队列的调用栈如下vp_interrupt-> vp_config_changed-> virtio_config_changed-> __virtio_config_changed-> drv->config_changed (virtballoon_changed)*/add_wait_queue(&vb->config_change, &wait);for (;;) {// towards_target 用来计算要释放的 page 数量 ->num_pagesif (((diff = towards_target(vb)) != 0 && vb->alloc_page_tried < 5) ||vb->need_stats_update || !atomic_read(&vb->stop_balloon) ||kthread_should_stop() || freezing(current))// 需要执行 balloon 则退出这层循环break;wait_woken(&wait, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);vb->alloc_page_tried = 0;atomic_set(&vb_dev->stop_balloon, 0);}// 去除等待队列,处理时暂不接受新的 balloon 的 notifyremove_wait_queue(&vb->config_change, &wait);// 更新 stat 信息,在初始化时置零,在 stats_request 调用时置一,并唤醒 config_change 队列// stats_request 放入了 virtqueue 的 callbackif (vb->need_stats_update)stats_handle_request(vb);// diff 大于零表示需要从 gust 申请内存放入 balloon,释放内存// 这样 gust 可用的内存减少,因为内存释放所以 host 可用内存增多if (diff > 0)fill_balloon(vb, diff);// diff 小于零,表示 gust 需要从 balloon 中回收内存// 这样 gust 可用内存增加,host 内存被 gust 占用则可用内存减少else if (diff < 0)leak_balloon(vb, -diff);// 更新 balloon 中记录的 actual,刷新 balloon 实际申请到或释放掉的内存update_balloon_size(vb);/** For large balloon changes, we could spend a lot of time* and always have work to do. Be nice if preempt disabled.*/cond_resched();}return 0;
}
主要涉及到的处理:
- 添加等待队列,等待
config_change
被唤醒,即 QEMU 有执行 balloon 操作。 - 计算需要申请或者释放的空间,即
diff
值。 - 如果需要申请或者释放空间,则调用
fill_balloon
或者leak_balloon
进行操作。 - 更新 balloon 实际占用的空间,记录到
actual
变量中,并通知给 QEMU。
计算 diff
值的操作如下:
static inline s64 towards_target(struct virtio_balloon *vb)
{s64 target;u32 num_pages;// 获取最新的 num_pages 数据virtio_cread(vb->vdev, struct virtio_balloon_config, num_pages, &num_pages);/* Legacy balloon config space is LE, unlike all other devices. */if (!virtio_has_feature(vb->vdev, VIRTIO_F_VERSION_1))num_pages = le32_to_cpu((__force __le32) num_pages);target = num_pages;// 使用最新的 num_pages 数据和已有的数据做差return target - vb->num_pages;
}
在 virtio-balloon 的内核线程中,主要逻辑如下:
- 监听通知:将当前线程加入到工作队列中,等待 QEMU 通过 virtio 配置空间发送的通知。
- 处理通知:当收到通知后,根据通知内容执行相应的操作,如更新 balloon 的目标大小或处理统计信息。
- 内存充气和放气:根据 balloon 的目标大小,调用 fill_balloon 或 leak_balloon 函数,动态调整虚拟机的内存使用。
2.3 balloon 充气过程
static void fill_balloon(struct virtio_balloon *vb, size_t num)
{struct balloon_dev_info *vb_dev_info = &vb->vb_dev_info;/* We can only do one array worth at a time. */num = min(num, ARRAY_SIZE(vb->pfns));mutex_lock(&vb->balloon_lock);for (vb->num_pfns = 0; vb->num_pfns < num;vb->num_pfns += VIRTIO_BALLOON_PAGES_PER_PAGE) {// 从 gust 空间申请一个页面,并且加入到 vb_dev_info->pages 链表中// 并标记 page 的 mapcount 和设定 private 标志。这样可以让 page 不会被 kernel 继续使用struct page *page = balloon_page_enqueue(vb_dev_info);if (!page) {dev_info_ratelimited(&vb->vdev->dev,"Out of puff! Can't get %u pages\n",VIRTIO_BALLOON_PAGES_PER_PAGE);vb->alloc_page_tried++;/* Sleep for at least 1/5 of a second before retry. */msleep(200);break;}// 清零页面申请失败计数vb->alloc_page_tried = 0;// 填充 vb->pfns 数组对应项(不太清楚作用,需再分析)set_page_pfns(vb, vb->pfns + vb->num_pfns, page);// num_pages 为通知 QEMU 侧申请到的页面数量vb->num_pages += VIRTIO_BALLOON_PAGES_PER_PAGE;if (!virtio_has_feature(vb->vdev,VIRTIO_BALLOON_F_DEFLATE_ON_OOM))adjust_managed_page_count(page, -1);}/* Did we get any? */if (vb->num_pfns != 0)// 通过 ivq 队列将申请到的页面信息发送给 qemutell_host(vb, vb->inflate_vq);mutex_unlock(&vb->balloon_lock);
}
基本流程可以总结为:从 gust 空间申请页面放入 balloon 的链表中,并做标记使该内存内核不可用,填充设备的 pfn 数组,然后通过 ivq 通知设备侧进行处理。
2.4 leak_balloon 过程
static unsigned leak_balloon(struct virtio_balloon *vb, size_t num)
{unsigned num_freed_pages;struct page *page;struct balloon_dev_info *vb_dev_info = &vb->vb_dev_info;/* We can only do one array worth at a time. */num = min(num, ARRAY_SIZE(vb->pfns));mutex_lock(&vb->balloon_lock);/* We can't release more pages than taken */num = min(num, (size_t) vb->num_pages);for (vb->num_pfns = 0; vb->num_pfns < num;vb->num_pfns += VIRTIO_BALLOON_PAGES_PER_PAGE) {// 将申请到 balloon 的页面释放出来page = balloon_page_dequeue(vb_dev_info);if (!page)break;// 设置 pfn 数组set_page_pfns(vb, vb->pfns + vb->num_pfns, page);vb->num_pages -= VIRTIO_BALLOON_PAGES_PER_PAGE;}num_freed_pages = vb->num_pfns;/** Note that if* virtio_has_feature(vdev, VIRTIO_BALLOON_F_MUST_TELL_HOST);* is true, we *have* to do it in this order*/if (vb->num_pfns != 0)// 使用 dvq 通知 qemu 进行处理tell_host(vb, vb->deflate_vq);release_pages_balloon(vb);mutex_unlock(&vb->balloon_lock);return num_freed_pages;
}
leak_balloon
的过程和fill_balloon
刚好相反,它会释放存放在 balloon 的 page 链表中的 page 项归还给 gust,同理,这部分内存会被 qemu 从 host 申请回来留给 gustos 备用,此时 host 主机的可用内存就减少了。
fill_balloon
该函数用于增加 balloon 的大小,即从虚拟机的空闲内存中分配页面并通知 QEMU。主要步骤如下:
- 从虚拟机的空闲内存中分配页面,并将这些页面加入到 balloon 的链表中。
- 更新 virtqueue,将分配的页面信息发送给 QEMU。
- 更新 balloon 的实际大小,记录当前 balloon 所占用的内存页面数。
leak_balloon
该函数用于减少 balloon 的大小,即从 balloon 中释放页面并归还给虚拟机。主要步骤如下:
- 从 balloon 的链表中取出页面,并将其标记为可用。
- 更新 virtqueue,通知 QEMU 释放对应的页面。
- 更新 balloon 的实际大小,减少 balloon 所占用的内存页面数。
Virtio 简介(三)—— Virtio-balloon QEMU 设备创建
posted @ 2022-01-25 10:30 Edver
1. 概述
Virtio 设备分为前端设备(Guest 内部)、通信层和后端设备(QEMU)。本章从后端设备(以 QEMU 的 balloon 设备为例)的初始化开始分析。
从启动到 balloon 设备开始初始化基本调用流程如下:
balloon 代码执行流程如下:
2. 关键结构
2.1 Balloon 设备结构
typedef struct VirtIOBalloon {VirtIODevice parent_obj;VirtQueue *ivq, *dvq, *svq; // 3 个 virtqueueuint32_t num_pages; // 希望 guest 归还给 host 的内存页数uint32_t actual; // balloon 实际捕获的页数uint64_t stats[VIRTIO_BALLOON_S_NR]; // 状态统计信息VirtQueueElement *stats_vq_elem;size_t stats_vq_offset;QEMUTimer *stats_timer; // 定时器,用于定时查询功能int64_t stats_last_update;int64_t stats_poll_interval;uint32_t host_features; // 支持的特性uint64_t res_size; // 保留的内存大小
} VirtIOBalloon;
- num_pages:表示 balloon 中希望 guest 归还给 host 的内存页数。
- actual:表示 balloon 实际捕获的页数。
- stats:用于存储 balloon 的状态统计信息,如内存使用情况等。
- stats_timer:定时器,用于定时查询 balloon 的状态信息。
2.2 消息通讯结构 VirtQueue
struct VirtQueue
{VRing vring;/* Next head to pop */uint16_t last_avail_idx;/* Last avail_idx read from VQ. */uint16_t shadow_avail_idx;uint16_t used_idx;/* Last used index value we have signalled on */uint16_t signalled_used;/* Last used index value we have signalled on */bool signalled_used_valid;/* Notification enabled? */bool notification;uint16_t queue_index;//队列中正在处理的请求的数目unsigned int inuse;uint16_t vector;//回调函数VirtIOHandleOutput handle_output;VirtIOHandleAIOOutput handle_aio_output;VirtIODevice *vdev;EventNotifier guest_notifier;EventNotifier host_notifier;QLIST_ENTRY(VirtQueue) node;
};
- vring:存储 I/O 请求的环形缓冲区。
- last_avail_idx:记录上一次处理的可用索引位置。
- used_idx:记录已处理的 I/O 请求的索引位置。
- handle_output:处理 virtqueue 中数据的回调函数。
3. 初始化流程
3.1 设备类型注册
type_init(virtio_register_types) {type_register_static(&virtio_balloon_info);virtio_balloon_info.instance_init = virtio_balloon_instance_init;virtio_balloon_info.class_init = virtio_balloon_class_init;
}
3.2 类及实例初始化
// vl.c
qemu_opts_foreach(qemu_find_opts("device"), device_init_func, NULL, NULL);// qdev-monitor.c
qdev_device_add() {Object *obj = object_new();// 调用类初始化函数obj->class_init();// 调用实例初始化函数obj->instance_init();// 设置对象属性为已实现状态object_property_set_bool(obj, "realized", true);
}// virtio-balloon.c
virtio_balloon_device_realize(Object *obj) {virtio_init(obj);virtio_add_queue(obj);
}
3.3 Balloon 设备实例化
在 virtio_balloon_device_realize 函数中,主要完成以下操作:
- 初始化 virtio 设备:调用 virtio_init 函数,初始化 virtio 设备的公共部分,包括设置设备 ID、配置空间等。
- 创建 virtqueue:调用 virtio_add_queue 函数,创建三个 virtqueue(ivq、dvq 和 svq),并为每个 virtqueue 设置相应的回调函数,用于处理 virtqueue 中的数据。
3.3.1 virtio_init
void virtio_init(VirtIODevice *vdev, const char *name, uint16_t device_id, size_t config_size) {BusState *qbus = qdev_get_parent_bus(DEVICE(vdev));VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);int i;int nvectors = k->query_nvectors ? k->query_nvectors(qbus->parent) : 0;if (nvectors) {vdev->vector_queues = g_malloc0(sizeof(*vdev->vector_queues) * nvectors);}vdev->device_id = device_id;vdev->status = 0;atomic_set(&vdev->isr, 0); // 中断请求vdev->queue_sel = 0; // 配置队列时选择队列vdev->config_vector = VIRTIO_NO_VECTOR; // MSI 中断相关vdev->vq = g_malloc0(sizeof(VirtQueue) * VIRTIO_QUEUE_MAX);vdev->vm_running = runstate_is_running();vdev->broken = false;for (i = 0; i < VIRTIO_QUEUE_MAX; i++) {vdev->vq[i].vector = VIRTIO_NO_VECTOR;vdev->vq[i].vdev = vdev;vdev->vq[i].queue_index = i;}vdev->name = name;vdev->config_len = config_size;if (vdev->config_len) {vdev->config = g_malloc0(config_size);} else {vdev->config = NULL;}vdev->vmstate = qemu_add_vm_change_state_handler(virtio_vmstate_change, vdev);vdev->device_endian = virtio_default_endian();vdev->use_guest_notifier_mask = true;
}
- 设置中断:初始化中断请求(ISR)和 MSI 中断向量。
- 申请 virtqueue 空间:为 virtqueue 分配内存空间。
- 申请配置数据空间:为设备配置空间分配内存。
3.3.2 virtio_add_queue
VirtQueue *virtio_add_queue(VirtIODevice *vdev, int queue_size, VirtIOHandleOutput handle_output) {int i;for (i = 0; i < VIRTIO_QUEUE_MAX; i++) {if (vdev->vq[i].vring.num == 0) {break;}}if (i == VIRTIO_QUEUE_MAX || queue_size > VIRTQUEUE_MAX_SIZE) {abort();}vdev->vq[i].vring.num = queue_size;vdev->vq[i].vring.num_default = queue_size;vdev->vq[i].vring.align = VIRTIO_PCI_VRING_ALIGN;vdev->vq[i].handle_output = handle_output;vdev->vq[i].handle_aio_output = NULL;return &vdev->vq[i];
}
- 查找空闲队列:在 virtio 设备的队列数组中查找空闲位置。
- 设置队列大小:根据传入的队列大小参数,设置 virtqueue 的大小。
- 设置回调函数:为 virtqueue 设置处理数据的回调函数。
4. Balloon 处理
4.1 回调函数处理流程
在 virtio_balloon_device_realize 函数中,为每个 virtqueue 注册了回调函数。当 guest 侧通过 virtqueue 发送通知时,会调用这些回调函数来处理数据。
4.1.1 virtio_balloon_handle_output
static void virtio_balloon_handle_output(VirtIODevice *vdev, VirtQueue *vq) {VirtIOBalloon *s = VIRTIO_BALLOON(vdev);VirtQueueElement *elem;MemoryRegionSection section;for (;;) {size_t offset = 0;uint32_t pfn;elem = virtqueue_pop(vq, sizeof(VirtQueueElement));if (!elem) {return;}while (iov_to_buf(elem->out_sg, elem->out_num, offset, &pfn, 4) == 4) {ram_addr_t pa;ram_addr_t addr;int p = virtio_ldl_p(vdev, &pfn);offset += 4;pa = (ram_addr_t)p << VIRTIO_BALLOON_PFN_SHIFT;section = memory_region_find(get_system_memory(), pa, 1);if (!int128_nz(section.size) || !memory_region_is_ram(section.mr) || memory_region_is_rom(section.mr) || memory_region_is_romd(section.mr)) {trace_virtio_balloon_bad_addr(pa);memory_region_unref(section.mr);continue;}trace_virtio_balloon_handle_output(memory_region_name(section.mr), pa);addr = section.offset_within_region;balloon_page(memory_region_get_ram_ptr(section.mr) + addr, pa, !!(vq == s->dvq));memory_region_unref(section.mr);}virtqueue_push(vq, elem, offset);virtio_notify(vdev, vq);g_free(elem);}
}
- 从 virtqueue 中取出数据:调用 virtqueue_pop 函数,从 virtqueue 中取出数据到 VirtQueueElement 结构中。
- 地址转换:将取出的 GPA 地址转换为 HVA 地址。
- 处理页面:根据 HVA 地址和队列信息(dvq/ivq),调用balloon_page 函数处理对应的页面。
4.1.2 balloon_page
static void balloon_page(void *addr, ram_addr_t gpa, int deflate) {if (!qemu_balloon_is_inhibited() && (!kvm_enabled() || kvm_has_sync_mmu())) {
#ifdef _WIN32if (!hax_enabled() || !hax_ept_set_supported()) {return;}
#endifif (deflate || hax_invalid_ept_entries(gpa, BALLOON_PAGE_SIZE)) {return;}qemu_madvise(addr, BALLOON_PAGE_SIZE, deflate ? QEMU_MADV_WILLNEED : QEMU_MADV_DONTNEED);}
}
- 检查是否启用 balloon:确保 balloon 功能未被禁用。
- 处理 deflate 操作:如果为 deflate 操作,直接返回,因为 deflate 操作表示 guest 会再次使用对应的页面地址。
- 处理 inflate 操作:取消对应的 EPT 映射,并对 QEMU 侧的 HVA 地址使用 qemu_madvise 进行处理。
4.2 QEMU 处理虚拟内存
4.2.1 qemu_madvise
void qemu_madvise(void *addr, size_t len, int advice) {switch (advice) {case QEMU_MADV_WILLNEED:madvise_willneed(addr, len);break;case QEMU_MADV_DONTNEED:madvise_dontneed(addr, len);break;default:fprintf(stderr, "Unknown madvise advice: %d\n", advice);break;}
}
- 处理 willneed:表示 deflate 操作,guest 会再次使用对应的页面地址。
- 处理 dontneed:表示 inflate 操作,guest 不再使用对应的页面地址,需要释放内存。
Virtio 简介(四)—— 从零实现一个 virtio 设备
posted @ 2022-02-09 11:05 Edver
1. 功能
实现一个简单的 virtio 设备,功能如下:
- QEMU 模拟的设备启动一个定时器,每 5 秒发送一次中断通知 GUEST。
- GUEST 对应的驱动接收到中断后将自身变量自增,并通过 vring 发送给 QEMU。
- QEMU 收到 GUEST 发送过来的消息后打印出接收到的数值。
2. 设备创建
2.1 添加 virtio id
用于 guest 内部的设备和驱动匹配,需要和 Linux 内核中定义一致。
#define VIRTIO_ID_TEST 21 /* virtio test */
2.2 添加 device id
vendor-id 和 device-id 用于区分 PCI 设备,注意不要超过 0x104f。
#define PCI_DEVICE_ID_VIRTIO_TEST 0x1013
2.3 添加 virtio-test 设备配置空间定义的头文件
定义 GUEST 协商配置的 feature 和 config 结构体,需要与 Linux 中定义一致。
#ifndef _LINUX_VIRTIO_TEST_H
#define _LINUX_VIRTIO_TEST_H#include <linux/types.h>
#include <linux/virtio_types.h>
#include <linux/virtio_ids.h>
#include <linux/virtio_config.h>#define VIRTIO_TEST_F_CAN_PRINT 0struct virtio_test_config {uint32_t num_pages;uint32_t actual;uint32_t event;
};struct virtio_test_stat {__virtio16 tag;__virtio64 val;
} __attribute__((packed));#endif
2.4 添加 virtio-test 设备模拟代码
此代码包括对 vring 的操作和简介中的功能主体实现,与驱动交互的代码逻辑都在这里。
#include "qemu/osdep.h"
#include "qemu/log.h"
#include "qemu/iov.h"
#include "qemu/timer.h"
#include "qemu-common.h"
#include "hw/virtio/virtio.h"
#include "hw/virtio/virtio-test.h"
#include "sysemu/kvm.h"
#include "sysemu/hax.h"
#include "exec/address-spaces.h"
#include "qapi/error.h"
#include "qapi/qapi-events-misc.h"
#include "qapi/visitor.h"
#include "qemu/error-report.h"
#include "hw/virtio/virtio-bus.h"
#include "hw/virtio/virtio-access.h"
#include "migration/migration.h"static void virtio_test_handle_output(VirtIODevice *vdev, VirtQueue *vq) {VirtIOTest *s = VIRTIO_TEST(vdev);VirtQueueElement *elem;MemoryRegionSection section;for (;;) {size_t offset = 0;uint32_t pfn;elem = virtqueue_pop(vq, sizeof(VirtQueueElement));if (!elem) {return;}while (iov_to_buf(elem->out_sg, elem->out_num, offset, &pfn, 4) == 4) {int p = virtio_ldl_p(vdev, &pfn);offset += 4;qemu_log("=========get virtio num:%d\n", p);}virtqueue_push(vq, elem, offset);virtio_notify(vdev, vq);g_free(elem);}
}static void virtio_test_get_config(VirtIODevice *vdev, uint8_t *config_data) {VirtIOTest *dev = VIRTIO_TEST(vdev);struct virtio_test_config config;config.actual = cpu_to_le32(dev->actual);config.event = cpu_to_le32(dev->event);memcpy(config_data, &config, sizeof(struct virtio_test_config));
}static void virtio_test_set_config(VirtIODevice *vdev, const uint8_t *config_data) {VirtIOTest *dev = VIRTIO_TEST(vdev);struct virtio_test_config config;memcpy(&config, config_data, sizeof(struct virtio_test_config));dev->actual = le32_to_cpu(config.actual);dev->event = le32_to_cpu(config.event);
}static uint64_t virtio_test_get_features(VirtIODevice *vdev, uint64_t f, Error **errp) {VirtIOTest *dev = VIRTIO_TEST(vdev);f |= dev->host_features;virtio_add_feature(&f, VIRTIO_TEST_F_CAN_PRINT);return f;
}static int virtio_test_post_load_device(void *opaque, int version_id) {VirtIOTest *s = VIRTIO_TEST(opaque);return 0;
}static const VMStateDescription vmstate_virtio_test_device = {.name = "virtio-test-device",.version_id = 1,.minimum_version_id = 1,.post_load = virtio_test_post_load_device,.fields = (VMStateField[]) {VMSTATE_UINT32(actual, VirtIOTest),VMSTATE_END_OF_LIST(),},
};static void test_stats_change_timer(VirtIOTest *s, int64_t secs) {timer_mod(s->stats_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + secs * 1000);
}static void test_stats_poll_cb(void *opaque) {VirtIOTest *s = opaque;VirtIODevice *vdev = VIRTIO_DEVICE(s);qemu_log("==============set config:%d\n", s->set_config++);virtio_notify_config(vdev);test_stats_change_timer(s, 1);
}static void virtio_test_device_realize(DeviceState *dev, Error **errp) {VirtIODevice *vdev = VIRTIO_DEVICE(dev);VirtIOTest *s = VIRTIO_TEST(dev);int ret;virtio_init(vdev, "virtio-test", VIRTIO_ID_TEST, sizeof(struct virtio_test_config));s->ivq = virtio_add_queue(vdev, 128, virtio_test_handle_output);g_assert(s->stats_timer == NULL);s->stats_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL, test_stats_poll_cb, s);test_stats_change_timer(s, 30);
}static void virtio_test_device_unrealize(DeviceState *dev, Error **errp) {VirtIODevice *vdev = VIRTIO_DEVICE(dev);VirtIOTest *s = VIRTIO_TEST(dev);virtio_cleanup(vdev);
}static void virtio_test_device_reset(VirtIODevice *vdev) {VirtIOTest *s = VIRTIO_TEST(vdev);
}static void virtio_test_set_status(VirtIODevice *vdev, uint8_t status) {VirtIOTest *s = VIRTIO_TEST(vdev);
}static void virtio_test_instance_init(Object *obj) {VirtIOTest *s = VIRTIO_TEST(obj);
}static const VMStateDescription vmstate_virtio_test = {.name = "virtio-test",.minimum_version_id = 1,.version_id = 1,.fields = (VMStateField[]) {VMSTATE_VIRTIO_DEVICE,VMSTATE_END_OF_LIST(),},
};static Property virtio_test_properties[] = {DEFINE_PROP_END_OF_LIST(),
};static void virtio_test_class_init(ObjectClass *klass, void *data) {DeviceClass *dc = DEVICE_CLASS(klass);VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);dc->props = virtio_test_properties;dc->vmsd = &vmstate_virtio_test;set_bit(DEVICE_CATEGORY_MISC, dc->categories);vdc->realize = virtio_test_device_realize;vdc->unrealize = virtio_test_device_unrealize;vdc->reset = virtio_test_device_reset;vdc->get_config = virtio_test_get_config;vdc->set_config = virtio_test_set_config;vdc->get_features = virtio_test_get_features;vdc->set_status = virtio_test_set_status;vdc->vmsd = &vmstate_virtio_test_device;
}static const TypeInfo virtio_test_info = {.name = TYPE_VIRTIO_TEST,.parent = TYPE_VIRTIO_DEVICE,.instance_size = sizeof(VirtIOTest),.instance_init = virtio_test_instance_init,.class_init = virtio_test_class_init,
};static void virtio_register_types(void) {type_register_static(&virtio_test_info);
}type_init(virtio_register_types)
2.5 添加 virtio-test-pci 设备的实现
virtio-test 设备属于 virtio 设备,挂接在 virtio 总线上,但 virtio 属于 PCI 设备。因此,将 virtio-test 设备包含于 virtio-test-pci 中,提供给外层的感知是这是一个 PCI 设备,遵循 PCI 协议的规范。
#include "hw/virtio/virtio-gpu.h"
#include "hw/virtio/virtio-crypto.h"
#include "hw/virtio/vhost-user-scsi.h"
#include "hw/virtio/virtio-test.h"#if defined(CONFIG_VHOST_USER) && defined(CONFIG_LINUX)
#include "hw/virtio/vhost-user-blk.h"
#endiftypedef struct VirtIOTestPCI {VirtIOPCIProxy parent_obj;VirtIOTest vdev;
} VirtIOTestPCI;#define TYPE_VIRTIO_TEST_PCI "virtio-test-pci"
#define VIRTIO_TEST_PCI(obj) \OBJECT_CHECK(VirtIOTestPCI, (obj), TYPE_VIRTIO_TEST_PCI)static Property virtio_test_pci_properties[] = {DEFINE_PROP_UINT32("class", VirtIOPCIProxy, class_code, 0),DEFINE_PROP_END_OF_LIST(),
};static void virtio_test_pci_realize(VirtIOPCIProxy *vpci_dev, Error **errp) {VirtIOTestPCI *dev = VIRTIO_TEST_PCI(vpci_dev);DeviceState *vdev = DEVICE(&dev->vdev);if (vpci_dev->class_code != PCI_CLASS_OTHERS && vpci_dev->class_code != PCI_CLASS_MEMORY_RAM) {vpci_dev->class_code = PCI_CLASS_OTHERS;}qdev_set_parent_bus(vdev, BUS(&vpci_dev->bus));object_property_set_bool(OBJECT(vdev), true, "realized", errp);
}static void virtio_test_pci_class_init(ObjectClass *klass, void *data) {DeviceClass *dc = DEVICE_CLASS(klass);VirtioPCIClass *k = VIRTIO_PCI_CLASS(klass);PCIDeviceClass *pcidev_k = PCI_DEVICE_CLASS(klass);k->realize = virtio_test_pci_realize;set_bit(DEVICE_CATEGORY_MISC, dc->categories);dc->props = virtio_test_pci_properties;pcidev_k->vendor_id = PCI_VENDOR_ID_REDHAT_QUMRANET;pcidev_k->device_id = PCI_DEVICE_ID_VIRTIO_TEST;pcidev_k->revision = VIRTIO_PCI_ABI_VERSION;pcidev_k->class_id = PCI_CLASS_OTHERS;
}static void virtio_test_pci_instance_init(Object *obj) {VirtIOTestPCI *dev = VIRTIO_TEST_PCI(obj);virtio_instance_init_common(obj, &dev->vdev, sizeof(dev->vdev), TYPE_VIRTIO_TEST);
}static const TypeInfo virtio_test_pci_info = {.name = TYPE_VIRTIO_TEST_PCI,.parent = TYPE_VIRTIO_PCI,.instance_size = sizeof(VirtIOTestPCI),.instance_init = virtio_test_pci_instance_init,.class_init = virtio_test_pci_class_init,
};static void virtio_pci_register_types(void) {type_register_static(&virtio_test_pci_info);
}type_init(virtio_pci_register_types)
2.6 使设备生效
- 将 virtio-test.c 文件加入编译工程,并确保设置 include 目录(-I)时未遗漏相关路径。
- 编译生成可执行文件。
- 启动时加入对应参数:
-qemu -device virtio-test-pci
。 - 在 HMP 界面输入
info qtree
,可看到设备已创建。
Virtio 简介(五)—— Virtio_blk 设备分析
posted @ 2022-05-10 20:38 Edver
1. 创建过程关键函数
1.1 virtblk_probe
虚拟机启动时,virtio bus 上检测到 virtio 块设备后,调用 probe 函数插入该设备。初始化设备的散列表,从简介(一)的流程图可知,系统的 I/O 请求会先映射到散列表中。
static int virtblk_probe(struct virtio_device *vdev) {struct virtio_blk *vblk;int err;if (!vdev->config->get) {return -EINVAL;}vdev->priv = vblk = kmalloc(sizeof(*vblk), GFP_KERNEL);if (!vblk) {err = -ENOMEM;goto out;}vblk->vdev = vdev;vblk->sg = kmalloc_array(VIRTIO_BLK_SG_SEGMENTS, sizeof(struct scatterlist), GFP_KERNEL);if (!vblk->sg) {err = -ENOMEM;goto out_free_vblk;}err = virtio_find_single_vq(vdev, &vblk->vq, blk_done);if (err)goto out_free_sg;virtio_device_ready(vdev);return 0;out_free_sg:kfree(vblk->sg);
out_free_vblk:kfree(vblk);
out:return err;
}
- 初始化设备结构体:分配并初始化 virtio_blk 结构体。
- 创建 vring_virtqueue:调用 virtio_find_single_vq 函数,为 virtio 块设备生成一个 vring_virtqueue。
- 注册回调函数:为 virtqueue 注册回调函数 blk_done,用于处理 I/O 请求完成后的回调。
1.2 vring_virtqueue
每个 virtio 设备都有一个 virtqueue 接口,提供对 vring 的操作函数,如 add_buf、get_buf 和 kick 等。vring_virtqueue 是 virtqueue 和 vring 的管理结构。
struct vring_virtqueue {struct virtqueue vq;struct vring vring;// 其他成员...
};
- vring:存储 I/O 请求的环形缓冲区。
- num_free:表示 vring_desc 表中还有多少项是空闲可用的。
- free_head:表示在 vring_desc 中第一个空闲表项的位置。
- num_added:表示在通知对端进行读写时,与上次通知相比,添加了多少个新的 I/O 请求到 vring_desc 中。
- last_used_idx:表示 vring_used 表中的 idx 上次 I/O 操作之后被更新到哪个位置。
1.3 setup_vq
static int setup_vq(struct virtio_device *vdev, unsigned int index, void **queue, void (*callback)(struct virtqueue *), void *priv, const char *name) {struct virtio_pci_device *vp_dev = to_vp_device(vdev);struct virtqueue *vq;int err;err = virtio_find_vqs(vdev, 1, &vq, callback, name, NULL);if (err)return err;*queue = vq;vq->priv = priv;return 0;
}
- 查找空闲队列:调用 virtio_find_vqs 函数,查找空闲的 virtqueue。
- 设置回调函数和私有数据:为 virtqueue 设置回调函数和私有数据。
1.4 vring_new_virtqueue
struct virtqueue *vring_new_virtqueue(unsigned int index, unsigned int num, unsigned int vring_align, struct virtio_device *vdev, bool (*notify)(struct virtqueue *), void (*callback)(struct virtqueue *), void *priv) {struct vring_virtqueue *vvq;int err;vvq = kzalloc(sizeof(*vvq), GFP_KERNEL);if (!vvq)return NULL;vvq->vq.callback = callback;vvq->vq.vdev = vdev;vvq->vq.index = index;vvq->vq.priv = priv;vvq->vring.num = num;vvq->vring.align = vring_align;err = vring_alloc_queue(vvq);if (err)goto err_free_vvq;return &vvq->vq;err_free_vvq:kfree(vvq);return NULL;
}
- 分配 vring_virtqueue:为 vring_virtqueue 分配内存。
- 初始化 vring:设置 vring 的大小和对齐方式。
- 分配队列空间:调用 vring_alloc_queue 函数,为 vring 分配空间。
2. QEMU 获取 VRING 地址
2.1 virtio_ioport_write
static void virtio_ioport_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) {VirtIODevice *vdev = opaque;struct virtio_pci_device *vp_dev = to_vp_device(vdev);switch (addr) {case VIRTIO_PCI_QUEUE_PFN:virtio_queue_set_addr(vp_dev, val);break;default:fprintf(stderr, "Unknown ioport write: 0x%" PRIx64 "\n", val);break;}
}
- 处理队列地址设置:当 guest 写入 VIRTIO_PCI_QUEUE_PFN 寄存器时,调用 virtio_queue_set_addr 函数设置队列地址。
2.2 virtio_queue_set_addr
static void virtio_queue_set_addr(struct virtio_pci_device *vp_dev, uint64_t val) {vp_dev->queue_pfn = val;
}
- 设置队列地址:将 guest 写入的物理页号保存到 vp_dev->queue_pfn 中。
2.3 virtqueue_init
static void virtqueue_init(struct virtqueue *vq) {struct vring_virtqueue *vvq = to_vvq(vq);vvq->vring.avail->idx = 0;vvq->vring.used->idx = 0;
}
- 初始化 vring 的 avail 和 used 表:将 avail 和 used 表的索引设置为 0。
3. 完整的读写流程
3.1 前端写请求(Guest kernel)
3.1.1 do_virtblk_request
static void do_virtblk_request(struct request_queue *q) {struct virtio_blk *vblk = q->queuedata;struct request *req;int issued = 0;while ((req = blk_fetch_request(q))) {do_req(vblk, req);issued++;}if (issued)virtqueue_kick(vblk->vq);
}
- 处理 I/O 请求队列:从请求队列中取出请求,调用 do_req 函数处理每个请求。
- 通知后端:如果处理了请求,则调用 virtqueue_kick 函数通知后端。
3.1.2 do_req
static void do_req(struct virtio_blk *vblk, struct request *req) {struct scatterlist sg[2];struct virtio_blk_iod *iod;int n, num_out = 0, num_in = 0;iod = kzalloc(sizeof(*iod), GFP_ATOMIC);if (!iod)return;iod->req = req;iod->vblk = vblk;// 设置 I/O 请求的扇区信息sg_init_one(&sg[0], &iod->sector, sizeof(iod->sector));num_out = 1;// 将请求的数据地址存入散列表n = blk_rq_map_sg(req->q, req, &sg[1]);if (n) {num_in = n;}// 将本次请求的状态等额外信息存入散列表末尾sg_init_one(&sg[n + 1], &iod->status, sizeof(iod->status));num_in++;// 将散列表中的请求地址存入 vring 数据结构virtqueue_add_buf_gfp(vblk->vq, sg, num_out, num_in, iod, GFP_ATOMIC);// 通知后端virtqueue_kick(vblk->vq);
}
- 获取 I/O 请求的扇区信息:将请求的扇区信息存入 virtio device 的散列表中。
- 将请求的数据地址存入散列表:调用 blk_rq_map_sg 函数,将请求的数据地址存入散列表。
- 将本次请求的状态等额外信息存入散列表末尾:将请求的状态信息存入散列表。
- 将散列表中的请求地址存入 vring 数据结构:调用 virtqueue_add_buf 函数,将散列表中的请求地址存入 vring 数据结构。
- 通知后端:调用 virtqueue_kick 函数,通知后端 QEMU。
3.1.3 blk_rq_map_sg
int blk_rq_map_sg(struct request_queue *q, struct request *rq, struct scatterlist *sglist) {struct bio_vec *bvec;struct scatterlist *sg;int n = 0;rq_for_each_segment(bvec, rq) {sg = &sglist[n];sg_set_page(sg, bvec->bv_page, bvec->bv_len, bvec->bv_offset);n++;}return n;
}
- 遍历请求的 bio_vec:从请求中依次取出 bio_vec,并保存在 bvec 变量中。
- 将 bio_vec 的数据存入散列表:调用 sg_set_page 函数,将 bio_vec 的数据存入散列表。
3.1.4 virtqueue_add_buf_gfp
void virtqueue_add_buf_gfp(struct virtqueue *vq, struct scatterlist *sgs[], unsigned int num_out, unsigned int num_in, void *data, gfp_t gfp) {struct vring_virtqueue *vvq = to_vvq(vq);unsigned int i, j;spin_lock(&vvq->vq_lock);// 检查是否有足够的空闲空间if (vvq->free_head == -1) {spin_unlock(&vvq->vq_lock);return;}// 将散列表中的请求地址存入 vring 数据结构for (i = 0; i < num_out + num_in; i++) {struct scatterlist *sg = sgs[i];unsigned int len = sg->length;unsigned int offset = sg->offset;vvq->vring.desc[vvq->free_head].addr = sg_phys(sg);vvq->vring.desc[vvq->free_head].len = len;vvq->vring.desc[vvq->free_head].flags = VRING_DESC_F_NEXT;vvq->free_head = vvq->vring.desc[vvq->free_head].next;}// 更新 vring_avail 表vvq->vring.avail->ring[vvq->vring.avail->idx % vvq->vring.num] = vvq->vring.desc[0].addr;vvq->vring.avail->idx++;spin_unlock(&vvq->vq_lock);
}
- 检查是否有足够的空闲空间:如果 vring_desc 表中没有足够的空闲空间,则返回。
- 将散列表中的请求地址存入 vring 数据结构:将散列表中的请求地址存入 vring_desc 表中。
- 更新 vring_avail 表:将 vring_desc 表中的请求地址存入 vring_avail 表中。
3.1.5 virtqueue_kick
void virtqueue_kick(struct virtqueue *vq) {struct vring_virtqueue *vvq = to_vvq(vq);spin_lock(&vvq->vq_lock);// 更新 vring.avail->idxvvq->vring.avail->idx = vvq->vring.avail->idx;// 通知后端if (vvq->vq.notify) {vvq->vq.notify(&vvq->vq);}spin_unlock(&vvq->vq_lock);
}
- 更新 vring.avail->idx:更新 vring_avail 表的索引。
- 通知后端:调用 notify 函数,通知后端 QEMU。
3.2 QEMU 端写请求代码流程(QEMU 代码)
3.2.1 virtio_ioport_write
static void virtio_ioport_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) {VirtIODevice *vdev = opaque;struct virtio_pci_device *vp_dev = to_vp_device(vdev);switch (addr) {case VIRTIO_PCI_QUEUE_PFN:virtio_queue_set_addr(vp_dev, val);break;default:fprintf(stderr, "Unknown ioport write: 0x%" PRIx64 "\n", val);break;}
}
- 处理队列地址设置:当 guest 写入 VIRTIO_PCI_QUEUE_PFN 寄存器时,调用 virtio_queue_set_addr 函数设置队列地址。
3.2.2 virtio_blk_handle_output
static void virtio_blk_handle_output(VirtIODevice *vdev, VirtQueue *vq) {VirtIOBlock *s = VIRTIO_BLOCK(vdev);VirtQueueElement *elem;MemoryRegionSection section;while ((elem = virtqueue_pop(vq, sizeof(VirtQueueElement)))) {struct virtio_blk_req *req = elem->opaque;uint64_t sector = le64_to_cpu(req->sector);uint32_t type = le32_to_cpu(req->type);// 处理读写请求if (type & VIRTIO_BLK_T_OUT) {// 写操作qemu_iovec_init_external(&req->qiov, req->out_sg, req->out_num);bdrv_aio_writev(s->bs, sector, &req->qiov, req->num_sectors, blk_request_done, req);} else {// 读操作qemu_iovec_init_external(&req->qiov, req->in_sg, req->in_num);bdrv_aio_readv(s->bs, sector, &req->qiov, req->num_sectors, blk_request_done, req);}virtqueue_push(vq, elem, req->out_num * sizeof(req->out_sg[0]));virtio_notify(vdev, vq);}
}
- 从 virtqueue 中取出数据:调用 virtqueue_pop 函数,从 virtqueue 中取出数据。
- 处理读写请求:根据请求类型,调用 bdrv_aio_writev 或 bdrv_aio_readv 函数处理读写请求。
- 通知前端:调用 virtqueue_push 和 virtio_notify 函数,通知前端请求已完成。
3.2.3 virtqueue_pop
VirtQueueElement *virtqueue_pop(VirtQueue *vq, size_t sz) {struct vring_virtqueue *vvq = to_vvq(vq);unsigned int head;VirtQueueElement *elem;spin_lock(&vvq->vq_lock);if (vvq->vring.used->idx == vvq->last_used_idx) {spin_unlock(&vvq->vq_lock);return NULL;}head = vvq->vring.used->ring[vvq->last_used_idx % vvq->vring.num].id;vvq->last_used_idx++;elem = kzalloc(sz, GFP_ATOMIC);if (!elem) {spin_unlock(&vvq->vq_lock);return NULL;}// 从 vring_desc 表中取出数据for (unsigned int i = head; i != vvq->free_head; i = vvq->vring.desc[i].next) {struct scatterlist *sg = &elem->sg[i - head];sg->length = vvq->vring.desc[i].len;sg->offset = vvq->vring.desc[i].addr;}spin_unlock(&vvq->vq_lock);return elem;
}
- 检查是否有可用的数据:如果 vring_used 表中的 idx 等于 last_used_idx,则表示没有可用的数据。
- 从 vring_desc 表中取出数据:根据 vring_used 表中的索引,从 vring_desc 表中取出数据。
- 返回 VirtQueueElement:将取出的数据存入 VirtQueueElement 结构中并返回。
3.2.4 virtio_blk_handle_request
static void virtio_blk_handle_request(VirtIOBlockReq *req) {struct virtio_blk *vblk = req->vblk;struct virtio_blk_req *vbr = &req->req;uint64_t sector = le64_to_cpu(vbr->sector);uint32_t type = le32_to_cpu(vbr->type);if (type & VIRTIO_BLK_T_OUT) {// 写操作qemu_iovec_init_external(&req->qiov, req->out_sg, req->out_num);bdrv_aio_writev(vblk->bs, sector, &req->qiov, req->num_sectors, blk_request_done, req);} else {// 读操作qemu_iovec_init_external(&req->qiov, req->in_sg, req->in_num);bdrv_aio_readv(vblk->bs, sector, &req->qiov, req->num_sectors, blk_request_done, req);}
}
- 处理读写请求:根据请求类型,调用 bdrv_aio_writev 或 bdrv_aio_readv 函数处理读写请求。
3.2.5 virtqueue_push
void virtqueue_push(VirtQueue *vq, VirtQueueElement *elem, unsigned int len) {struct vring_virtqueue *vvq = to_vvq(vq);spin_lock(&vvq->vq_lock);// 更新 vring_used 表vvq->vring.used->ring[vvq->last_used_idx % vvq->vring.num].id = vvq->vring.desc[0].addr;vvq->vring.used->ring[vvq->last_used_idx % vvq->vring.num].len = len;vvq->last_used_idx++;spin_unlock(&vvq->vq_lock);
}
- 更新 vring_used 表:将处理完成的请求存入 vring_used 表中。
3.2.6 virtio_notify
void virtio_notify(VirtIODevice *vdev, VirtQueue *vq) {struct virtio_pci_device *vp_dev = to_vp_device(vdev);if (vp_dev->notify) {vp_dev->notify(vp_dev, vq);}
}
- 通知前端:调用 notify 函数,通知前端请求已完成。
3.3 前端回调函数后续处理 (内核代码)
3.3.1 blk_done
static void blk_done(struct virtqueue *vq) {struct virtio_blk *vblk = vq->vdev->priv;struct virtio_blk_iod *iod;unsigned int len;while ((iod = virtqueue_get_buf(vq, &len))) {struct request *req = iod->req;if (iod->status) {printk(KERN_ERR "virtio_blk: I/O error\n");req->errors = 1;}blk_end_request_all(req);kfree(iod);}
}
- 处理完成的 I/O 请求:调用 virtqueue_get_buf 函数,获取处理完成的 I/O 请求。
- 通知系统:根据请求的状态,调用 blk_end_request_all 函数通知系统请求已完成。
3.3.2 virtqueue_get_buf
void *virtqueue_get_buf(struct virtqueue *vq, unsigned int *len) {struct vring_virtqueue *vvq = to_vvq(vq);unsigned int head;void *data;spin_lock(&vvq->vq_lock);if (vvq->last_used_idx == vvq->vring.used->idx) {spin_unlock(&vvq->vq_lock);return NULL;}head = vvq->vring.used->ring[vvq->last_used_idx % vvq->vring.num].id;vvq->last_used_idx++;data = vvq->vring.desc[head].addr;spin_unlock(&vvq->vq_lock);return data;
}
- 检查是否有可用的数据:如果 vring_used 表中的 idx 等于 last_used_idx,则表示没有可用的数据。
- 从 vring_desc 表中取出数据:根据 vring_used 表中的索引,从 vring_desc 表中取出数据。
- 返回数据:返回处理完成的 I/O 请求。
3.3.3 detach_buf
static void detach_buf(struct vring_virtqueue *vvq, unsigned int head) {unsigned int i = head;do {vvq->vring.desc[i].next = vvq->free_head;vvq->free_head = i;i = vvq->vring.desc[i].next;} while (i != head);vvq->num_free += vvq->vring.num;
}
- 将 vring_desc 表中的请求链表添加到空闲链表中:将处理完成的请求链表添加到空闲链表中。
- 更新空闲链表的头指针:更新空闲链表的头指针。
- 更新空闲数量:更新空闲链表的数量。
Virtio 简介(六)—— Virtio_net 设备分析
posted @ 2022-05-11 11:07 Edver
1. Virtio_net 设备创建流程
via:
- virtio 简介(一)—— 框架分析 - Edver - 博客园
https://www.cnblogs.com/edver/p/14684104.html - virtio 简介(二) —— virtio-balloon guest 侧驱动 - Edver - 博客园
https://www.cnblogs.com/edver/p/14684138.html - virtio 简介(三) —— virtio-balloon qemu 设备创建 - Edver - 博客园
https://www.cnblogs.com/edver/p/14684117.html - virtio 简介(四)—— 从零实现一个 virtio 设备 - Edver - 博客园
https://www.cnblogs.com/edver/p/15874178.html - virtio 简介(五)—— virtio_blk 设备分析 - Edver - 博客园
https://www.cnblogs.com/edver/p/16255243.html - virtio 简介(六)—— virtio_net 设备分析 - Edver - 博客园
https://www.cnblogs.com/edver/p/16257120.html