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

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

转载自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函数分析

前端驱动发送数据的核心为virtqueue_add函数,下面给出该函数的分析
image.png
image3c893a7d601ffe15.png
imageac78f058293c48aa.png
image1d8142a64bd8fc25.png
imagedc16b199b59eebdb.png

上面的截图很壮观,下面通过一张图展现该过程,

image279611020032fc76.png

说明1:可见 virtqueue_add 函数封装了一次数据请求,所有 out & in 请求均组织为一个 decriptor chain 提交到 avail ring
从上文分析可见,如果将 out & in 数据请求组织在一起,将使得接收端的处理逻辑非常复杂。因此在实际使用中(e.g. virtio-net,rpmsg),一般为 out & in 的数据请求单独建立 virtqueue,即输入和输出使用不同的虚拟队列

说明2:由上图可知,descriptor table 以静态链表的方式管理,因此空闲链表中各个描述符在物理上不一定是连续的,而是依靠描述符中的next域维护链接关系

说明3:对 virtqueue_add 函数的使用。virtqueue_add 函数被封装为如下 4 种方式供前端驱动调用,

  1. virtqueue_add_sgs 可以同时提交out & in数据请求,且个数可设置
    image786371976fe93c29.png
  2. virtqueue_add_outbuf只提交了一个out数据请求
    imaged25489e0d75a6d8a.png
  3. virtqueue_add_inbuf 只提交一个 in 数据请求
    imagebebed2c31d28dd19.png
  4. virtqueue_add_inbuf_ctx 只提交一个 in 数据请求,且携带上下文信息。注意:ctx上下文信息与 inderect 特性是互斥的
    imageac59c6dfb03f1e30.png

3.3 前端驱动触发中断

前端驱动通过 virtqueue_kick 函数通知后端驱动有数据需要处理
image39b25f60a71bebc1.png
其中 virqueue_kick_prepare 函数判断是否需要触发中断,virtqueue_notify 函数实际触发中断

3.3.1 virtqueue_kick_prepare函数分析

image4073e3549d58a445.png
说明:vring_need_event函数实现
image77329c9fad7dc83c.png
只有当event_idx在[old, new-1]范围时,才会允许出发中断

3.3.2 virtqueue_notify函数分析

image3e917d37e116cbae.png
virtqueue_notify 会调用上文介绍的 notify 回调函数,实现对后端驱动的通知,在本文环境中,该回调函数为 vp_notify 函数
imagefeda8ce4b9d19490.png

3.4 前端驱动被触发中断

3.4.1 注册中断处理函数

在创建virtqueue时,会为每条virtqueue注册中断,可参考vp_find_vqs_msix函数
image8f926cc1ff1bb25f.png
可见中断处理函数为vring_interrupt,注意这里注册的是msi中断

3.4.2 vring_interrupt函数分析

image3cee7d43a1e5b2eb.png

vring_interrupt函数的核心操作是调用创建virtqueue时注册的callback回调函数,以virtio-net模块为例,接收和发送队列注册的callback回调函数如下,
image6ca7e83aa051bf28.png

3.5 前端驱动接收数据

3.5.1 virtqueue_get_bug_ctx函数分析

imagee9b06c384483b4b4.png
image2bde171fd0735a66.png

说明:virtqueue_get_buf_ctx 函数的返回值为 vq->desc_state[i].data,该值在调用 virtqueue_add 时设置。virtqueue_add 写入该值,目的就是用于索引 buffer(the token identifying the buffer)

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

在 vring_virtqueue 结构中定义
如上文所述,在 vring_virtqueue 结构中定义了 desc_state 数组,根据注释,该结构描述了每个描述符的状态(更好的理解是每个描述符有一个)
image67daa068f6a09b4e.png
数组大小为 virtqueue 大小,空间随 vring_virtqueue 结构一同分配,该数组用于存储每次数据传输请求的上下文
imagefd4348dd03093ae3.png

vring_desc_state结构如下,
image1fe77942477f4f91.png
我们这里就是讨论其中 data 成员的使用

在 virtqueue_add 函数中设置
image73dc17c0f9338cc1.png

这里注意 2 点,

  1. 填入 data 的值。此处填入的值为 virtqueue_add 函数的入参 data
  2. 填入 desc_state 数组的下标。此处使用的下标为 head,为本次数据请求的 chain descriptor 的首个描述符下标

在 virtqueue_get_buf_ctx 中读取
imageef297c5a34ed105f.png
此处使用的下标i是used ring中取出的chain descriptor 中首个描述符的下标,这里对应了一次virtqueue_add加入的数据请求。此处就将当时virtqueue_add函数写入data作为返回值
说明:这里就可以看出 virtio 机制设计的巧妙之处,后端驱动在使用不同的 chain descriptor 后不需要按取出的顺序归还。这里有 2 点机制上的保障,

  1. descriptor table 使用静态链表方式管理
  2. desc_state 数组按描述符管理

3.5.3 detach_buf函数分析

image39644a4b5c68c239.png
最终给出一张图,就是虚拟机和宿主机指向同一段内存,以实现二者之间的交互
image8890fd36a03772c1.png

4. virtio-net前端驱动分析

4.1 重要数据结构

4.1.1 send_queue结构

imagee724aacf9e040600.png
send_queue结构是对virtqueue的封装,是virtio-net的发送队列,即数据流向从前端驱动(Guest)到后端驱动(Host)

4.1.2 receive_queue结构

image1d0f319d5630e0d7.png
receive_queue结构也是对virtqueue的封装,是virtio-net的接收队列,即数据流向从后端驱动(Host)到前端驱动(Guest)

说明:multiqueue virtio-net
virtio-net前端驱动支持multiqueue机制,也就是允许有多对send_queue & receive_queue,在virtnet_probe过程中会检查宿主机的设置,获取收发队列的对数
imagec94e4a156f6f3955.png
这样在创建virtqueue时,会根据配置项分配内存
image4dc9c4d90433b36d.png
但是在一般情况下,均使用1条send_queue+1条receive_queue,且没有控制队列

4.1.3 virtnet_netdev_callback数组

imaged5e6b3f9a737da17.png
在Linux中,net_device结构描述了一个网络设备,其中的net_device_ops则包含了该网络设备的操作方法集。其中特别注意ndo_start_xmit_callback函数,该函数为网卡发送报文时使用的函数

4.2 发送报文流程

4.2.1 到达start_xmit函数

内核协议栈
dev_hard_start_xmit  //net\core\dev.c
    xmit_one
        netdev_start_xmit   //include/linux/netdevice.h
            __netdev_start_xmit
                ops->ndo_start_xmit(skb, dev);  到virtio_net.c 中
    ||
    \/
virtio_net.c中
static const struct net_device_ops virtnet_netdev = {
    .ndo_start_xmit      = start_xmit,     
 
start_xmit
    xmit_skb        // 把skb放到vqueue中
        virtqueue_add_outbuf
         
            //把数据写到队列中
            virtqueue_add       //virtio_ring.c
            virtqueue_add_split
                virtqueue_kick  //virtio_ring.c
    ||
    \/
virtqueue_kick
    virtqueue_notify
        vq->notify(_vq)      // agile_nic.c中notify函数,通知板卡驱动给队列中写数据了,然后板卡收到notify后,读取数据
  1. 虚拟机中的进程发送网络包时,仍然通过文件系统和 socket 调用网络协议栈到达网络设备层。只不过此时不是到达普通的网络设备,而是 virtio-net 前端驱动
  2. virtio-net 前端驱动作为网卡设备驱动层,接收 IP 层传输下来的二层网络数据包
  3. 发送网络包的流程最终将调用 net_device_ops 结构中的 ndo_start_xmit 回调函数,在 virtio-net 驱动中,就是 start_xmit 函数

image84310d30a5142e29.png

4.2.2 start_xmit函数主要流程

与virtio框架相关的只有2个步骤,

  1. 调用xmit_skb函数将网络包写入virtqueue
    image7da806322aa569d2.png
  2. 触发后端驱动中断
    image0e62b28c2e5718fd.png
    virtqueue_kick函数在上文已有说明,此处说明一下xmit_skb函数的实现

4.2.3 xmit_skb函数

image8e4d17ac85d87cde.png
xmit_skb()函数将sk_buff映射到scatterlist中,之后调用virtqueue_add_outbuf函数将数据请求加入send_queue的avail ring

说明:这里传递给data的值为skb,也就是要发送的skb的地址。注意,skb的地址值是一个GVA(Guest Virtual Address),因此只在虚拟机中使用

4.3 接收报文流程

数据接收流程:

数据接收流程:
 
 napi_gro_receive(&rq->napi, skb);
                netif_receive_skb
                    __netif_receive_skb         // 传输skb给网络层
    /\
    ||
驱动 virtio_net.c 中poll方法 napi_poll(n, &repoll); 即virtio_net.c 中 virtnet_poll()
 virtnet_poll
     virtnet_receive
         receive_buf                 // 接收到的数据转换成skb
             //根据接收类型XDP_PASS、XDP_TX等对 virtqueue 中的数据进行不同的处理
             skb = receive_mergeable(dev, vi, rq, buf, ctx, len, xdp_xmit,stats); or
             skb = receive_big(dev, vi, rq, buf, len, stats);  or
             skb = receive_small(dev, vi, rq, buf, ctx, len, xdp_xmit, stats);
             napi_gro_receive(&rq->napi, skb);   // 把skb上传到上层协议栈


         schedule_delayed_work                  //通过你延迟队列接收数据
             refill_work
                 try_fill_recv(vi, rq, GFP_KERNEL);
                 如果检测到本次中断 receive 数据完成,则重新开启中断                                
                local_bh_enable                 //enable 软中断 等待下一次中断接收数据
    /\
    ||
中断下半步
 
执行软中断回调函数 net_rx_action(), 调用 virtio_net.c 中 virtnet_poll()
 
    /\
    ||
检查poll队列上是否有设备在等待轮询
napi_schedule ->__napi_schedule  ->   list_add_tail(&napi->poll_list, &sd->poll_list); //把 NAPI 加入到本地cpu的 softnet_data 的 poll_list链表头
        __raise_softirq_irqoff(NET_RX_SOFTIRQ);          // 调度收包软中断
                     
    /\
    ||
skb_recv_done           //virtio_net.c 中 virtnet_find_vqs() 中,数据接收完成回调函数
    virtqueue_napi_schedule
        调用 napi_schedule
    /\
    ||
每个vq 对应一个数据接收函数 vring_interrupt()
vring_interrupt()       //virtio_ring.c
    vq->vq.callback(&vq->vq);   即virtio_net.c 中 skb_recv_done
    /\
    ||
中断上半步
pcie网卡发送数据给host时,会触发pci msix硬中断,然后host driver agile_nic.c 中执行回调函数vring_interrupt

4.3.1 NAPI接收网络包流程概述

  1. 传统的网络收包流程完全靠中断驱动,当网络包到达十分频繁时,就会频繁触发中断,进而影响系统的整体性能
  2. NAPI方式的核心就是当有数据包到达时,集中处理网络包,之后再去处理其它事情
  3. NAPI的处理流程是,当一些网络包到达触发中断时,内核处理完这些网络包之后,主动轮询poll网卡,主动去接收到来的网络包。如果一直有,就一直处理,等处理告一段落再返回

当再有下一批网络包到达时,再中断,再轮询 poll。这样就会大大减少中断的数量,提升网络处理的效率

说明:注册 NAPI 收包 poll 函数。在 virtio-net 前端驱动中,在 probe 过程中,会调用 netif_napi_add 函数注册收包 poll 函数
image994be96ee72fc651.png
可见此处注册的函数为virtnet_poll

4.3.2 virtio中断处理函数skb_recv_done

如上文所述,virtqueue的中断处理函数最终会调用到创建virtqueue时注册的callback回调函数,该函数为skb_recv_done,这也就是virtio-net前端驱动的收包中断顶半部操作
imageeedac1dc47408b37.png
image3f0f43f66f4cacf2.png

4.3.3 virtnet_poll函数分析

image46bd4792793e8c41.png
说明:virtnet_poll_cleantx 函数分析

在接收数据报文之前,先调用了virtnet_poll_cleantx函数处理了send_queue
imagee4374ce4c052d021.png
其中的核心为 free_old_xmit_skbs 函数,分析如下,
imagefec429a2ddcfdf77.png
这里也很好地体现了 vring_desc_state 结构中 data 成员的使用,

  1. 前端驱动发送报文时,将含有报文的skb写入data成员,数据请求加入avail ring
  2. 后端驱动处理完数据请求后,将chain descriptor从avail ring加入used ring
  3. 前端驱动在处理后端驱动已使用的chain descriptor时,从data成员中取出skb地址,并释放sk_buffer

4.3.4 virtnet_receive函数分析前奏

首先思考一个问题,receive_queue 中的 avail ring 是何时填充的 ?

receive_queue 的数据流向是从后端驱动到前端驱动,但是前端驱动需要先将数据请求加入 avail ring,这样后端驱动在要发送网络包时,才能从 avail ring 中取出可用的 chain descriptor

而且这里还带来另外一个问题,前端驱动是不知道后端驱动所要发送的报文大小的,那么该如何组织 descriptor ring 呢 ?

结合上文,这里解题的线索就是 virtqueue_add_inbuf & virtqueue_add_inbuf_ctx 函数在 virtio-net 前端驱动中的调用。这样我们就很容易地找到关键的函数 try_fill_recv !

image6050f823310a5a46.png

可见 try_fill_recv 函数会将所有可用的描述符均加入receive_queue 的 avail ring,供后端驱动使用。我们分析 add_recvbuf_small 函数,另外两种情况需要后端驱动配置支持

image35f1e079eff48548.png

这里需要注意调用 virtqueue_add_inbuf_ctx 的 2 个参数,因为后续的接收报文流程会使用

  • data:实参为 buf,即分配的内存页面的 GVA
  • ctx:实参为 ctx,值为 xdp_headroom

说明:try_fill_recv 函数的调用时机

  1. 打开网卡时
    imageb3da2b5463a8e9a6.png
    其中调度 vi->refill 工作,也会导致 try_fill_recv 函数被调用
  2. 网卡restore时
    image7cd17f6bb8cf0a28.png
  3. 接收报文时,也就是接下来要分析的函数
    image9cab8e0c198e047c.png

4.3.5 virtnet_receive函数分析

image911c46f731cbf62b.png

  1. 调用 virtqueue_get_buf 函数将 receive_queue 中 used ring 的 chain descriptor 归还 descriptor table,返回的 buf 就是上文分析的分配的内存的 GVA,该地址在虚拟机中可以使用
  2. 调用 receive_buf 函数接收报文数据
    image88b0abb4fc4ec4ab.png
    至此,virtio-net 前端驱动接收报文的工作就结束后,后续就是虚拟机 Linux 内核网络协议栈的工作了

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

5.1 scatterlist实现分析

5.1.1 scatterlist产生背景

scatterlist 用于汇总分散的物理内存(以页为单位),并以数组的形式组织起来,典型的应用场景如下图所示,
image046d728b28c2c102.png
image4498683262782009.png

在一个系统中,CPU、DMA 和 Device 通过不同的方式使用内存,

  1. CPU通过MMU以虚拟地址(VA)访问内存
  2. DMA直接以物理地址(PA)访问内存
  3. Device 通过自己的 IOMMU 以设备地址(DA)访问内存

如果访问的内存虚拟地址连续但是物理地址不连续,CPU 的访问没有问题,但是当需要将内存地址交给 DMA 进行传输时,只能以不连续的物理内存块的方式传递。而 scatterlist 就是用户汇总这些不连续的物理内存块的方式

5.1.2 scatterlist结构

image6032bf359a455379.png
scatterlist 以 page 为单位,描述了一个物理地址连续的内存块

说明1:如果要组织的连续物理内存超过一页怎么办 ?

要组织的连续物理内存超过一页是常态,所以单个 scatterlist 结构是没啥实际用途的。在实际使用中,Linux 内核默认将 scatterlist 组织为数组使用。在 virtio-net 前端驱动中,收发队列中均包含了 scatterlist 数组
image41577a08e7340d7d.png

需要注意的是,这里 scatterlist 数组的大小与 sk_buff 中分片的个数是匹配的,这里增加的 2 个 scatterlist 分别用于存放 sk_buff 的线性数据部分和 virtio-net 的头部信息,可以参考下图理解
image3a2f250afc344064.png

说明2:page_link 中 bit1 的作用

page_link 中的 bit1 是数组有效成员终止位,因为一次传输不一定使用 scatterlist 数组的所有成员,因此需要对最后一个有效的成员进行标记。下图中,一个 scatterlist 数组有 6 个成员,但是本次传输只使用其中 3 个
image1dc8b26583fd0297.png

Linux 内核代码中通过如下接口设置 & 检查该标志位
imageca3e89b1c0dda2aa.png
image81cfecb66fdf9d07.png

说明3:page_link 中 bit0 的作用

page_link 中的 bit0 是 sacatterlist 数组链接标志,用于实现将 2 个 scatterlist 数组链接起来。如果 bit0 置 1,则该 page_link 指向的不是一个 page 结构,而是指向另一个 scatterlist 数组
image7a502ae0c024eda8.png
Linux 内核代码中通过如下接口设置 & 检查该标志位

image43de1e1216ba4d12.png
可见如果需要链接 2 个 scatterlist 数组,前一个数组的最后一个成员不能指向有效 page。看到这里,就更容易理解之前分析的virtqueue_add 函数

5.1.3 scatterlist常用API

  • sg_init_table
    image6d742e901015c245.png
  • sg_assign_page
    image86747cec0793122b.png
    sg_assign_page 函数将一个 page 与一个 scatterlist 关联起来
  • sg_set_page
    image3be2a2d2d6d5bbe2.png
    sg_set_page 在关联 page 的基础上,设置了内存块的偏移量与长度
  • sg_set_buf
    imagedeb00719cd140f7f.png
    sg_set_buf函数是最常用的关联内存块与scatterlist的API,此处出入的buf参数为内存块起始的虚拟地址
  • sg_init_one
    image191a3ba7be29413e.png
    sg_init_one用于初始化一个scatterlist结构,并与一个内存块关联(该内存块必须在1个page内)
  • sg_page
    imagebfefd3db7efb0b32.png
    sg_page返回与scatterlist关联的物理页面地址
  • sg_next
    image51726bcb2d94be19.png
    sg_next用于取出scatterlist数组中的下一个成员,如果达到终止成员,则返回NULL

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

5.2.1 将sk_buff关联到scatterlist数组

imagea07b4b3a964118a1.png
这里的核心是 skb_to_sgvec 函数,该函数用于将 sk_buff 中存储报文用的各个 page 关联到 scatterlist 数组,下面分析该函数
imageecd958c13fbe524c.png
__skb_to_sgvec 函数中,将 sk_buff 的逐个分片都关联到 scatterlist 数组中
image015764b01bbbb36f.png

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

image3db72098bff79c7b.png
imagef8dcc11f441cf64b.png

这里其实就回到了我们之前分析的 virtqueue_add 函数

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

备忘录:

topic 2:seL4中如何对接virtio-net

topic 3:virtio-net的上下游模块

topic 4:宿主机如何注册pci device,可以先分析qemu的实现思路

topic 5:SKB buffer的使用(这个属于网络相关知识点的补强)

0

评论 (0)

取消