DPDK用户态驱动解析
参考代码:dpdk-20.11
参考文档:
《深入浅出DPDK》
《linux_gsg-20.11》
《nics-20.11》
一、 DPDK简介
Intel DPDK 框架是一种增强的NIC驱动框架,提供一种快速的简单的轮训模式的数据 包接收处理转发的功能,并且这些动作以用户态下的应用程序的方式存在,独立于Linux内 核。这种方式,对网卡实时性能具有较高要求,以及快速处理数据包要求较高的应用具有 很好优越性和可扩展性。同时,DPDK框架也为网卡驱动开发提供了一个基础框架以及精简 优化后的C库,为新的网卡驱动开发提供了基本的平台。
二、 用户太网卡驱动的设计与实现
基于DPDK框架下的网卡驱动,需要依据DPDK的模式去开发驱动。基本的原如图 所示:
在内核态下,驱动需要将网卡注册到UIO驱动框架中,这个驱动是DPDK框架下和内 核通信的基本接口之一。igb_uio驱动类属于UIO驱动框架之下,uio将注册到其框架下的设 备并加入到设备链表中,创建并初始化设备,同时初始化PCI相关的功能以及sysfs的基本 信息的建立。
在用户态下,需要将PCI注册到DPDK的管理框架之下,同时兼容其他类型的网卡,完成 EAL层的适配及初始化等动作。
1 网卡的驱动的实现
1.1 IGB_UIO
内核部分主要是将此PCI的设备注册到UIO驱动框架之下,之后UIO的框架会将此设备在 内核部分使能起来,同时建立基本的sysfs系统,以及UIO驱动相关的接口注册,同时建立 起基本的映射关系。驱动部分需要做的事情就是将基本的网卡信息以内核要求的形式注册 到内核即可,基本的信息如下数据结构所示:
驱动首先找到此设备的内部寄存器BAR0空间的物理基地址,之后将物理地址、名称以及 BAR0空间的长度传入到UIO设备的描述符中,完成此设备在内核部分的注册。
注:之前遇到的设备收发口对应的dma不同的情况,可以在上图的位置追加一个bar空间。使用DMA机制用于实现 设备对外部接口的读写操作。用户态程序要求获取并使用此DMA区域的地址,此驱动在 设计时,使用了以下方法 : 通常每个 BAR空间对应到sysfs下的一个路径,比如 /sys/class/uio/uio0/maps/map0/。在mapX/路径下,有以下几个只读文件用于指定此BAR 空间的属性: name addr size offset 内核驱动在mapX/目录下增加了一个只读文件 phys,用来保存成功分配的DMA内存区域 基地址。这样,用户态程序可以通过读取/sys/class/uio/uioX/maps/mapX/phys文件的内 容而获取到DMA基地址。 之前遇到的设备在PCI域只有BAR0一个地址空间,因此,内核驱动在UIO初始化时,虚拟增 加了一个地址空间BAR4,并将DMA基地址保存到BAR4对应的sysfs文件系统目录: /sys/class/uio/uio0/maps/map4/phys.两个BAR空间分别对应着读写接口
1.1.1 igb_uio的启动流程
Igb_uio内核模块主要做的事情是注册一个pci设备。在执行insmod igb_uio.ko操作时,会在/sys/目录下创建igb_uio相应的目录,但不会调用probe()来探测pci设备,原因是id_table为NULL,找不到设备列表(在这里说一下,如果想在加载insmod时自动加载网卡,可以将设备列表补充进去)
Igb_uio的probe执行,实际上是在执行python脚本dpdk_nic_bind.py的时候完成的。此脚本的主要工作是:
- 将pcie号写入/sys/bus/pci/drivers/igb/unbind。将网卡从原有驱动中去绑定。
- 将vendor deviceid 写入igb_uio驱动的new_id文件。写入后会调用igb_uio的probe函数。
1.1.2 pci设备的探测
略,此部分为pci设备的使能
1.2 用户态驱动部分
用户态部分主要完成与DPDK框架的适配,如果是一些定制网卡则需要完成定制网卡内部寄存器读写功能的实现, 即EAL层的功能实现。
1.2.1 DPDK框架注册
2.1.2.1.1 scan()
网卡驱动框架注册实际是将此PCI设备加入到DPDK框架中的过程,此过程由DPDK框 架部分实现。DPDK层在初始化的时候会scan()所有sysfs下的PCI子设备,将注册在UIO设备下 的PCI设备扫描出来并建立相应的PCI的设备链表
2.1.2.1.2 probe()
以上代码功能将PCI设备BAR空间映射到用户态地址空间。例如,PCI设备05:00.0对 应的BAR0地址信息文件/sys/bus/pci/devices/0000:05:00.0/resource0,DPDK将此空间映 射到用户态,并保存到mem_resource[X].addr。然后用户进程内可以直接访问已保存的地 址,进行进村器读写操作。
函数调用关系:
2 DPDK收发包操作
收发包的操作参考basicfwd.c
收发包过程可以分为两部分
- 收发包的配置和初始化,主要是配置收发队列等。
- 数据包的接收和发送,主要是通过从队列中获取到数据包或者把数据包放到队列中。
2.1 收发包的配置与初始化
2.1.1收发包的配置
收发包的配置最主要的工作就是配置网卡的收发队列,设置DMA拷贝数据包的地址等,配置好地址后,网卡收到数据包后会通过DMA控制器直接把数据包拷贝到指定的内存地址。我们使用数据包时,只要从对应队列中取出指定地址的数据即可。
收发包的配置是从rte_eth_dev_configure()开始的,这里根据参数会配置队列的个数,以及接口的配置信息,如队列的使用模式,多队列的方式等。
一开始会进行各项检查,如果设备已经启动,就得先停下来才能配置。然后把传进去的配置参数拷贝到设备的数据区。
获取设备的配置信息,主要也是为了后面的检查使用:
dev_infos_get是在驱动初始化过程中设备初始化时配置的(eth_ixgbe_dev_init())
接收队列的配置,接收队列是从 eth_dev_rx_queue_config(dev, nb_rx_q);开始的(20.11的版本之前此接口为rte_eth_dev_rx_queue_config())
在接收配置中,考虑的是有两种情况,一种是第一次配置;另一种是重新配置。所以,代码中都做了区分。
(1)如果是第一次配置,那么就为每个队列分配一个指针。
(2)如果是重新配置,配置的queue数量不为0,那么就取消之前的配置,重新配置。
(3)如果是重新配置,但要求的queue为0,那么释放已有的配置。
这里要说明:
DPDK定义了一个rte_eth_devices数组,数组元素类型为struct rte_eth_dev,一个数组元素表示一块网卡。struct rte_eth_dev有四个重要的成员:rx/tx_pkt_burst、dev_ops、data,其中前两者分别是网卡的burst收/发包函数;dev_ops是网卡驱动注册的函数表,类型为struct eth_dev_ops;data包含了网卡的主要信息,类型为struct rte_eth_dev_data
发送队列同接收队列。
当收发队列配置完成后,就调用设备的配置函数,进行最后的配置。(*dev->dev_ops->dev_configure)(dev),我们找到对应的配置函数,进入ixgbe_dev_configure()来分析其过程,其实这个函数并没有做太多的事。
在函数中,先调用了ixgbe_check_mq_mode()来检查队列的模式。然后设置允许接收批量和向量的模式
2.1.2接收队列的初始化
接收队列的初始化rte_eth_rx_queue_setup()
这里的参数需要指定要初始化的port_id,queue_id,以及描述符的个数,还可以指定接收的配置,如释放和回写的阈值。
如果rx_conf为NULL,则会以缺省配置。
缺省配置:缺省配置接口ixgbe_dev_info_get()
Ixgbe_dev_rx_queue_setup指明队列结构体、描述符队列的空间、sw_ring
- 分配队列结构体,并填充结构。其中mp为申请的内存池
- 分配DMA描述符队列的空间,按照最大的描述符个数进行分配
接着获取描述符队列的头和尾寄存器的地址,在收发包后,软件要对这个寄存器进行处理。
设置队列的接收描述符ring的物理地址和虚拟地址。
- 分配sw_ring,这个ring中存储的对象是struct ixgbe_rx_entry,其实里面就是数据包mbuf的指针。
以上三步做完以后,新分配的队列结构体重要的部分就已经填充完了。
结构框图:
2.1.3发送队列的初始化
同接收队列初始化,略
3 设备的启动(涉及零拷贝建立mempool、queue、DMA、ring之间的关系)
设备启动函数为rte_eth_dev_start(),调用函数为:(*dev->dev_ops->dev_start)(dev);
此接口交代了零拷贝实现的方式。(以rx举例)
- 从队列所属内存池的ring中循环取出了nb_rx_desc个mbuf指针(这里可回想前面ring的创建是以缺省值创建),也就是为了填充rxq->sw_ring。每个指针都指向内存池里的一个数据包空间。
- 从内存池中mbuf,计算mbuf头指针。分别填充进rx_ring与sw_ring中。
2.3.1 tx启动与rx有所不同,它不需要配置队列,原因是在发送时将mbuf发送过去即可。不需要配置。
4 数据包的发送与接收
数据包的获取是指驱动把数据包放入了内存中,上层应用从队列中去取出这些数据包;发送是指把要发送的数据包放入到发送队列中,为实际发送做准备。
4.1 数据包的获取
获取数据包是从rte_eth_rx_burst()开始的。
这里的dev->rx_pkt_burst在驱动初始化的时候已经注册过了,对于ixgbe设备,就是ixgbe_recv_pkts()函数。
注意:
在收发包中,采用轮询方式其实检查的就是网卡的DD标志这个标志标识着一个描述符是否可用的情况:网卡在使用这个描述符前,先检查DD位是否为0,如果为0,那么就可以使用描述符,把数据拷贝到描述符指定的地址,之后DMA寄存器把DD标志位置为1,否则表示不能使用这个描述符。而对于驱动而言,恰恰相反,在读取数据包时,先检查DD位是否为1,如果为1,表示网卡已经把数据放到了内存中,可以读取,读取完后,再把DD位设置为0,否则,就表示没有数据包可读。
取值rx_id = rxq->rx_tail,这个值初始化时为0,用来标识当前ring的尾。然后循环读取请求数量的描述符,这时候第一步判断就是这个描述符是否可用
如果描述符的DD位不为1,则表明这个描述符网卡还没有准备好,也就是没有包!没有包,就跳出循环。
如果描述符准备好了,就取出对应的描述符,因为网卡已经把一些信息存到了描述符里,可以后面把这些信息填充到新分配的数据包里。
以上代码的主要功能是申请一个新的mbuf,替换DD标志为1的buf,并将DD置为0,
将有数据的buf存放到rx_pkts指针数组中,就是把包的指针返回给用户。
4.2数据包发送
略
5 总结:
DPDK 提供了一个用户态的高效数据包处理库函数,它通过环境抽象层、内核旁路协议栈、轮询模式的报文无中断收发、优化内存/缓冲区/队列管理、基于网卡多队列和流识别的负载均衡等多项技术,实现了在 x86 处理器架构下的高性能报文转发能力,用户可以在 Linux 用户态开发各类高速转发应用,也适合与各类商业化的数据平面加速解决方案进行集成。简而言之,DPDK 重载了网卡驱动,将数据包的控制平面和数据平面分离,驱动在收到数据包后不再硬中断通知 CPU,而是让数据包通过内核旁路的协议栈绕过了 Linux 内核协议栈,并通过零拷贝技术存入内存,这时应用层的程序就可以通过 DPDK 提供的接口读取数据包。