Linux Kernel 之九 详解 virtio-net 源码框架、执行流程
virtio 表示虚拟化 IO,用于实现设备半虚拟化,即虚拟机中运行的操作系统需要加载特殊的驱动(e.g. virtio-net)且虚拟机知道自己是虚拟机
基于 virtio 的半虚拟化概述
virtio运行结构
-
virtio 表示虚拟化 IO,用于实现设备半虚拟化,即虚拟机中运行的操作系统需要加载特殊的驱动(e.g. virtio-net)且虚拟机知道自己是虚拟机
相较于基于完全模拟的全虚拟化,基于virtio的半虚拟化可以提升设备访问性能
-
运行在虚拟机中的部分称为前端驱动,负责对虚拟机提供统一的接口
-
运行在宿主机中的部分称为后端驱动,负责适配不同的物理硬件设备
virtio 架构层次
virtio 前端驱动
- 运行在虚拟机中
- 针对不同类型的设备有不同的驱动程序,但是与后端驱动交互的接口都是统一的
- 本文分析 virtio-net 模块,源码位于
drivers/net/virtio_net.c
virtio 层
- virtio 层实现虚拟队列接口,作为前后端通信的桥梁
- 不同类型的设备使用的虚拟队列数量不同,比如virtio-net使用两个队列,一个用于接收,另一个用于发送
- 源码位于
drivers/virtio/virtio.c
virtio-ring 层
- virtio-ring 层是虚拟队列的具体实现
- 源码位于
driver/virtio/virtio_ring.c
virtio 后端驱动
- 运行在宿主机中
- 实现 virtio 后端的逻辑,主要是操作硬件设备,比如向内核协议栈发送一个网络包完成虚拟机对于网络的操作
- 在 Qemu + KVM 虚拟化环境中,源码位于 Qemu 源码中(尚未详细分析)。后续将分析 seL4 中的后端实现
Linux virtio 核心数据结构
virtio_bus 结构
struct bus_type
是基于总线驱动模型的公共数据结构,定义新的 bus,就是填充该结构。virtio_bus 定义在 drivers/virtio/virtio.c
中,具体如下,
说明1:注册 virtio_bus
virtio_bus 以 core_initcall 的方式被注册,该方式注册的启动顺序优先级很高(作为对比,module_init 最终对应的是 device_initcall),在使用中要注意不同组件的启动顺序
说明2:virtio_dev_match 函数
virtio 驱动的 match 涉及到 virtio_device_id 结构,在 virtio_device 结构中包含该结构;在 virtio_driver 中则是包含该驱动支持的virtio_device_id 列表
具体的 match 流程如下,
可见 virtio 驱动的 match 函数先匹配 device 字段,后匹配 vendor 字段,二者都满足条件时 match 成功
补充:从 virtio_dev_match 的流程可以看出,virtio_driver 中的 id_table 必须以 id->device = 0 结尾,以便结束循环
virtio_device 结构
struct virtio_device
定义在 include/linux/virtio.h
中,具体如下,
struct virtio_device_id id
其中的 device 成员标识了当前 virtio_device 的用途,virtio-net 是其中的一种,
const struct virtio_config_ops *config
virtio_config_ops 操作集中的函数主要与 virtio_device 的配置相关,主要有如下 2 类操作,
- 实例化 / 反实例化 virtqueue,其中要特别注意 find_vqs 函数,该函数用于实例化 virtio_device 所持有的 virtqueue
- 获取 / 设置 virtio_device 的属性与状态,相关属性均在虚拟机虚拟出的 PCI 配置空间
struct list_head vqs
virtio_device 持有的 virtqueue 链表,virtio-net 中建立了 2 条 virtqueue(虚拟队列)
u64 features
virtio_driver & virtio_device 同时支持的通信特性,也就是前后端最终协商的通信特性
virtio_driver 结构
struct virtio_driver
定义在 include/linux/virtio.h
中,具体如下,
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 函数
virtqueue 结构
struct virtqueue
定义在 include/linux/virtio.h
中,具体如下,
vring 结构
struct virtqueue
定义在 include/uapi/linux/virtio_ring.h
中,具体如下,
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 存储结构的布局
实际 vring 的内存布局如下图所示,
在计算 used ring 的起始地址时,在 avail->ring[num]
的地址之后又加了 sizeof(__virtio16)
,也就是增加了 2B,是为了容纳 avail ring 末尾的used_event,该机制详见下文(是一种控制中断触发频率的机制)
说明3:实际 vring 的大小
实际 vring 的大小可以通过 vring_size 函数获得
- 计算 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
- 由前端驱动(Geust)设置,标识希望后端驱动(Host)触发中断的阈值
- 后端驱动(Host)在向 Used Ring 加入 buffer 后,检查 Used Ring 中的 idx 字段,只有达到阈值才触发中断
-
used_ring 中的 avail_event
- 由后端驱动(Host)设置,标识希望前端驱动(Guest)触发中断的阈值
- 前端驱动(Guest)在向 Avail Ring 加入 buffer 后,检查 Avail Ring 的 idx 字段,只有达到阈值才触发中断
综上所属,vring 结构的构成如下图所示,
vring_virtqueue结构
vring_virtqueue 结构用于描述前端驱动(Guest)中的一条虚拟队列
总结:virtio_device / vring_virtqueue / virtqueue / vring结构之间的关系
virtio 操作
如上文所述,virtio 框架向虚拟机中的前端驱动提供了统一的 IO 操作接口,我们下面就分析这些操作。在理解了 virtio 的操作之后,配合不同虚拟设备的属性,就比较容易理解虚拟设备前端驱动的实现。比如 virtio-net 就是网卡驱动 + virtio 操作
创建 virtqueue
核心流程梳理(重点)
virtio_pci_probe阶段
在 virtio 框架中,首先向虚拟机注册的是一个 pci_driver,后续所有 vitio 设备均是以虚拟 pci 设备的形式注册,因此第 1 步流程即是运行virtio_pci_probe 函数
virtio_pci_modern_probe 阶段
在 virtio_pci_probe 函数中,会调用 virtio_pci_modern_probe 函数,进行进一步初始化
在 virtio_pci_modern_probe 函数中会进行 2 步非常重要的操作,
- 设置 virtio_device 中的 virtio_config_ops 回调函数集合,其中就包括了最重要的 find_vqs 回调函数
- 设置 setup_vq 回调函数,该回调函数会在 find_vqs 回调函数中被调用
所以在初始化 virtqueue 的过程中,是先调用 find_vqs 回调函数,后调用 setup_vq 回调函数
find_vqs 回调函数阶段
在 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
下面我们就分析最核心的几个函数
setup_vq 函数分析
setup_vq 函数有如下 3 个核心步骤,
检查 virtqueue 配置参数
前端驱动读取 PCI 配置空间中的参数,判断其合法性。其中要注意 virtqueue 的长度(queue_size)必须是 2 的幂次,因为后续需要通过简单的位与运算实现绕回
实际生成 virtqueue
实际生成 virtqueue 通过 vring_create_virtqueue 函数实现,此处需要注意如下 3 点,
- 对齐要求。如上文所述,vring 结构在内存布局上有对齐要求,该要求在创建 virtqueue 时传递,就是此处的 SMP_CACHE_BYTES 宏
- notify hook 函数。用于前端驱动(Guest)触发后端驱动(Host)中断,通知其有数据需要处理
这里的 notification register 也在 PCI 配置空间中,该地址在 setup_vq 函数中指定
- callback hook 函数。callback hook函数在 virtqueue 被触发时调用,以 virtio-net 驱动为例,callback hoot 函数在 virtnet_find_vqs 函数中指定
同步 GPA 到宿主机
virtqueue 作为前端驱动与后端驱动的交互媒介,需要在虚拟机和宿主机中同步这段共享内存的地址。调用 vring_create_vritqueue 函数生成的 virtqueue,分配的内存为 GPA,需要将其同步到宿主机,宿主机才能将其转换为 HVA 使用(因为虚拟机的 GPA 就是宿主机分配的)
vring_create_virtqueue 函数分析
可见 vring 的内存被分配在连续的 1 个或多个 page 中,而且如果内存条件不满足,会动态调整 vring 的长度(num 变量)
__vring_new_virtqueue 函数分析
__vring_new_virtqueue 函数的实现注释已经比较清楚了,需要说明的是,该函数返回的是 virtqueue 结构。在 Linux 的 virtio 层实现中,代码会根据需要在 virtqueue 与 vring_virtqueue 结构间进行转换
前端驱动发送数据
流程概要
-
从 descriptor table 中取出描述符
-
根据要发送的数据填充并组织描述符
-
将组织好的描述符加入 avail ring
-
触发后端驱动中断,通知其处理数据
说明:vring 的描述符结构与 scatterlist 结构是绝配
virtqueu_add 函数分析
前端驱动发送数据的核心为 virtqueue_add 函数,下面给出该函数的分析
上面的截图很壮观,下面通过一张图展现该过程,
-
说明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数据请求,且个数可设置
- virtqueue_add_outbuf 只提交一个 out 数据请求
- virtqueue_add_inbuf 只提交一个 in 数据请求
- virtqueue_add_inbuf_ctx 只提交一个 in 数据请求,且携带上下文信息。注意:ctx上下文信息与 inderect 特性是互斥的
- virtqueue_add_sgs 可以同时提交out & in数据请求,且个数可设置
前端驱动触发中断
前端驱动通过 virtqueue_kick 函数通知后端驱动有数据需要处理
其中 virqueue_kick_prepare 函数判断是否需要触发中断,virtqueue_notify 函数实际触发中断
virtqueue_kick_prepare 函数分析
说明:vring_need_event 函数实现
只有当 event_idx 在 [old, new - 1 ]范围时,才会允许触发中断
virtqueue_notify 函数分析
virtqueue_notify 会调用上文介绍的 notify 回调函数,实现对后端驱动的通知,在本文环境中,该回调函数为 vp_notify 函数
前端驱动被触发中断
注册中断处理函数
在创建 virtqueue 时,会为每条 virtqueue 注册中断,可参考 vp_find_vqs_msix 函数
可见中断处理函数为 vring_interrupt,注意这里注册的是 msi 中断
vring_interrupt 函数分析
vring_interrupt 函数的核心操作是调用创建 virtqueue 时注册的 callback 回调函数,以 virtio-net 模块为例,接收和发送队列注册的 callback回调函数如下
前端驱动接收数据
virtqueue_get_buf_ctx 函数分析
说明:virtqueue_get_buf_ctx 函数的返回值为 vq->desc_state[i].data,该值在调用 virtqueue_add 时设置。virtqueue_add 写入该值,目的就是用于索引 buffer(the token identifying the buffer)
补充:对 vring_desc_state desc_state 结构中 data 成员的使用
在 vring_virtqueue 结构中定义
如上文所述,在 vring_virtqueue 结构中定义了 desc_state 数组,根据注释,该结构描述了每个描述符的状态(更好的理解是每个描述符有一个)
数组大小为 virtqueue 大小,空间随 vring_virtqueue 结构一同分配,该数组用于存储每次数据传输请求的上下文
vring_desc_state 结构如下,
我们这里就是讨论其中 data 成员的使用
在 virtqueue_add 函数中设置
这里注意 2 点,
-
填入 data 的值。此处填入的值为 virtqueue_add 函数的入参 data
-
填入 desc_state 数组的下标。此处使用的下标为 head,为本次数据请求的 chain descriptor 的首个描述符下标
在 virtqueue_get_buf_ctx 中读取
此处使用的下标 i 是 used ring 中取出的 chain descriptor 中首个描述符的下标,这里对应了一次 vritqueue_add 加入的数据请求。此处就将当时 virtqueue_add 函数写入的 data 作为返回值
说明:这里就可以看出 virtio 机制设计的巧妙之处,后端驱动在使用不同的 chain descriptor 后不需要按取出的顺序归还。这里有 2 点机制上的保障,
- descriptor table 使用静态链表方式管理
- desc_state 数组按描述符管理
detach_buf 函数分析
最终给出一张图,就是虚拟机和宿主机指向同一段内存,以实现二者之间的交互
virio-net 前端驱动分析
重要数据结构
send_queue 结构
send_queue 结构是对 virtqueue 的封装,是 virtio-net 的发送队列,即数据流向从前端驱动(Guest)到后端驱动(Host)
receive_queue 结构
receive_queue 结构也是对 virtqueue 的封装,是 virtio-net 的接收队列,即数据流向从后端驱动(Host)到前端驱动(Guest)
说明:multiqueue virtio-net
virtio-net 前端驱动支持 multiqueue 机制,也就是允许有多对 send_queue & receive_queue,在 virtnet_probe 过程中会检查宿主机的设置,获取收发队列的对数
这样在创建 virtqueue 时,会根据配置项分配内存
但是在一般情况下,均使用 1 条 send_queue + 1 条 receive_queue,且没有控制队列
virtnet_netdev callback数组
在 Linux 中,net_device 结构描述了一个网络设备,其中的 net_device_ops 则包含了该网络设备的操作方法集。其中特别注意 ndo_start_xmit callback 函数,该函数为网卡发送报文时使用的函数
发送报文流程
到达 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后,读取数据
- 虚拟机中的进程发送网络包时,仍然通过文件系统和 socket 调用网络协议栈到达网络设备层。只不过此时不是到达普通的网络设备,而是 virtio-net 前端驱动
- virtio-net 前端驱动作为网卡设备驱动层,接收 IP 层传输下来的二层网络数据包
- 发送网络包的流程最终将调用 net_device_ops 结构中的 ndo_start_xmit 回调函数,在 virtio-net 驱动中,就是 start_xmit 函数
start_xmit 函数主要流程
与 virtio 框架相关的只有 2 个步骤,
- 调用 xmit_skb 函数将网络包写入 virtqueue
- 触发后端驱动中断
virtqueue_kick 函数在上文已有说明,此处说明一下 xmit_skb 函数的实现
xmit_skb 函数
xmit_skb 函数将 sk_buff 映射到 scatterlist 中,之后调用 virtqueue_add_outbuf 函数将数据请求加入 send_queue 的 avail ring
说明:这里传递给 data 的值为 skb,也就是要发送的 skb 的地址。注意,skb 的地址值是一个 GVA(Guest Virtual Address),因此只在虚拟机中使用
接收报文流程
数据接收流程:
数据接收流程:
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
NAPI 接收网络包流程概述
- 传统的网络收包流程完全靠中断驱动,当网络包到达十分频繁时,就会频繁触发中断,进而影响系统的整体性能
- NAPI 方式的核心就是当有数据包到达时,集中处理网络包,之后再去处理其他事情
- NAPI的处理流程是,当一些网络包到达触发中断时,内核处理完这些网络包之后,主动轮询 poll 网卡,主动去接收到来的网络包。如果一直有,就一直处理,等处理告一段落再返回
当再有下一批网络包到达时,再中断,再轮询 poll。这样就会大大减少中断的数量,提升网络处理的效率
说明:注册 NAPI 收包 poll 函数。在 virtio-net 前端驱动中,在 probe 过程中,会调用 netif_napi_add 函数注册收包 poll 函数
可见此处注册的函数为 virtnet_poll
virtio 中断处理函数 skb_recv_done
如上文所述,virtqueue 的中断处理函数最终会调用到创建 virtqueue 时注册的 callback 回调函数,该函数为 skb_recv_done,这也就是virtio-net 前端驱动的收包中断顶半部操作
virtnet_poll 函数分析
说明:virtnet_poll_cleantx 函数分析
在接收数据报文之前,先调用了virtnet_poll_cleantx函数处理了send_queue
其中的核心为 free_old_xmit_skbs 函数,分析如下,
这里也很好地体现了 vring_desc_state 结构中 data 成员的使用,
- 前端驱动发送报文时,将含有报文的skb写入data成员,数据请求加入avail ring
- 后端驱动处理完数据请求后,将chain descriptor从avail ring加入used ring
- 前端驱动在处理后端驱动已使用的chain descriptor时,从data成员中取出skb地址,并释放sk_buffer
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
!
可见 try_fill_recv 函数会将所有可用的描述符均加入receive_queue 的 avail ring,供后端驱动使用。我们分析 add_recvbuf_small 函数,另外两种情况需要后端驱动配置支持
这里需要注意调用 virtqueue_add_inbuf_ctx 的 2 个参数,因为后续的接收报文流程会使用
- data:实参为 buf,即分配的内存页面的 GVA
- ctx:实参为 ctx,值为 xdp_headroom
说明:try_fill_recv 函数的调用时机
-
打开网卡时
其中调度 vi->refill 工作,也会导致 try_fill_recv 函数被调用 -
网卡 restore 时
-
接收报文时,也就是接下来要分析的函数
virtnet_receive 函数分析
-
调用 virtqueue_get_buf 函数将 receive_queue 中 used ring 的 chain descriptor 归还 descriptor table,返回的 buf 就是上文分析的分配的内存的 GVA,该地址在虚拟机中可以使用
-
调用 receive_buf 函数接收报文数据
至此,virtio-net 前端驱动接收报文的工作就结束后,后续就是虚拟机 Linux 内核网络协议栈的工作了
Linux virtio-net 中对内存的使用
scatterlist 实现分析
scatterlist 产生背景
scatterlist 用于汇总分散的物理内存(以页为单位),并以数组的形式组织起来,典型的应用场景如下图所示,
在一个系统中,CPU、DMA 和 Device 通过不同的方式使用内存,
- CPU通过MMU以虚拟地址(VA)访问内存
- DMA直接以物理地址(PA)访问内存
- Device 通过自己的 IOMMU 以设备地址(DA)访问内存
如果访问的内存虚拟地址连续但是物理地址不连续,CPU 的访问没有问题,但是当需要将内存地址交给 DMA 进行传输时,只能以不连续的物理内存块的方式传递。而 scatterlist 就是用户汇总这些不连续的物理内存块的方式
scatterlist 结构
scatterlist 以 page 为单位,描述了一个物理地址连续的内存块
说明1:如果要组织的连续物理内存超过一页怎么办 ?
要组织的连续物理内存超过一页是常态,所以单个 scatterlist 结构是没啥实际用途的。在实际使用中,Linux 内核默认将 scatterlist 组织为数组使用。在 virtio-net 前端驱动中,收发队列中均包含了 scatterlist 数组
需要注意的是,这里 scatterlist 数组的大小与 sk_buff 中分片的个数是匹配的,这里增加的 2 个 scatterlist 分别用于存放 sk_buff 的线性数据部分和 virtio-net 的头部信息,可以参考下图理解
说明2:page_link 中 bit1 的作用
page_link 中的 bit1 是数组有效成员终止位,因为一次传输不一定使用 scatterlist 数组的所有成员,因此需要对最后一个有效的成员进行标记。下图中,一个 scatterlist 数组有 6 个成员,但是本次传输只使用其中 3 个
Linux 内核代码中通过如下接口设置 & 检查该标志位
说明3:page_link 中 bit0 的作用
page_link 中的 bit0 是 sacatterlist 数组链接标志,用于实现将 2 个 scatterlist 数组链接起来。如果 bit0 置 1,则该 page_link 指向的不是一个 page 结构,而是指向另一个 scatterlist 数组
Linux 内核代码中通过如下接口设置 & 检查该标志位
可见如果需要链接 2 个 scatterlist 数组,前一个数组的最后一个成员不能指向有效 page。看到这里,就更容易理解之前分析的virtqueue_add 函数
scatterlist 常用 API
sg_init_table
sg_assign_page
sg_assign_page 函数将一个 page 与一个 scatterlist 关联起来
sg_set_page
sg_set_page 在关联 page 的基础上,设置了内存块的偏移量与长度
sg_set_buf
sg_set_buf 函数是最常用的关联内存块与 scatterlist 的 API,此处传入的 buf 参数为内存块起始的虚拟地址
sg_init_one
sg_init_one 用于初始化一个 scatterlist 结构,并与一个内存块关联(该内存块必须在 1 个 page 内)
sg_page
sg_page 返回与 scatterlist 关联的物理页面地址
sg_next
sg_next 用于取出 scatterlist 数组中的下一个成员,如果达到终止成员,则返回 NULL
virtio-net 发送数据中的内存操作
将 sk_buff 关联到 scatterlist 数组
这里的核心是 skb_to_sgvec 函数,该函数用于将 sk_buff 中存储报文用的各个 page 关联到 scatterlist 数组,下面分析该函数
在 __skb_to_sgvec 函数中,将 sk_buff 的逐个分片都关联到 scatterlist 数组中
将 scatterlist 数组映射到 vring 描述符
这里其实就回到了我们之前分析的 virtqueue_add 函数
virtio-net 接收数据中的内存操作
备忘录:
topic 2:seL4中如何对接virtio-net
topic 3:virtio-net的上下游模块
topic 4:宿主机如何注册pci device,可以先分析qemu的实现思路
topic 5:SKB buffer的使用(这个属于网络相关知识点的补强)
更多推荐
所有评论(0)