Linux Kernel 之九 详解 virtio-net 源码框架、执行流程

adtxl
2024-11-22 / 0 评论 / 6 阅读 / 正在检测是否收录...

转载自https://blog.csdn.net/ZCShouCSDN/article/details/132361829

1. 基于virtio的半虚拟化概述

1.1 virtio运行结构

image.png

  1. virtio 表示虚拟化 IO,用于实现设备半虚拟化,即虚拟机中运行的操作系统需要加载特殊的驱动(e.g. virtio-net)且虚拟机知道自己是虚拟机
相较于基于完全模拟的全虚拟化,基于virtio的半虚拟化可以提升设备访问性能
  1. 运行在虚拟机中的部分称为前端驱动,负责对虚拟机提供统一的接口
  2. 运行在宿主机中的部分称为后端驱动,负责适配不同的物理硬件设备

1.2 virtio架构层次

image31425034dc35875a.png

1.2.1 virtio前端驱动

  1. 运行在虚拟机中
  2. 针对不同类型的设备有不同的驱动程序,但是与后端驱动交互的接口都是统一的
  3. 本文分析 virtio-net 模块,源码位于 drivers/net/virtio_net.c

1.2.2 virtio层

  1. virtio 层实现虚拟队列接口,作为前后端通信的桥梁
  2. 不同类型的设备使用的虚拟队列数量不同,比如virtio-net使用两个队列,一个用于接收,另一个用于发送
  3. 源码位于 drivers/virtio/virtio.c

1.2.3 virtio-ring层

  1. virtio-ring 层是虚拟队列的具体实现
  2. 源码位于 driver/virtio/virtio_ring.c

1.2.4 virtio后端驱动

  1. 运行在宿主机中
  2. 实现 virtio 后端的逻辑,主要是操作硬件设备,比如向内核协议栈发送一个网络包完成虚拟机对于网络的操作
  3. 在 Qemu + KVM 虚拟化环境中,源码位于 Qemu 源码中(尚未详细分析)。后续将分析 seL4 中的后端实现

2. Linux virtio核心数据结构

2.1 virtio_bus结构

struct bus_type 是基于总线驱动模型的公共数据结构,定义新的 bus,就是填充该结构。virtio_bus 定义在 drivers/virtio/virtio.c 中,具体如下,
imaged279d0e8864b46f7.png

说明1:注册 virtio_bus

imaged242c92515ecf6e8.png
imageed06ba2dbb92cc7c.png

virtio_bus 以 core_initcall 的方式被注册,该方式注册的启动顺序优先级很高(作为对比,module_init 最终对应的是 device_initcall),在使用中要注意不同组件的启动顺序

说明2:virtio_dev_match 函数

virtio 驱动的 match 涉及到 virtio_device_id 结构,在 virtio_device 结构中包含该结构;在 virtio_driver 中则是包含该驱动支持的virtio_device_id 列表
image0d4d35e2c0b1f983.png
具体的 match 流程如下,

image5afba08c7a72b2f9.png
image5659e0fd870fd403.png

可见 virtio 驱动的 match 函数先匹配 device 字段,后匹配 vendor 字段,二者都满足条件时 match 成功

补充:从 virtio_dev_match 的流程可以看出,virtio_driver 中的 id_table 必须以 id->device = 0 结尾,以便结束循环

2.2 virtio_device结构

struct virtio_device 定义在 include/linux/virtio.h 中,具体如下
image5437a49ca1fd94e1.png

struct virtio_device_id id

imagedca00e47d8ffee20.png

其中的 device 成员标识了当前 virtio_device 的用途,virtio-net 是其中的一种,

image8e641d14c0bb4de1.png

const struct virtio_config_ops *config

imagefeb38d7670207a77.png

virtio_config_ops 操作集中的函数主要与 virtio_device 的配置相关,主要有如下 2 类操作,

  1. 实例化 / 反实例化 virtqueue,其中要特别注意 find_vqs 函数,该函数用于实例化 virtio_device 所持有的 virtqueue
  2. 获取 / 设置 virtio_device 的属性与状态,相关属性均在虚拟机虚拟出的 PCI 配置空间

struct list_head vqs
virtio_device 持有的 virtqueue 链表,virtio-net 中建立了 2 条 virtqueue(虚拟队列)

u64 features
virtio_driver & virtio_device 同时支持的通信特性,也就是前后端最终协商的通信特性

2.3 virtio_driver结构

struct virtio_driver 定义在 include/linux/virtio.h 中,具体如下,
imagecc39f5d393b0d0f8.png

const struct virtio_device_id *id_table
对应 virtio_device 结构中的 id 成员,virtio_device 中标识的是当前 device 的 id 属性;而 virtio_driver 中的 id_table 则是当前 driver 支持的所有 id 列表

const unsigned int *feature_table & unsigned int feature_table_size
feature 列表包含了当前 driver 支持的所有 virtio 传输属性,feature_table_size 则说明属性数组的元素个数

probe 函数
virtio_driver 层面注册的 probe 函数,如上文所述,virtio_bus 层面也注册了 probe 函数,在 Linux 总线驱动框架 & virtio 核心层中,当virtio_device & virtio_driver匹配成功后,先调用 bus 层面的 probe 函数,在 virtio_bus 层面的 probe 函数中,又调用了 virtio_driver 层面的probe 函数

2.4 virtqueue结构

struct virtqueue 定义在 include/linux/virtio.h 中,具体如下,
image0a97d0d95afc11ab.png

2.5 vring结构

struct vring 定义在 include/uapi/linux/virtio_ring.h 中,具体如下,
image2f9e848fb6e2ac6b.png

2021 / 08 / 06补充:一定要结合一个后端驱动进行分析,可以对照 rpmsg-lite 分析。可以就分析 rpmsg-lite 对 virtqueue 的操作部分,不用上升到 rpmsg 协议的部分

说明1:vring的三个构成区域

  1. Destcriptor Table:描述内存 buffer,主要包括 addr & len 等信息
  2. Avail Ring:用于前端驱动(Guest)通知后端驱动(Host)有可用的描述符。e.g. 前端驱动有一个报文需要发送,需要将其加入 Avail Ring,之后通知后端驱动读取
  3. Used Ring:用于后端驱动(Host)通知前端驱动(Guest)有可用的描述符,或者是后端驱动已将前端驱动提供的描述符使用完毕。e.g. 后端驱动有一个报文需要发送,需要将其加入Used Ring,之后通知前端驱动读取

可见 avail & used 的命名都是站在 Host 的角度进行的

说明2:vring的存储
vring 结构只是用于描述 vring 在内存中的布局(因此包含的都是指针变量),实际用于通信的 vring 是存储在内存中。上文提到的 vring 的三个区域是在内存中连续存储的,而且是存储在 Guest & Host 共享的一片连续内存中。我们可以通过 vring_init 函数理解 vring 存储结构的布局

imagefad8a85974b09697.png

实际 vring 的内存布局如下图所示,

image4d7bf1d269c65148.png

在计算 used ring 的起始地址时,在 avail->ring[num] 的地址之后又加了 sizeof(__virtio16),也就是增加了 2B,是为了容纳 avail ring 末尾的used_event,该机制详见下文(是一种控制中断触发频率的机制)

说明3:实际 vring 的大小
实际 vring 的大小可以通过 vring_size 函数获得
image0783fb922fe8b98f.png

  1. 计算 avail ring 时加 3,分别为 flags、idx 和 used_event
  2. 计算 used ring 时加3,分别为 flags、idx 和 avail_event
  3. 计算过程中,包含了为满足对齐要求 padding 的空间

说明4:used_event 与 avail_event 机制概述

这 2 个字段均与 virtio 设备的 VIRTIO_RING_F_EVENT_IDX 特性有关,由于 virtio 驱动触发对方中断将导致 CPU 反复进出虚拟机 & 宿主机模式,从而降低性能,因此需要控制触发中断频率的机制

  1. avail ring 中的 used_event
  • 由前端驱动(Geust)设置,标识希望后端驱动(Host)触发中断的阈值
  • 后端驱动(Host)在向 Used Ring 加入 buffer 后,检查 Used Ring 中的 idx 字段,只有达到阈值才触发中断
  1. used_ring 中的 avail_event
  • 由后端驱动(Host)设置,标识希望前端驱动(Guest)触发中断的阈值
  • 前端驱动(Guest)在向 Avail Ring 加入 buffer 后,检查 Avail Ring 的 idx 字段,只有达到阈值才触发中断

综上所属,vring 结构的构成如下图所示,

imaged89833152d18edda.png

2.6 vring_virtqueue结构

vring_virtqueue 结构用于描述前端驱动(Guest)中的一条虚拟队列
imagea9bc11f9fb9c53fa.png

总结:virtio_device / vring_virtqueue / virtqueue / vring结构之间的关系
image78ad68024da8b51f.png

3. virtio操作

如上文所述,virtio 框架向虚拟机中的前端驱动提供了统一的 IO 操作接口,我们下面就分析这些操作。在理解了 virtio 的操作之后,配合不同虚拟设备的属性,就比较容易理解虚拟设备前端驱动的实现。比如 virtio-net 就是网卡驱动 + virtio 操作

3.1 创建virtqueue

3.1.1 核心流程梳理(重点)

virtio_pci_probe阶段
在 virtio 框架中,首先向虚拟机注册的是一个 pci_driver,后续所有 vitio 设备均是以虚拟 pci 设备的形式注册(转载注,这样说应该是不对的,并不是所有的virtio设备都是以pci设备的形式注册的),因此第 1 步流程即是运行virtio_pci_probe 函数

image7c20b29f734f73f8.png

virtio_pci_modern_probe 阶段
在 virtio_pci_probe 函数中,会调用 virtio_pci_modern_probe 函数,进行进一步初始化
image4754d3d2f86b0236.png
在 virtio_pci_modern_probe 函数中会进行 2 步非常重要的操作,
imagee204c366cca6f6af.png

  1. 设置 virtio_device 中的 virtio_config_ops 回调函数集合,其中就包括了最重要的 find_vqs 回调函数
  2. 设置 setup_vq 回调函数,该回调函数会在 find_vqs 回调函数中被调用

所以在初始化 virtqueue 的过程中,是先调用 find_vqs 回调函数,后调用 setup_vq 回调函数

find_vqs 回调函数阶段
image11b1d69386ac8d01.png

在 virtio_pci_probe 的最后阶段,会注册 virtio_device,该操作会触发 virtio 驱动的 probe 函数被调用,在该函数中,会触发 find_vqs 回调函数被调用。下面我们以 virtio-net 前端驱动为例,说明创建 virtqueue 的流程

virtio_pci_probe
--> virtio_pci_modern_probe
    // 设置find_vqs回调函数
    // 设置setup_vq回调函数
--> register_virtio_device // 触发virtio probe函数被调用
  
// virtio probe函数阶段
virtio_dev_probe
    // 调用virtio_driver注册的probe函数
--> virtnet_probe
  --> init_vqs
        // 分配virtqueue结构(send_queue & recv_queue)
    --> virtnet_alloc_queues
    --> virtnent_find_vqs
      --> find_vqs回调函数(vp_modern_find_vqs)
        --> vp_find_vqs
          --> vp_find_vqs_msix // 还注册了vring_interrupt
            --> vp_setup_vq
              --> setup_vq回调函数(setup_vq)
                --> vring_create_virtqueue
                      // 分配存储vring的连续物理内存
                  --> vring_alloc_queue
                      // 生成vring_virtqueue结构,并初始化
                  --> __vring_new_virtqueue

下面我们就分析最核心的几个函数

3.1.2 setup_vq 函数分析

setup_vq 函数有如下 3 个核心步骤,

1. 检查 virtqueue 配置参数
image917812752ea5fdbe.png
前端驱动读取 PCI 配置空间中的参数,判断其合法性。其中要注意 virtqueue 的长度(queue_size)必须是 2 的幂次,因为后续需要通过简单的位与运算实现绕回

2. 实际生成virtqueue
image3b9becf826e87e9d.png
实际生成 virtqueue 通过 vring_create_virtqueue 函数实现,此处需要注意如下 3 点,

  1. 对齐要求。如上文所述,vring 结构在内存布局上有对齐要求,该要求在创建 virtqueue 时传递,就是此处的 SMP_CACHE_BYTES 宏
    image831c2c2c82a2b2b1.png
  2. notify hook 函数。用于前端驱动(Guest)触发后端驱动(Host)中断,通知其有数据需要处理
    imagece24a02fee4f0989.png

这里的 notification register 也在 PCI 配置空间中,该地址在 setup_vq 函数中指定

image3d0c051aaeb72d67.png

  1. callback hook 函数。callback hook函数在 virtqueue 被触发时调用,以 virtio-net 驱动为例,callback hoot 函数在 virtnet_find_vqs 函数中指定
    imaged2ab436ab4860bd3.png

3. 同步 GPA 到宿主机
image060905712a115192.png
virtqueue 作为前端驱动与后端驱动的交互媒介,需要在虚拟机和宿主机中同步这段共享内存的地址。调用 vring_create_vritqueue 函数生成的 virtqueue,分配的内存为 GPA,需要将其同步到宿主机,宿主机才能将其转换为 HVA 使用(因为虚拟机的 GPA 就是宿主机分配的)

3.1.3 vring_create_virtqueue函数分析

image972ef0924e2f471f.png

可见 vring 的内存被分配在连续的 1 个或多个 page 中,而且如果内存条件不满足,会动态调整 vring 的长度(num 变量)

3.1.4 _vring_new_virtqueue函数分析

image35538287c91489ca.png
__vring_new_virtqueue 函数的实现注释已经比较清楚了,需要说明的是,该函数返回的是 virtqueue 结构。在 Linux 的 virtio 层实现中,代码会根据需要在 virtqueue 与 vring_virtqueue 结构间进行转换

3.2 前端驱动发送数据

3.2.1 流程概要

  1. 从 descriptor table 中取出描述符
  2. 根据要发送的数据填充并组织描述符
  3. 将组织好的描述符加入 avail ring
  4. 触发后端驱动中断,通知其处理数据

说明:vring 的描述符结构与 scatterlist 结构是绝配

3.2.2 virtqueue_add函数分析

3.3 前端驱动触发中断

3.3.1 virtqueue_kick_prepare函数分析

3.3.2 virtqueue_notify函数分析

3.4 前端驱动被触发中断

3.4.1 注册中断处理函数

3.4.2 vring_interrupt函数分析

3.5 前端驱动接收数据

3.5.1 virtqueue_get_bug_ctx函数分析

3.5.2 补充:对对 vring_desc_state desc_state 结构中 data 成员的使用

3.5.3 detach_buf函数分析

4. virtio-net前端驱动分析

4.1 重要数据结构

4.1.1 send_queue结构

4.1.2 receive_queue结构

4.1.3 virtnet_netdev_callback数组

4.2 发送报文流程

4.2.1 到达start_xmit函数

4.2.2 start_xmit函数主要流程

4.2.3 xmit_skb函数

4.3 接收报文流程

4.3.1 NAPI接收网络包流程概述

4.3.2 virtio中断处理函数

4.3.3 virtnet_poll函数分析

4.3.4 virtnet_receive函数分析前奏

4.3.5 virtnet_receive函数分析

5. Linux virtio-net中对内存的使用

5.1 scatterlist实现分析

5.1.1 scatterlist产生背景

5.1.2 scatterlist结构

5.1.3 scatterlist常用API

5.2 virtio-net发送数据中的内存操作

5.2.1 将sk_buff关联到scatterlist数组

5.2.2 将scatterlist数组映射到vring描述符

5.3 virtio-net接收数据中的内存操作

0

评论 (0)

取消