virtio-net 实现机制

目录

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

1.1 virtio运行结构

1.2 virtio架构层次

1.2.1 virtio前端驱动

1.2.2 virtio层

1.2.3 virtio-ring层

1.2.4 virtio后端驱动

2. Linux virtio核心数据结构

2.1 virtio_bus结构

2.2 virtio_device结构

2.2.1 struct virtio_device_id id

2.2.2 const struct virtio_config_ops *config

2.2.3 struct list_head vqs

2.2.4 u64 features

2.3 virtio_driver结构

2.3.1 const struct virtio_device_id *id_table

2.3.2 const unsigned int *feature_table & unsigned int feature_table_size

2.3.3 probe函数

2.4 virtqueue结构

2.5 vring结构

2.6 vring_virtqueue结构

3. virtio操作

3.1 创建virtqueue

3.1.1 核心流程梳理(重点)

3.1.2 setup_vq函数分析

3.1.3 vring_create_virtqueue函数分析

3.1.4 __vring_new_virtqueue函数分析

3.2 前端驱动发送数据

3.2.1 流程概要

3.2.2 virtqueu_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_buf_ctx函数分析

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

3.5.3 detach_buf函数分析

4. virio-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接收数据中的内存操作


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

1.1 virtio运行结构

0

① virtio表示虚拟化IO,用于实现设备半虚拟化,即虚拟机中运行的操作系统需要加载特殊的驱动(e.g. virtio-net)且虚拟机知道自己是虚拟机

相较于基于完全模拟的全虚拟化,基于virtio的半虚拟化可以提升设备访问性能

② 运行在虚拟机中的部分称为前端驱动,负责对虚拟机提供统一的接口

③ 运行在宿主机中的部分称为后端驱动,负责适配不同的物理硬件设备

1.2 virtio架构层次

0

1.2.1 virtio前端驱动

① 运行在虚拟机中

② 针对不同类型的设备有不同的驱动程序,但是与后端驱动交互的接口都是统一的

③ 本文分析virtio-net模块,源码位于drivers/net/virtio_net.c

1.2.2 virtio层

① virtio层实现虚拟队列接口,作为前后端通信的桥梁

② 不同类型的设备使用的虚拟队列数量不同,比如virtio-net使用两个队列,一个用于接收,另一个用于发送

③ 源码位于drivers/virtio/virtio.c

1.2.3 virtio-ring层

① virtio-ring层是虚拟队列的具体实现

② 源码位于driver/virtio/virtio_ring.c

1.2.4 virtio后端驱动

① 运行在宿主机中

② 实现virtio后端的逻辑,主要是操作硬件设备,比如向内核协议栈发送一个网络包完成虚拟机对于网络的操作

③ 在Qemu + KVM虚拟化环境中,源码位于Qemu源码中(尚未详细分析)。后续将分析seL4中的后端实现

2. Linux virtio核心数据结构

2.1 virtio_bus结构

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

0

说明1:注册virtio_bus

0

0

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

说明2:virtio_dev_match函数

virtio驱动的match涉及到virtio_device_id结构,在virtio_device结构中包含该结构;在virtio_driver中则是包含该驱动支持的virtio_device_id列表

0

具体的match流程如下,

0

0

可见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中,具体如下,

0

重点说明如下4个字段,

2.2.1 struct virtio_device_id id

0

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

0

2.2.2 const struct virtio_config_ops *config

0

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

① 实例化 / 反实例化virtqueue,其中要特别注意find_vqs函数,该函数用于实例化virtio_device所持有的virtqueue

②. 获取 / 设置virtio_device的属性与状态,相关属性均在虚拟机虚拟出的PCI配置空间

2.2.3 struct list_head vqs

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

2.2.4 u64 features

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

2.3 virtio_driver结构

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

0

重点说明如下4个字段,

2.3.1 const struct virtio_device_id *id_table

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

2.3.2 const unsigned int *feature_table & unsigned int feature_table_size

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

2.3.3 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中,具体如下,

0

2.5 vring结构

struct virtqueue定义在include/uapi/linux/virtio_ring.h中,具体如下,​​​​​​​0

2021 08 / 06补充:一定要结合一个后端驱动进行分析,可以对照rpmsg-lite分析

可以就分析rpmsg-lite对virtqueue的操作部分,不用上升到rpmsg协议的部分

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

① Destcriptor Table:描述内存buffer,主要包括addr & len等信息

② Avail Ring:用于前端驱动(Guest)通知后端驱动(Host)有可用的描述符

e.g. 前端驱动有一个报文需要发送,需要将其加入Avail Ring,之后通知后端驱动读取

③ Used Ring:用于后端驱动(Host)通知前端驱动(Guest)有可用的描述符,或者是后端驱动已将前端驱动提供的描述符使用完毕

e.g. 后端驱动有一个报文需要发送,需要将其加入Used Ring,之后通知前端驱动读取

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

说明2:vring的存储

vring结构只是用于描述vring在内存中的布局(因此包含的都是指针变量),实际用于通信的vring是存储在内存中

上文提到的vring的三个区域是在内存中连续存储的,而且是存储在Guest & Host共享的一片连续内存中

我们可以通过vring_init函数理解vring存储结构的布局

0

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

0

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

说明3:实际vring的大小

实际vring的大小可以通过vring_size函数获得

0

① 计算avail ring时加3,分别为flags、idx和used_event

② 计算used ring时加3,分别为flags、idx和avail_event

③ 计算过程中,包含了为满足对齐要求padding的空间

说明4:used_event与avail_event机制概述

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

① avail ring中的used_event

a. 由前端驱动(Geust)设置,标识希望后端驱动(Host)触发中断的阈值

b. 后端驱动(Host)在向Used Ring加入buffer后,检查Used Ring中的idx字段,只有达到阈值才触发中断

② used_ring中的avail_event

a. 由后端驱动(Host)设置,标识希望前端驱动(Guest)触发中断的阈值

b. 前端驱动(Guest)在向Avail Ring加入buffer后,检查Avail Ring的idx字段,只有达到阈值才触发中断

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

0

2.6 vring_virtqueue结构

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

0

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

0

3. virtio操作

如上文所述,virtio框架向虚拟机中的前端驱动提供了统一的IO操作接口,我们下面就分析这些操作

在理解了virtio的操作之后,配合不同虚拟设备的属性,就比较容易理解虚拟设备前端驱动的实现。比如virtio-net就是网卡驱动 + virtio操作

3.1 创建virtqueue

3.1.1 核心流程梳理(重点)

3.1.1.1 virtio_pci_probe阶段

在virtio框架中,首先向虚拟机注册的是一个pci_driver,后续所有vitio设备均是以虚拟pci设备的形式注册,因此第1步流程即是运行virtio_pci_probe函数

0

3.1.1.2 virtio_pci_modern_probe阶段

在virtio_pci_probe函数中,会调用virtio_pci_modern_probe函数,进行进一步初始化

0

在virtio_pci_modern_probe函数中会进行2步非常重要的操作,

0

① 设置virtio_device中的virtio_config_ops回调函数集合,其中就包括了最重要的find_vqs回调函数

② 设置setup_vq回调函数,该回调函数会在find_vqs回调函数中被调用

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

3.1.1.3 find_vqs回调函数阶段

0

在virtio_pci_probe的最后阶段,会注册virtio_device,该操作会触发virtio驱动的probe函数被调用,在该函数中,会触发find_vqs回调函数被调用

下面我们以virtio-net前端驱动为例,说明创建virtqueue的流程


  
  
  1. virtio_pci_probe
  2. --> virtio_pci_modern_probe
  3. // 设置find_vqs回调函数
  4. // 设置setup_vq回调函数
  5. --> register_virtio_device // 触发virtio probe函数被调用
  6. // virtio probe函数阶段
  7. virtio_dev_probe
  8. // 调用virtio_driver注册的probe函数
  9. --> virtnet_probe
  10. --> init_vqs
  11. // 分配virtqueue结构(send_queue & recv_queue)
  12. --> virtnet_alloc_queues
  13. --> virtnent_find_vqs
  14. --> find_vqs回调函数(vp_modern_find_vqs)
  15. --> vp_find_vqs
  16. --> vp_find_vqs_msix // 还注册了vring_interrupt
  17. --> vp_setup_vq
  18. --> setup_vq回调函数(setup_vq)
  19. --> vring_create_virtqueue
  20. // 分配存储vring的连续物理内存
  21. --> vring_alloc_queue
  22. // 生成vring_virtqueue结构,并初始化
  23. --> __vring_new_virtqueue

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

3.1.2 setup_vq函数分析

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

3.1.2.1 检查virtqueue配置参数

0

前端驱动读取PCI配置空间中的参数,判断其合法性

其中要注意virtqueue的长度(queue_size)必须是2的幂次,因为后续需要通过简单的位与运算实现绕回

3.1.2.2 实际生成virtqueue

0

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

① 对齐要求

如上文所述,vring结构在内存布局上有对齐要求,该要求在创建virtqueue时传递,就是此处的SMP_CACHE_BYTES宏

0

② notify hook函数

notify hook函数用于前端驱动(Guest)触发后端驱动(Host)中断,通知其有数据需要处理

0

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

0

③ callback hook函数

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

0

3.1.2.3 同步GPA到宿主机

0

virtqueue作为前端驱动与后端驱动的交互媒介,需要在虚拟机和宿主机中同步这段共享内存的地址

调用vring_create_vritqueue函数生成的virtqueue,分配的内存为GPA,需要将其同步到宿主机,宿主机才能将其转换为HVA使用(因为虚拟机的GPA就是宿主机分配的)

3.1.3 vring_create_virtqueue函数分析

0

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

3.1.4 __vring_new_virtqueue函数分析

0

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

3.2 前端驱动发送数据

3.2.1 流程概要

step 1:从descriptor table中取出描述符

step 2:根据要发送的数据填充并组织描述符

step 3:将组织好的描述符加入avail ring

step 4:触发后端驱动中断,通知其处理数据

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

3.2.2 virtqueu_add函数分析

前端驱动发送数据的核心为virtqueue_add函数,下面给出该函数的分析

0

0

0

0

0

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

0

说明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种方式供前端驱动调用,

① virtqueue_add_sgs

可以同时提交out & in数据请求,且个数可设置

0

② virtqueue_add_outbuf

只提交一个out数据请求

0

③ virtqueue_add_inbuf

只提交一个in数据请求

0

④ virtqueue_add_inbuf_ctx

只提交一个in数据请求,且携带上下文信息

注意:ctx上下文信息与inderect特性是互斥的

0

3.3 前端驱动触发中断

前端驱动通过virtqueue_kick函数通知后端驱动有数据需要处理

0

其中virqueue_kick_prepare函数判断是否需要触发中断,virtqueue_notify函数实际触发中断

3.3.1 virtqueue_kick_prepare函数分析

0

说明:vring_need_event函数实现

0

只有当event_idx在[old, new - 1]范围时,才会允许触发中断

3.3.2 virtqueue_notify函数分析

0

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

0

3.4 前端驱动被触发中断

3.4.1 注册中断处理函数

在创建virtqueue时,会为每条virtqueue注册中断,可参考vp_find_vqs_msix函数

0

可见中断处理函数为vring_interrupt,注意这里注册的是msi中断

3.4.2 vring_interrupt函数分析

0

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

0

3.5 前端驱动接收数据

3.5.1 virtqueue_get_buf_ctx函数分析

0

0

说明: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成员的使用

3.5.2.1 在vring_virtqueue结构中定义

如上文所述,在vring_virtqueue结构中定义了desc_state数组,根据注释,该结构描述了每个描述符的状态(更好的理解是每个描述符有一个)

0

数组大小为virtqueue大小,空间随vring_virtqueue结构一同分配,该数组用于存储每次数据传输请求的上下文

0

vring_desc_state结构如下,

0

我们这里就是讨论其中data成员的使用

3.5.2.2 在virtqueue_add函数中设置

0

这里注意2点,

① 填入data的值

此处填入的值为virtqueue_add函数的入参data

② 填入desc_state数组的下标

此处使用的下标为head,为本次数据请求的chain descriptor的首个描述符下标

3.5.2.3 在virtqueue_get_buf_ctx中读取

0

此处使用的下标i是used ring中取出的chain descriptor中首个描述符的下标,这里对应了一次vritqueue_add加入的数据请求

此处就将当时virtqueue_add函数写入的data作为返回值

说明:这里就可以看出virtio机制设计的巧妙之处,后端驱动在使用不同的chain descriptor后不需要按取出的顺序归还

这里有2点机制上的保障,

① descriptor table使用静态链表方式管理

② desc_state数组按描述符管理

3.5.3 detach_buf函数分析

0

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

0

4. virio-net前端驱动分析

4.1 重要数据结构

4.1.1 send_queue结构

0

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

4.1.2 receive_queue结构

0

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

说明:multiqueue virtio-net

virtio-net前端驱动支持multiqueue机制,也就是允许有多对send_queue & receive_queue,在virtnet_probe过程中会检查宿主机的设置,获取收发队列的对数

0

这样在创建virtqueue时,会根据配置项分配内存

0

但是在一般情况下,均使用1条send_queue + 1条receive_queue,且没有控制队列

4.1.3 virtnet_netdev callback数组

0

在Linux中,net_device结构描述了一个网络设备,其中的net_device_ops则包含了该网络设备的操作方法集

其中特别注意ndo_start_xmit callback函数,该函数为网卡发送报文时使用的函数

4.2 发送报文流程

4.2.1 到达start_xmit函数


  
  
  1. 内核协议栈
  2. dev_hard_start_xmit //net\core\dev.c
  3. xmit_one
  4. netdev_start_xmit //include/linux/netdevice.h
  5. __netdev_start_xmit
  6. ops-> ndo_start_xmit(skb, dev); 到virtio_net.c 中
  7. ||
  8. \/
  9. virtio_net.c中
  10. static const struct net_device_ops virtnet_netdev = {
  11. .ndo_start_xmit = start_xmit,
  12. start_xmit
  13. xmit_skb // 把skb放到vqueue中
  14. virtqueue_add_outbuf
  15. //把数据写到队列中
  16. virtqueue_add //virtio_ring.c
  17. virtqueue_add_split
  18. virtqueue_kick //virtio_ring.c
  19. ||
  20. \/
  21. virtqueue_kick
  22. virtqueue_notify
  23. vq-> notify(_vq) // agile_nic.c中notify函数,通知板卡驱动给队列中写数据了,然后板卡收到notify后,读取数据

① 虚拟机中的进程发送网络包时,仍然通过文件系统和socket调用网络协议栈到达网络设备层。只不过此时不是到达普通的网络设备,而是virtio-net前端驱动

② virtio-net前端驱动作为网卡设备驱动层,接收IP层传输下来的二层网络数据包

③ 发送网络包的流程最终将调用net_device_ops结构中的ndo_start_xmit回调函数,在virtio-net驱动中,就是start_xmit函数

0

4.2.2 start_xmit函数主要流程

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

① 调用xmit_skb函数将网络包写入virtqueue

0

② 触发后端驱动中断

0

virtqueue_kick函数在上文已有说明,此处说明一下xmit_skb函数的实现

4.2.3 xmit_skb函数

0

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

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

4.3 接收报文流程


  
  
  1. 数据接收流程:
  2. napi_gro_receive(&rq->napi, skb);
  3. netif_receive_skb
  4. __netif_receive_skb // 传输skb给网络层
  5. /\
  6. ||
  7. 驱动 virtio_net.c 中poll方法 napi_poll(n, &repoll); 即virtio_net.c 中 virtnet_poll()
  8. virtnet_poll
  9. virtnet_receive
  10. receive_buf // 接收到的数据转换成skb
  11. //根据接收类型XDP_PASS、XDP_TX等对 virtqueue 中的数据进行不同的处理
  12. skb = receive_mergeable(dev, vi, rq, buf, ctx, len, xdp_xmit,stats); or
  13. skb = receive_big(dev, vi, rq, buf, len, stats); or
  14. skb = receive_small(dev, vi, rq, buf, ctx, len, xdp_xmit, stats);
  15. napi_gro_receive(&rq->napi, skb); // 把skb上传到上层协议栈
  16. schedule_delayed_work //通过你延迟队列接收数据
  17. refill_work
  18. try_fill_recv (vi, rq, GFP_KERNEL);
  19. 如果检测到本次中断 receive 数据完成,则重新开启中断
  20. local_bh_enable //enable 软中断 等待下一次中断接收数据
  21. /\
  22. ||
  23. 中断下半步
  24. 执行软中断回调函数 net_rx_action(), 调用 virtio_net.c 中 virtnet_poll()
  25. /\
  26. ||
  27. 检查poll队列上是否有设备在等待轮询
  28. napi_schedule ->__napi_schedule -> list_add_tail(&napi->poll_list, &sd->poll_list); //把 NAPI 加入到本地cpu的 softnet_data 的 poll_list链表头
  29. __raise_softirq_irqoff(NET_RX_SOFTIRQ); // 调度收包软中断
  30. /\
  31. ||
  32. skb_recv_done //virtio_net.c 中 virtnet_find_vqs() 中,数据接收完成回调函数
  33. virtqueue_napi_schedule
  34. 调用 napi_schedule
  35. /\
  36. ||
  37. 每个vq 对应一个数据接收函数 vring_interrupt()
  38. vring_interrupt() //virtio_ring.c
  39. vq->vq. callback(&vq->vq); 即virtio_net.c 中 skb_recv_done
  40. /\
  41. ||
  42. 中断上半步
  43. pcie网卡发送数据给host时,会触发pci msix硬中断,然后host driver agile_nic.c 中执行回调函数vring_interrupt

4.3.1 NAPI接收网络包流程概述

① 传统的网络收包流程完全靠中断驱动,当网络包到达十分频繁时,就会频繁触发中断,进而影响系统的整体性能

② NAPI方式的核心就是当有数据包到达时,集中处理网络包,之后再去处理其他事情

③ NAPI的处理流程是,当一些网络包到达触发中断时,内核处理完这些网络包之后,主动轮询poll网卡,主动去接收到来的网络包。如果一直有,就一直处理,等处理告一段落再返回

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

说明:注册NAPI收包poll函数

在virtio-net前端驱动中,在probe过程中,会调用netif_napi_add函数注册收包poll函数

0

可见此处注册的函数为virtnet_poll

4.3.2 virtio中断处理函数 skb_recv_done

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

0

0

4.3.3 virtnet_poll函数分析

0

说明:virtnet_poll_cleantx函数分析

在接收数据报文之前,先调用了virtnet_poll_cleantx函数处理了send_queue

0

其中的核心为free_old_xmit_skbs函数,分析如下,

0

这里也很好地体现了vring_desc_state结构中data成员的使用,

① 前端驱动发送报文时,将含有报文的skb写入data成员,数据请求加入avail ring

② 后端驱动处理完数据请求后,将chain descriptor从avail ring加入used ring

③ 前端驱动在处理后端驱动已使用的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 !

0

可见try_fill_recv函数会将所有可用的描述符均加入receive_queue的avail ring,供后端驱动使用

我们分析add_recvbuf_small函数,另外两种情况需要后端驱动配置支持

0

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

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

ctx:实参为ctx,值为xdp_headroom

说明:try_fill_recv函数的调用时机

① 打开网卡时

0

其中调度vi->refill工作,也会导致try_fill_recv函数被调用

② 网卡restore时

0

③ 接收报文时,也就是接下来要分析的函数

0

4.3.5 virtnet_receive函数分析

0

① 调用virtqueue_get_buf函数

将receive_queue中used ring的chain descriptor归还descriptor table,返回的buf就是上文分析的分配的内存的GVA,该地址在虚拟机中可以使用

② 调用receive_buf函数接收报文数据

0

至此,virtio-net前端驱动接收报文的工作就结束后,后续就是虚拟机Linux内核网络协议栈的工作了

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

5.1 scatterlist 实现分析

5.1.1 scatterlist 产生背景

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

0

0

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

① CPU通过MMU以虚拟地址(VA)访问内存

② DMA直接以物理地址(PA)访问内存

③ Device通过自己的IOMMU以设备地址(DA)访问内存

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

而scatterlist就是用户汇总这些不连续的物理内存块的方式

5.1.2 scatterlist结构

0

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

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

要组织的连续物理内存超过一页是常态,所以单个scatterlist结构是没啥实际用途的。在实际使用中,Linux内核默认将scatterlist组织为数组使用

在virtio-net前端驱动中,收发队列中均包含了scatterlist数组

0

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

0

说明2:page_link中bit1的作用

page_link中的bit1是数组有效成员终止位,因为一次传输不一定使用scatterlist数组的所有成员,因此需要对最后一个有效的成员进行标记

下图中,一个scatterlist数组有6个成员,但是本次传输只使用其中3个

0

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

0

0

说明3:page_link中bit0的作用

page_link中的bit0是sacatterlist数组链接标志,用于实现将2个scatterlist数组链接起来。如果bit0置1,则该page_link指向的不是一个page结构,而是指向另一个scatterlist数组

0

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

0

0

可见如果需要链接2个scatterlist数组,前一个数组的最后一个成员不能指向有效page

看到这里,就更容易理解之前分析的virtqueue_add函数

5.1.3 scatterlist常用API

5.1.3.1 sg_init_table

0

5.1.3.2 sg_assign_page

0

sg_assign_page函数将一个page与一个scatterlist关联起来

5.1.3.3 sg_set_page

0

sg_set_page在关联page的基础上,设置了内存块的偏移量与长度

5.1.3.4 sg_set_buf

0

sg_set_buf函数是最常用的关联内存块与scatterlist的API,此处传入的buf参数为内存块起始的虚拟地址

5.1.3.5 sg_init_one

0

sg_init_one用于初始化一个scatterlist结构,并与一个内存块关联(该内存块必须在1个page内)

5.1.3.6 sg_page

0

sg_page返回与scatterlist关联的物理页面地址

5.1.3.7 sg_next

0

sg_next用于取出scatterlist数组中的下一个成员,如果达到终止成员,则返回NULL

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

5.2.1 将sk_buff关联到scatterlist数组

0

这里的核心是skb_to_sgvec函数,该函数用于将sk_buff中存储报文用的各个page关联到scatterlist数组,下面分析该函数

0

在__skb_to_sgvec函数中,将sk_buff的逐个分片都关联到scatterlist数组中

0

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

0

0

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

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

备忘录:

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

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

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

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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值