Virtio 技术解析 | 框架、设备实现与实践指南

本文为 “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 中读取数据和放入数据。

img

3. 原理

3.1 整体流程

从代码层面来看,virtio 的代码主要分为 QEMU 和内核驱动程序两部分。Virtio 设备的模拟由 QEMU 完成,QEMU 在虚拟机启动之前创建虚拟设备。虚拟机启动后检测到设备,调用内核的 virtio 设备驱动程序来加载该 virtio 设备。

对于 KVM 虚拟机,其均通过 QEMU 这一用户空间程序创建,每个 KVM 虚拟机对应一个 QEMU 进程,虚拟机的 virtio 设备由 QEMU 进程模拟,虚拟机的内存也从 QEMU 进程的地址空间内分配。

VRING 是由虚拟机 virtio 设备驱动创建的用于数据传输的共享内存,QEMU 进程通过这块共享内存获取前端设备递交的 I/O 请求。整个虚拟机 I/O 请求的流程如下图所示:

img

  1. 虚拟机产生的 I/O 请求被前端的 virtio 设备接收,并存放在 virtio 设备散列表 scatterlist 中;
  2. Virtio 设备的 virtqueue 提供 add_buf 将散列表中的数据映射至前后端数据共享区域 Vring 中;
  3. Virtqueue 通过 kick 函数通知后端 qemu 进程。Kick 通过写 pci 配置空间的寄存器产生 kvm_exit;
  4. Qemu 端注册 ioport_write/read 函数监听 PCI 配置空间的改变,获取前端的通知消息;
  5. Qemu 端维护的 virtqueue 队列从数据共享区 vring 中获取数据;
  6. Qemu 将数据封装成 virtioreq;
  7. 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 来访问该寄存器空间。配置空间如下图所示:

img

虚拟机系统在启动过程中在 PCI 总线上发现 virtio-pci 设备后,会调用 virtio-pci 的 probe 函数。该函数将 PCI 配置空间上的寄存器映射到内存空间,并将该地址赋值给 virtio_pci_device 的 ioaddr 变量。之后,对 PCI 配置空间上的寄存器操作时,只需使用 ioaddr 加偏移量即可。

img

pci_iomap 函数完成 PCI BAR 的映射,第一个参数是 pci 设备的指针,第二个参数指定要映射的是 0 号 BAR,第三个参数确定要映射的 BAR 空间大小。当第三个参数为 0 时,将整个 BAR 空间均映射到内存空间上。VirtioPCI 设备的 0 号 BAR 指向的就是配置空间的寄存器空间,即配置空间上用于消息通知的寄存器。

通过 pci_iomap 之后,即可像操作普通内存一样(调用 ioread 和 iowrite)来读写 pci 硬件设备上的寄存器。

3.2.2 虚拟机如何操作该配置空间
  1. kick

当前端设备的驱动程序需通知后端 QEMU 程序执行某些操作时,会调用 kick 函数,触发读写 PCI 配置空间寄存器的动作。

  1. 读写 PCI 寄存器

ioread/iowrite 实现了对配置空间寄存器的读写,例如:

img

vp_dev->ioaddr + VIRTIO_PCI_QUEUE_NOTIFY 表示写 notify 这个寄存器,位置如图 2-1 所示。

img

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 模式,若支持,回调函数会有所不同。后续有时间会补充相关内容。

  1. 监听 PCI 寄存器

virtio_ioport_write/read 即为 QEMU 进程监听 PCI 配置空间上寄存器消息的函数,针对前端 iowrite/ioread 读写了哪个 PCI 寄存器,来决定下一步操作:

img

  1. 监听函数的注册

PCI 寄存器的这些监听函数均在 QEMU 为虚拟机创建虚拟设备时注册。

QEMU 先为虚拟机的 virtio-pci 设备创建 PCI 配置空间,配置空间内包含设备的一些基本信息;在配置空间的存储空间位置注册了一个 PCI BAR,并为该 BAR 注册了回调函数以监听寄存器的改变。

以下代码为初始化配置空间的基本信息:

img

为 PCI 设备注册 PCI BAR,指定起始地址为 PCI_BASE_ADDRESS_SPACE_IO(即 PCI 配置空间中存储空间到配置空间首地址的偏移值);指定该 BAR 的大小为 size,回调函数为 virtio_pci_config_ops 中的读写函数。

img

这里的 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 表中哪些项已被递交,可释放这些项。

  1. 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 域组成了链表:

img

  1. 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 号位置开始。

img

  1. 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 表中的相应项。

img

3.3.2 对 Vring 进行操作

Vring 的操作分为两部分:在前端虚拟机内,通过 virtqueue_add_buf 将 I/O 请求的内存地址存入 vring_desc 表中,同时更新 vring_avail 表;在后端 QEMU 进程内,根据 vring_avail 表的内容,通过 virtqueue_get_buf 从 vring_desc 表中取出数据,同时更新 vring_used 表。

  1. 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 函数通知后端来读取数据。

  1. 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 请求已结束。

img

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 执行流程如下:
img

2. 驱动创建

2.1 驱动注册

在 Linux 设备驱动模型中,各驱动可按总线类别进行划分,每个总线类别下可挂载“驱动”和“设备”两类对象。内核维护了一张“总线”到“驱动和设备”的映射表。当一个新驱动加入内核时,内核会扫描该驱动所挂载总线上的所有设备,并通过比对驱动中的 id_table 字段和设备配置空间中的 Device ID 来判断是否匹配。若匹配,则内核会针对该设备调用总线的 probe 函数(若总线无 probe 函数,则调用驱动的 probe 函数)。

Linux 内核中前端代码及 virtio-balloon-pci 设备初始化过程

在 Linux 内核中,前端代码主要包含 driver/virtio 目录下的相关文件以及 driver/virtio_balloon.c。最终生成的内核模块有 virtio.kovirtio_ring.kovirtio_pci.kovirtio_balloon.ko

在 Linux 内核设备驱动体系中,virtio-balloon-pci 设备隶属于 virtio-pci 设备类别,而 virtio-pci 设备本质上属于 PCI 设备家族。基于此层级关系,virtio-pci 设备驱动遵循 PCI 设备驱动模型,将其注册至 PCI 总线。以下详述该设备驱动的完整初始化流程:

  1. 内核模块加载与总线注册阶段

    • 内核依据驱动模块的依赖关系,优先定位并加载 virito-pci.ko 驱动模块。在此过程中,系统顺序加载 virtio.kovirtio-ring.ko 以及 virtio_pci.ko 模块。其中,virtio_pci.ko 模块依赖于前两者提供的基础功能。
    • virtio.ko 模块执行初始化函数时,会在系统中创建并注册名为 virtio 的新总线类型,为后续相关设备的接入提供统一管理机制。同时,virtio_pci.ko 模块的初始化函数触发已注册的 virtio_pci_probe 函数执行,为设备注册做准备。
  2. 设备注册阶段

    • virtio_pci_probe 函数通过调用 register_virtio_device 函数,在 virtio 总线架构下完成一个新的 virtio 设备实例的注册操作,构建起设备与总线的关联关系。
  3. 次级驱动搜索与加载阶段

    • 内核在完成上述设备注册后,依据设备属性与驱动匹配规则,再次执行驱动模块的搜索流程。最终定位并加载 virtio_balloon.ko 模块,随即调用其初始化函数。
  4. 总线驱动注册与优先级调度阶段

    • virtio_balloon 模块的初始化函数将 virtio_balloon 驱动注册至 virtio 总线,并触发总线的 probe 函数(即 virtio_dev_probe)执行。需注意,在 Linux 设备驱动模型中,总线的 probe 函数具备高于总线上设备 probe 函数的执行优先级,以确保总线层面的配置与设备初始化的有序性。
  5. 最终初始化完成阶段

    • virtio_dev_probe 函数通过调用 virtballoon_probe 函数,完成设备与驱动的深度绑定、资源分配及功能配置等核心初始化任务,至此 virtio-balloon-pci 设备驱动初始化流程全部完成 。

从 linux 设备初始化开始到调用 virtballoon_probe 的过程简化如下,仅供参考:

img

驱动可执行的动作包含在 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 函数中,主要完成以下工作:

  1. 初始化工作队列:通过 init_waitqueue_head 初始化两个工作队列,用于接收 QEMU 发来的通知。
  2. 初始化 virtqueue:调用 virtio_find_vqs 函数,根据设备支持的特性初始化三个 virtqueue(ivq、dvq 和 svq),分别用于处理不同的任务。
  3. 启动内核线程:创建并启动一个内核线程,用于执行 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;
}

主要涉及到的处理:

  1. 添加等待队列,等待 config_change 被唤醒,即 QEMU 有执行 balloon 操作。
  2. 计算需要申请或者释放的空间,即 diff 值。
  3. 如果需要申请或者释放空间,则调用 fill_balloon 或者 leak_balloon 进行操作。
  4. 更新 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 的内核线程中,主要逻辑如下:

  1. 监听通知:将当前线程加入到工作队列中,等待 QEMU 通过 virtio 配置空间发送的通知。
  2. 处理通知:当收到通知后,根据通知内容执行相应的操作,如更新 balloon 的目标大小或处理统计信息。
  3. 内存充气和放气:根据 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。主要步骤如下:

  1. 从虚拟机的空闲内存中分配页面,并将这些页面加入到 balloon 的链表中。
  2. 更新 virtqueue,将分配的页面信息发送给 QEMU。
  3. 更新 balloon 的实际大小,记录当前 balloon 所占用的内存页面数。

 
leak_balloon
 
该函数用于减少 balloon 的大小,即从 balloon 中释放页面并归还给虚拟机。主要步骤如下:

  1. 从 balloon 的链表中取出页面,并将其标记为可用。
  2. 更新 virtqueue,通知 QEMU 释放对应的页面。
  3. 更新 balloon 的实际大小,减少 balloon 所占用的内存页面数。

Virtio 简介(三)—— Virtio-balloon QEMU 设备创建

posted @ 2022-01-25 10:30 Edver

1. 概述

Virtio 设备分为前端设备(Guest 内部)、通信层和后端设备(QEMU)。本章从后端设备(以 QEMU 的 balloon 设备为例)的初始化开始分析。

从启动到 balloon 设备开始初始化基本调用流程如下:
img
balloon 代码执行流程如下:

img

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 函数中,主要完成以下操作:

  1. 初始化 virtio 设备:调用 virtio_init 函数,初始化 virtio 设备的公共部分,包括设置设备 ID、配置空间等。
  2. 创建 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 设备,功能如下:

  1. QEMU 模拟的设备启动一个定时器,每 5 秒发送一次中断通知 GUEST。
  2. GUEST 对应的驱动接收到中断后将自身变量自增,并通过 vring 发送给 QEMU。
  3. 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 使设备生效

  1. 将 virtio-test.c 文件加入编译工程,并确保设置 include 目录(-I)时未遗漏相关路径。
  2. 编译生成可执行文件。
  3. 启动时加入对应参数:-qemu -device virtio-test-pci
  4. 在 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 设备创建流程

img


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/78545.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

云计算赋能质检LIMS的价值 质检LIMS系统在云计算企业的创新应用

在云计算技术高速发展的背景下&#xff0c;实验室信息化管理正经历深刻变革。质检LIMS&#xff08;实验室信息管理系统&#xff09;作为实验室数字化转型的核心工具&#xff0c;通过与云计算深度融合&#xff0c;为企业提供了高弹性、高安全性的解决方案。本文将探讨质检LIMS在…

【win11 安装WSL2 详解一遍过!!】

共有五个步骤&#xff0c;按部就班的做&#xff0c;保准成功&#xff01; 1. 打开开发者模式 设置->系统->开发者模式 2. 打开linux的win子系统 找到控制面板-程序和功能-启用或关闭Windows功能&#xff0c;选中“适用于Linux的Windows子系统”&#xff0c;“虚拟机…

Godot开发2D冒险游戏——第三节:游戏地图绘制

一、初步构建游戏地图 在游戏场景当中添加一个新的子节点&#xff1a;TileMapLayer 这一层称为瓦片地图层 根据提示&#xff0c;下一步显然是添加资源 为TileMapLayer节点添加一个TileSet 将地板添加进来&#xff0c;然后选择自动分割图集 自定义时要确保大小合适 让Godot自…

Django创建的应用目录详细解释以及如何操作数据库自动创建表

创建好Django项目后 如果要创建 python manage.py startapp 模块名模块 使用 我创建一个system模块后是 注意:urls是我自己建的文件 1.migrations目录 存放数据库的迁移文件,当models.py中模型定义发生变化时&#xff0c;通过迁移操作能同步数据库结构变化 __init__ 使该目录…

将输入帧上下文打包到下一个帧的预测模型中用于视频生成

Paper Title: Packing Input Frame Context in Next-Frame Prediction Models for Video Generation 论文发布于2025年4月17日 Abstract部分 在这篇论文中,FramePack是一种新提出的网络结构,旨在解决视频生成中的两个主要问题:遗忘和漂移。 具体来说,遗忘指的是在生成视…

STM32 串口USART

目录 常见的通信方式 串行通信和并行通信 全双工&#xff0c;半双工和单工通信 同步通信和异步通信 通信速率 常见的通信协议 串口基础知识 电平特性 串口传输协议 STM32F103的USART资源 端口引脚 数据寄存器单元 发送接收控制单元 实现串口发送 printf…

Taro on Harmony :助力业务高效开发纯血鸿蒙应用

背景 纯血鸿蒙逐渐成为全球第三大操作系统&#xff0c;业界也掀起了适配鸿蒙原生的浪潮&#xff0c;用户迁移趋势明显&#xff0c;京东作为国民应用&#xff0c;为鸿蒙用户提供完整的购物体验至关重要。 &#xfeff; &#xfeff;&#xfeff; 去年 9 月&#xff0c;京东 AP…

gem5-gpu教程05 内存建模

memory-modeling|Details on how memory is modeled in gem5-gpu gem5-gpu’s Memory Simulation gem5-gpu在很大程度上避开了GPGPU-Sim的单独功能模拟,而是使用了gem5的执行中执行模型。因此,当执行存储/加载时,内存会被更新/读取。没有单独的功能路径。(顺便说一句,这…

【python】lambda用法(结合例子理解)

目录 lambda 是什么? 为什么叫 lambda? 语法 举例 1. 最简单的 lambda:单个数字处理 2. 用 lambda 排序一组字符串(按照长度排序) 3. 在列表里找出绝对值最小的数字 4. 给 map() 用 lambda 5. 组合使用:筛选出偶数 lambda 和 def 的对比 lambda 适合用在什么地…

【ROS2】机器人操作系统安装到Ubuntu22.04简介(手动)

主要参考&#xff1a; https://book.guyuehome.com/ROS2/1.系统架构/1.3_ROS2安装方法/ 官方文档&#xff1a;https://docs.ros.org/en/humble/Installation.html 虚拟机与ubuntu系统安装 略&#xff0c;见参考文档 ubutun换国内源&#xff0c;略 1. 设置本地语言 确保您有…

C 调用 C++:extern “C” 接口详解与实践 C/C++混合编译

C 调用 C&#xff1a;extern “C” 接口详解与实践 核心问题在于 C 编译器会对函数名进行“修饰”&#xff08;Name Mangling&#xff09;以支持函数重载等特性&#xff0c;而 C 编译器则不会。此外&#xff0c;C 语言本身没有类、对象等概念。为了解决这个问题&#xff0c;我…

汽车制造行业如何在数字化转型中抓住机遇?

近年来&#xff0c;随着新一轮科技革命和产业变革的深入推进&#xff0c;汽车制造行业正迎来一场前所未有的数字化转型浪潮。无论是传统车企还是新势力品牌&#xff0c;都在积极探索如何通过数字化技术提升竞争力、开拓新市场。那么&#xff0c;在这场变革中&#xff0c;汽车制…

k8s学习记录(五):Pod亲和性详解

一、前言 上一篇文章初步探讨了 Kubernetes 的节点亲和性&#xff0c;了解到它在 Pod 调度上比传统方式更灵活高效。今天我们继续讨论亲和性同时Kubernetes 的调度机制。 二、Pod亲和性 上一篇文章中我们介绍了节点亲和性&#xff0c;今天我们讲解一下Pod亲和性。首先我们先看…

HarmonyOS:Navigation实现导航之页面设置和路由操作

导读 设置标题栏模式设置菜单栏设置工具栏路由操作页面跳转页面返回页面替换页面删除移动页面参数获取路由拦截 子页面页面显示类型页面生命周期页面监听和查询 页面转场关闭转场自定义转场共享元素转场 跨包动态路由系统路由表自定义路由表 示例代码 Navigation组件适用于模块…

雪花算法

目录 一、什么是雪花算法 二、使用雪花算法 ​三、使用UUID 使用自增主键是数据库中常用的唯一标识&#xff0c;今天尝试使用mybatisplus来实现三种方式的主键ID 使用起来也很简单 用注解指定一下使用那种方式的主键 一、什么是雪花算法 一种特殊的算法可以计算得到一个唯…

HarmonyOs @hadss/hmrouter路由接入

参考文档&#xff1a;官方文档 在根目录oh-package.json5配置 {"dependencies": {"hadss/hmrouter": "^1.0.0-rc.11"} }加入路由编译插件 hvigor/hvigor-config.json文件 {"dependencies": {"hadss/hmrouter-plugin": &…

C++学习笔记(三十八)——STL之修改算法

STL 算法分类&#xff1a; 类别常见算法作用排序sort、stable_sort、partial_sort、nth_element等排序搜索find、find_if、count、count_if、binary_search等查找元素修改copy、replace、replace_if、swap、fill等修改容器内容删除remove、remove_if、unique等删除元素归约for…

Crawl4AI 部署安装及 n8n 调用,实现自动化工作流(保证好使)

Crawl4AI 部署安装及 n8n 调用&#xff0c;实现自动化工作流&#xff08;保证好使&#xff09; 简介 Crawl4AI 的介绍 一、Crawl4AI 的核心功能 二、Crawl4AI vs Firecrawl Crawl4AI 的本地部署 一、前期准备 二、部署步骤 1、检查系统的网络环境 2、下载 Crawl4AI 源…

32单片机——外部中断

STM32F103ZET6的系统中断有10个&#xff0c;外部中断有60个 1、中断的概念 中断是为使单片机具有对外部或内部随机发生的事件实时处理而设置的&#xff0c;中断功能的存在&#xff0c;很大程度上提高了单片机处理外部或内部事件的能力 eg&#xff1a;&#xff1a;你打开火&…

UG NX二次开发(C#)-获取具有相同属性名称的体对象

文章目录 1、前言2、在UG NX中的属性的赋予3、通过UG NX二次开发获取相同属性的体对象1、前言 UG NX中每个对象都可以属于属性的,包括体、面、边、特征、基准等。在QQ群中有个群有提出一个问题,就是获取相同属性的体对象,然后将这个体对象导出到一个part文件中。我们今天先…