目录
前言
DPDK是intel工程师开发的一款用来快速处理数据包的框架,最初的目的是为了证明传统网络数据包处理性能低不是intel处理器导致的,而是传统数据的处理流程导致,后来随着dpdk的开源及其生态的快速发展,dpdk成为了高性能网络数据处理的优秀框架。本篇文章主要介绍DPDK接收与发送报文的流程,包括CPU与网卡DMA协同工作的整个交互流程、数据包在内存、CPU、网卡之间游走的过程。
背景
DPDK从2013开始开源,经过前辈们的缝缝补补到现在为止DPDK框架比较成熟、使用比较方便,使得现在开发者在不需要深入了解底层数据包收发原理的情况下也可以做简单的项目开发。但是个人感觉做简单的项目尚且可以应付,如果需要做性能优化/调优等类似的需求时就需要取全面的了解DPDK的收发包机制,因为收发包性能与驱动工作流程、前期初始化配置息息相关。
收发包整体流程
DPDK收发包是基础核心模块,从网卡收到包,到DMA把包拷贝到系统内存中;再到系统对这块数据包的处理。
整个DPDK收发包过程中实现了零拷贝(DMA将包从网卡的FIFO接收队列拷贝到内存以及从内存中将包拷贝到网卡的FIFO发送队列;CPU没有参与,零拷贝是说CPU没有参与拷贝)。
收包流程:
- 通过DMA将网卡数据拷贝至主存(RX buffer);
- 使用mmap()直接将RX buffer的数据映射到用户用户空间,使用户空间可以直接访问RX buffer的数据,以此实现了零拷贝。
整体流程图
- 数据包接收的大体流程:
数据包到达网卡
网卡经过DMA操作将数据包从网卡拷贝到收包队列(内存)
DPDK应用从收包队列中取包
- 数据包发送的大体流程:
DPDK应用将数据包送到发包队列
网卡经过DMA操作将网卡队列(内存)中的数据包拷贝到网卡
数据包从网卡发出
- 收发包流程中的关键操作,主要是网卡如何与DPDK应用交互:
- 网卡的初始化配置操作有哪些;
- 网卡的DMA操作怎么找到DMA 地址(数据包存放的硬件地址),进而将数据包拷贝到指定的硬件地址(内存)中,供DPDK读取;
- DMA把数据包成功放到接收队列后如何通知DPDK应用(PMD驱动)去队列中读取;
- DPDK应用((PMD驱动))从接收队列中取完数据包后,需要做哪些操作通知DMA为下一次存放包做准备;
- DPDK应用((PMD驱动))将数据包送到发送队列中,需要做哪些预操作;
- 网卡DMA从发送队列中取包需要做哪些操作;
DMA 讲解
DMA的背景、介绍
CPU 有转移数据、计算、控制程序等很多功能,但其实转移数据(尤其是转移大量数据)是可以不需要 CPU 参与。比如希望外设A 的数据拷贝到外设 B,只要给两种外设提供一条数据通路,再加上一些控制转移的部件就可以完成数据的拷贝。
DMA(Direct Memory Access)允许不同速度的硬件装置(比如网络IO「网卡」和内存之间,磁盘IO「磁盘」和内存之间)来沟通,而不需要依于 CPU 的大量中断负载,解决了数据转移过度消耗CPU资源的问题。
DMA是I/O设备直接存储器访问,几乎不消耗CPU的资源。在I/O设备和主存传递数据的时候,CPU可以处理其他事。DMA 是一种允许外围设备(硬件子系统)直接访问系统主内存的机制。也就是说,基于 DMA 访问方式,系统主内存和硬盘或网卡之间的数据传输可以绕开 CPU 的调度。
整个数据传输操作在一个 DMA 控制器(DMAC)的控制下进行的,CPU 除了在数据传输开始和结束时做一点处理外(开始和结束时候要做中断处理),在传输过程中 CPU 可以继续进行其他的工作。这样在大部分时间里,CPU 计算和 I/O 操作都处于并行操作,使整个计算机系统的效率大大提高。
参考:DMA介绍
I/O设备与主存信息传送的控制方式
I/O设备与主存信息传送的控制方式分为程序轮询、中断、DMA、RDMA等。
其中黄线代表程序轮询方式,绿线代表中断方式,红线代表DMA方式,黑线代表RDMA方式,蓝线代表公用的线。
可以看出DMA方式与程序轮询方式还有中断方式的区别是传输数据跳过了CPU,直接和主存交流。
“DMA接口” 就是一些实现DMA机制的硬件电路和相应的控制软件。
“DMA接口”有时也叫做“DMA控制器”(DMAC)。
在DMA方式下, DMA控制器(即DMA接口)也是需要和CPU交流的,但是图中没有显示DMA控制器与CPU交流信息。上诉图中主要是数据线;如果要严格一点,把控制线和地址线也画出来,如下所示。
新增细实线表示地址线,细虚线表示控制线。可以看出在中断方式下,无论是传输数据、地址还是控制信息,都要经过CPU,即都要在CPU的寄存器中暂存一下,都要浪费CPU的资源;但是在DMA方式下,传输数据和地址时,I/O设备可以通过“DMA接口”直接与主存交流,只有传输控制信息时,才需要用到CPU。而传输控制信息占用的时间是极小的,可以忽略不计,所以可以认为DMA方式完全没有占用CPU资源,这等价于I/O设备和CPU可以实现真正的并行工作,这比中断方式下的并行程度要更高很多。
- 三种方式的CPU工作效率比较
在I/O准备阶段,程序轮询方式的CPU一直在查询等待;
中断方式的CPU可以继续执行现行程序,但是当I/O准备就绪,设备向CPU发出中断请求,CPU响应以实现数据的传输,这个过程会占用CPU一段时间,而且这段时间比使用程序轮询方式的CPU传输数据的时间还要长,因为CPU除了传输数据还要做一些准备工作,如把CPU寄存器中的数据都转移到栈中。
DMA方式不一样,当I/O准备就绪,设备向CPU发出DMA请求,CPU响应请求,关闭对主存的控制器,只关闭一个或者几个存取周期,在这一小段时间内,主存和设备完成数据交换。而且在这一小段时间内,CPU并不是什么都不能做,虽然CPU不能访问主存,即不能取指令,但是CPU的cache中已经保存了一些指令,CPU可以先执行这些指令,只要这些指令不涉及访存,CPU和设备还是并行执行。数据传输完成后,DMA接口向CPU发出中断请求,让CPU做后续处理。大家可能会奇怪DMA接口为什么也能发出中断请求,其实DMA接口内有一个中断机构,见下图,DMA技术其实是建立在中断技术之上的,它包含了中断技术。
参见:LINUX网络子系统中DMA机制的实现
网卡中的DMA
-
网卡中DMA的硬件布局
每个网卡(MAC)都有自己的专用DMA Engine,如上图的 TSEC 和 e1000 网卡intel82546。
上图中的红色线就是以太网数据流,DMA与DDR打交道需要其他模块的协助,如TSEC,PCI controller
以太网数据在 TSEC<—>DDR 或者 PCI_Controller<-----> DDR 之间的流动,CPU的core是不需要介入的;只有在数据流动结束时(接收完、发送完),DMA Engine才会以外部中断的方式告诉CPU的core。 -
DMA Engine
系统启动时网卡(NIC)进行初始化,在内存中腾出空间给 Ring Buffer 。Ring Buffer 队列每个中的每个元素 Packet Descriptor指向一个sk_buff ,状态均为ready。
DMA 接口将网卡(NIC)接收的数据包(packet)逐个写入 sk_buff ,被写入数据的 sk_buff 变为 used 状态。一个数据包可能占用多个 sk_buff , sk_buff读写顺序遵循先入先出(FIFO)原则。
DMA 写完数据之后,网卡(NIC)向网卡中断控制器(NIC Interrupt Handler)触发硬件中断请求。
NIC driver 注册 poll 函数。
poll 函数对数据进行检查,例如将几个 sk_buff 合并,因为可能同一个数据可能被分散放在多个 sk_buff 中。
poll 函数将 sk_buff 交付上层网络栈处理。
后续处理:
poll 函数清理 sk_buff,清理 Ring Buffer 上的 Descriptor 将其指向新分配的 sk_buff 并将状态设置为 ready。
上诉是Linux内核中网卡收发包时DMA转移数据包的流程;以收包为例:
- 1> 在System memory中为DMA开辟一端连续空间,用作BD数组 (一致性dma内存);BD是给DMA Engine使用的。
注:不同的设备,BD结构不同,但是大致都有状态、长度、指针3个成员。- 2> 初始化BD数组,status为E,length为0;
- 3 > 在System memory中再开辟一块一块的内存,可以不连续,用来存放以太网包;将这些内存块的总线地址(硬件地址)赋给buffer pointer(dma映射)
- 4> 当MAC接收以太网数据流,放在了Rx FIFO中;
- 5> 当一个以太网包接收完全后,DMA engine依次做以下事情:
fetch bd:开始一个个的遍历BD数组,直到当前BD状态为Empty为止;
update bd:更新BD状态为Ready;
move data:把数据从Rx FIFO中搬移到System Memory中dma映射的硬件地址对应的内存中;
generate interrupt:数据搬移完了,产生外部中断给指定的 cpu core;
- 6> cpu core处理外部中断
此时以太网数据已经在System memory中dma映射的硬件地址所在内存中了;
然后解除dma映射,更新bd状态为Empty;再开辟一端内存,将这块内存的总线地址(硬件地址)赋给bd的指针字段(buffer pointer);
DPDK-APP收发包相关结构与API接口
main
//环境抽象层初始化,比如网卡,cpu,内存等
rte_eal_init(argc, argv);
//为rx和tx队列分配内存,将用户指定的配置信息dev_conf保存到dev
rte_eth_dev_configure(portid, 1, 1, &port_conf);
//分配网卡接收队列结构体,接收ring硬件描述符和软件ring等内存
rte_eth_rx_queue_setup(portid, 0, nb_rxd,
rte_eth_dev_socket_id(portid),
NULL,
l2fwd_pktmbuf_pool);
//分配网卡发送队列结构体,发送ring硬件描述符等内存
rte_eth_tx_queue_setup(portid, 0, nb_txd,
rte_eth_dev_socket_id(portid),
NULL);
//启动网卡,设置网卡寄存器,将网卡和系统内存关联起来
rte_eth_dev_start(portid);
while (1) {
//接收报文
rte_eth_rx_burst(portid, 0, pkts_burst, MAX_PKT_BURST);
//处理报文
//发送报文,此函数只是将报文放到一个buffer中,满32个后才调用rte_eth_tx_burst真正发送
rte_eth_tx_buffer(dst_port, 0, buffer, m);
}
以ixgbe驱动为例,相关的数据结构如下
硬件/模块介绍
- Network interface: 指以太网卡。
它工作在OSI的下两层(物理层、数据链路层),工作在物理层的芯片称为PHY,工作在数据链路层的芯片称为MAC控制器,即Media Access Control,即媒体访问控制子层协议.该协议位于OSI七层协议中数据链路层的下半部分,但是目前好多网卡是将MAC和PHY功能做到了一颗芯片中,但是MAC和PHY的机制还是单独存在的,只是对外体现为一颗芯片。MAC控制器的功能主要是数据帧的构建、数据差错检查、传送控制、向网络层提供标准的数据接口等功能;PHY芯片的主要功能是将从PHY来的并行数据转换为穿行流数据,再按照物理层的编码规则把数字信号进行编码,最后再转换为模拟信号把数据发出去。
- RX_FIFO: 数据接收缓冲区
- TX_FIFO: 数据发送缓冲区
- DMA Engine:Direct Memory Access,即直接内存访问。
是一种高速的数据传输方式,允许在外部设备和存储器(内存)之间直接读写数据,数据的读写不消耗CPU资源。
DMA控制器通过一组描述符环形队列(rx_ring),与CPU互相操作完成数据包的收发。CPU(PMD驱动/程序)通过操作DMA寄存器来与DMA控制器进行部分通信与初始化配置,主要寄存器有Base、Size、Tail、Head;
head寄存器用于DMA往rx_ring里插入时使用;
tail是应用(PMD驱动)通过写寄存器,通知给DMA控制器,告知当前可用空间的最后一个描述符的位置。
(head->next 为tail时表示当前rx_ring存满了,再来报文会被记录rx_missed_error)。
- Rx_queue
DPDK的接口的收包队列结构体:我们主要关注两个环形队列rx_ring、sw_ring。
- rx_ring
一个地址连续环形队列,存储的是描述符。描述符中包含将来存放数据包的物理地址、DD标志等。
物理地址供网卡DMA模块使用,也称为DMA地址(硬件使用物理地址,当网卡收到数据包后会通过DMA操作将数据包拷贝到物理地址,物理地址是通过虚拟地址转换得到的,下面分析源码时会介绍)。
- sw_ring
存储的是将来存放数据包的虚拟地址,虚拟地址供应用使用(软件应用使用虚拟地址,应用往虚拟地址读写数据包)收包流程:
DMA从描述符队列(rx_ring数组)的可用位置(union ixgbe_adv_rx_desc类型)获取到mbuf的硬件地址;
然后将数据包从网卡FIFO写入到指定的硬件地址中(对应sw_ring数组的某个mbuf元素);设置对应描述符的DD位以及其他的信息(比如Rss hash,pkt-len等);(DD位为1表示当前描述符被占用了,接下来DMA将包放入到下一个可用描述符中指定的硬件地址中)
最后发送一个硬件中断(对于DPDK,屏幕了中断,PMD驱动程序通过轮询的方式检查是否有包到达)。
- DD标志:
用于标识一个描述符buf是否可用,无论网卡是工作在轮询方式下还是中断方式,判断数据包接收成功或者是否发送成功都需要检查描述符中的完成状态位(Description Done)DD。该状态位由DMA控制器在完成操作(从网卡FIFO接收队列到内存,从内存到网卡FIFO发送队列)后进行回写(设置为DD位为1)。
- Mbuf:
对应于Mbuf内存池(mbufpool)中的元素,通过alloc或者free操作内存池(mbufpool)获取或者释放mbuf对象。
这里需要说的一点是mbuf池创建的时候是连续的,但是rx_ring和sw_ring里指向的数据地址不一定是连续的,下面分析收包流程时会介绍。
- PCIE总线:
采用高速串行通信互联标准,自上而下分为事务传输层、数据链路层、物理层;网卡与CPU/内存之间数据包的传输、CPU对网卡寄存器的MMIO操作都通过PCIE进行传输。
- Tx_queue: 发包队列结构体:我们主要关注两个环形队列tx_ring、sw_ring.
- tx_ring
一个地址连续环形队列,存储的是描述符,描述符中包含将来存放数据包的物理地址、DD标志等,
DMA寄存器
网卡会通过DMA将报文放在系统内存中,另外,CPU(PMD驱动程序)配置网卡的操作也是通过操作网卡的寄存器(DMA寄存器)。存在几个问题:
- 网卡如何知道应该放在哪里呢?
- 如何将网卡和系统内存关联起来?
这需要用到网卡的几个寄存器。
RDBAL(Receive Descriptor Base Address Low),
RDBAH(Receive Descriptor Base Address High)
RDLEN(Receive Descriptor Length)
RDH(Receive Descriptor Head)
RDT(Receive Descriptor Tail)。
PMD驱动初始化时会分配一块内存,将这块内存的起始物理地址(64位)写到寄存器。
RDBAL(保存起始物理地址的低32位)和RDBAH(保存物理地址的高32位),然后将这块内存的大小写到寄存器RDLEN中;这块内存称为硬件描述符队列(union ixgbe_adv_rx_desc 数组)。
硬件描述符队列大小 = 接收队列硬件描述符个数 * 接收队列硬件描述符大小。
一个接收队列硬件描述符大小为16字节( sizeof(union ixgbe_adv_rx_desc) ),个数可配置,一般为1024。
DD位的理解
DD标志,这个标志标识着一个描述符是否可用的情况。
- 收包
网卡(DMA)在使用这个描述符前,先检查DD位是否为0,如果为0,那么就可以使用描述符,把数据拷贝到描述符指定的地址,之后把DD标志位置为1,否则表示不能使用这个描述符。
而对于PMD驱动而言,恰恰相反,在读取数据包时,先检查DD位是否为1,如果为1,表示网卡已经把数据放到了内存中,可以读取,读取完后,再把DD位设置为0,否则,就表示没有数据包可读。
- 发包
收包流程
接收报文时需要将网卡和内存关联起来,即将要将要存放报文的地址告诉网卡,这是通过接收硬件描述符来实现的。
网卡与内存关联
接收硬件描述符
接收硬件描述符,数据结构如下所示:
- 接收接收描述符的格式分析
一个描述符(union ixgbe_adv_rx_desc)有两种格式;如下所示:
- 读格式(union ixgbe_adv_rx_desc 中的read)
读格式是从网卡(DMA)角度来说的,由PMD驱动将mbuf的物理地址写到packet buffer address字段(pkt_addr),网卡(DMA)读取此字段获取内存物理地址,收到的报文就可以存到此内存。
- 回写格式(union ixgbe_adv_rx_desc 中的 wb「write-back」)
回写格式也是从网卡(DMA)角度来说,网卡(DMA)将报文写到指定的内存后,就会以下面的格式将报文的相关信息回写到描述符(union ixgbe_adv_rx_desc)中,最后设置DD位(第二个8字节的最低位);PMD驱动程序通过判断DD位是否为1来接收报文(DD位为1,说明DMA将报文放入到内存了,PMD驱动可以接收报文)。
总结:
接收队列硬件描述符队列就是一块内存。
网卡先以读格式,读取描述符,获取内存的物理地址,将报文写到内存;
然后以回写格式将报文额外信息写到描述符中;
PMD驱动程序可以以回写格式读取描述符,获取报文的长度,类型等信息。
PMD驱动/程序收包
RDH/RDT寄存器
网卡和内存关联起来后,就可以收取报文了,此时又用到两个寄存器: RDH(Receive Descriptor Head)和RDT(Receive Descriptor Tail)。
- RDH:
RDH为头指针,指向第一个可用描述符。
网卡收取报文并且DMA回写成功后,由网卡(DMA)来移动RDH到下一个可用描述符。
- RDT:
RDT为尾指针,指向最后一个可用描述符。
RDH和RDT之间的描述符为网卡可用描述符,RDT由PMD驱动来移动。
PMD驱动从第一个描述符开始,轮询DD位是否为1,为1就认为此描述符对应的mbuf有报文,此时会申请新的mbuf,将新mbuf物理地址写到此描述符的pkt_addr,并将DD位置0,这样的话此描述符就又可用被网卡使用了;同时将老的有报文的mbuf返回给用户。
描述符再次可用后,PMD驱动就可以更新RDT指向此描述符,为了性能考虑不会每次都会更新RDT,而是等可用描述符超过一定阈值(rx_free_thresh)才更新一次(PMD驱动/CPU通过写RDT寄存器来更新)。
DMA控制器收到数据后,往head写,当head=tail时表示当前队列为空,head->bext = tail表示当前队列已存满。
- 1> dpdk启动刚初始化完后,如下图所示:
- 2> 收取数据包,网卡DMA 移动head
- 3> 应用程序(PMD驱动)取包
- 4>驱动继续取包
可以看出来cpu(PMD驱动)对tail寄存器的更新,并不是在申请完新的mbuf,rx_ring描述符中填充完新mbuf的dma地址后立马就执行,而是等dma可用描述符低于一定阈值时才执行写寄存器更新tai。
详细的收包步骤
详细的收包的步骤,如下:
a. 分配接收队列硬件描述符rx_ring,分配软件ring( sw_ring);
b.CPU通过操作网卡的base、size寄存器,将rx_ring环形队列的起始地址和内存卡大小告诉给DMA控制器,即将描述符队列的物理地址写入到寄存器。
c. dma 通过读寄存器就知道了描述符队列的地址,网卡读取rx_ring队列里接收侧的描述符进而获取系统缓冲区(mbuf)地址。
d. 从外部到达网卡的报文数据先存储到网卡本地的RX_FIFO缓冲区。
e. DMA通过PCIE总线将报文数据写到系统的缓冲区地址(mbuf)中。
f. 网卡回写rx_ring接收侧描述符的更新状态DD标志,置1表示接收完毕。
g. CPU读取描述符sw_ring队列中元素的DD状态,如果为1则表示网卡已经接收完毕,应用可以读取数据包。
h. CPU从sw_ring中读取完数据包后有个“狸猫换太子”的动作,重新从mbuf池中申请一个mbuf替换到sw_ring的该描述符中,将新分配的mbuf虚拟地址转换为物理地址,更新rx_ring中该条目位置的dma物理地址,更新描述符rx_ring队列里的DD标志置0,这样网卡就可以持续往rx_ring缓冲区写数据了。
i. CPU判断rx_ring里可用描述符小于配置的阈值时更新tail寄存器,而不是回填一个mbuf到描述符就更新下tail寄存器(因为CPU高频率的操作寄存器是性能的杀手,所以改用此机制)
j. 至此,应用接收数据包完毕。注意:这里有两个非常关键的队列rx_ring、sw_ring;
rx_ring描述符里存放的是mbuf里data区的起始物理地址,供DMA控制器收到报文后往该地址写入报文数据(硬件DMA直接操作物理地址,不需要cpu参与);
sw_ring描述符里存放的是mbuf的起始虚拟地址,供应用读取数据包;
代码实现
接收队列设置
设置的流程:
分配队列结构体 struct ixgbe_rx_queue
分配接收队列的硬件描述符队列(rx_ring)(一般为1024),每个描述符16字节,保存到 rxq->rx_ring;
分配软件ring,用来保存mbuf,保存到 rxq->sw_ring;rte_eth_rx_queue_setup -> ixgbe_dev_rx_queue_setup
int __attribute__((cold))
ixgbe_dev_rx_queue_setup(struct rte_eth_dev *dev,
uint16_t queue_idx,
uint16_t nb_desc,
unsigned int socket_id,
const struct rte_eth_rxconf *rx_conf,
struct rte_mempool *mp)
const struct rte_memzone *rz;
struct ixgbe_rx_queue *rxq;
struct ixgbe_hw *hw;
uint16_t len;
struct ixgbe_adapter *adapter = (struct ixgbe_adapter *)dev->data->dev_private;
hw = IXGBE_DEV_PRIVATE_TO_HW(dev->data->dev_private);
/* First allocate the rx queue data structure */
rxq = rte_zmalloc_socket("ethdev RX queue", sizeof(struct ixgbe_rx_queue),
RTE_CACHE_LINE_SIZE, socket_id);
rxq->mb_pool = mp;
rxq->nb_rx_desc = nb_desc;
rxq->rx_free_thresh = rx_conf->rx_free_thresh;
rxq->queue_id = queue_idx;
rxq->reg_idx = (uint16_t)((RTE_ETH_DEV_SRIOV(dev).active == 0) ?
queue_idx : RTE_ETH_DEV_SRIOV(dev).def_pool_q_idx + queue_idx);
rxq->port_id = dev->data->port_id;
rxq->crc_len = (uint8_t) ((dev->data->dev_conf.rxmode.hw_strip_crc) ? 0 : ETHER_CRC_LEN);
rxq->drop_en = rx_conf->rx_drop_en;
rxq->rx_deferred_start = rx_conf->rx_deferred_start;
#define IXGBE_MAX_RING_DESC 4096 /* replicate define from rxtx */
#define RTE_PMD_IXGBE_RX_MAX_BURST 32
#define RX_RING_SZ ((IXGBE_MAX_RING_DESC + RTE_PMD_IXGBE_RX_MAX_BURST) * \
sizeof(union ixgbe_adv_rx_desc))
/*
* Allocate RX ring hardware descriptors. A memzone large enough to
* handle the maximum ring size is allocated in order to allow for
* resizing in later calls to the queue setup function.
*/
//分配接收队列硬件描述符内存,注意这里是按最大值分配。
//注意要128字节对齐,因为82599网卡芯片手册规则物理地址必须是128字节对齐
rz = rte_eth_dma_zone_reserve(dev, "rx_ring", queue_idx,
RX_RING_SZ, IXGBE_ALIGN, socket_id);
/*
* Zero init all the descriptors in the ring.
*/
memset(rz->addr, 0, RX_RING_SZ);
rxq->rdt_reg_addr =
IXGBE_PCI_REG_ADDR(hw, IXGBE_RDT(rxq->reg_idx));
rxq->rdh_reg_addr =
IXGBE_PCI_REG_ADDR(hw, IXGBE_RDH(rxq->reg_idx));
//保存接收队列硬件描述符的物理地址
rxq->rx_ring_phys_addr = rz->iova;
//保存接收队列硬件描述符的虚拟地址
rxq->rx_ring = (union ixgbe_adv_rx_desc *) rz->addr;
/*
* Allocate software ring. Allow for space at the end of the
* S/W ring to make sure look-ahead logic in bulk alloc Rx burst
* function does not access an invalid memory region.
*/
len = nb_desc;
if (adapter->rx_bulk_alloc_allowed)
len += RTE_PMD_IXGBE_RX_MAX_BURST;
//分配软件ring内存,这里的大小为参数指定的描述符个数 nb_desc
rxq->sw_ring = rte_zmalloc_socket("rxq->sw_ring",
sizeof(struct ixgbe_rx_entry) * len,
RTE_CACHE_LINE_SIZE, socket_id);
//将接收队列结构保存到对应位置
dev->data->rx_queues[queue_idx] = rxq;
接口启动时接收的初始化
将接收队列硬件描述符的物理地址写到网卡寄存器RDBAL和RDBAH,
将接收队列硬件描述符的长度写到网卡寄存器RDLEN。
rte_eth_dev_start -> ixgbe_dev_start -> ixgbe_dev_rx_init
/*
* Initializes Receive Unit.
*/
int __attribute__((cold))
ixgbe_dev_rx_init(struct rte_eth_dev *dev)
{
struct ixgbe_hw *hw;
struct ixgbe_rx_queue *rxq;
uint64_t bus_addr;
uint32_t rxctrl;
uint32_t fctrl;
uint32_t hlreg0;
uint16_t i;
struct rte_eth_rxmode *rx_conf = &dev->data->dev_conf.rxmode;
int rc;
hw = IXGBE_DEV_PRIVATE_TO_HW(dev->data->dev_private);
/*
* Make sure receives are disabled while setting
* up the RX context (registers, descriptor rings, etc.).
*/
//确保网卡的接收功能是关闭的
rxctrl = IXGBE_READ_REG(hw, IXGBE_RXCTRL);
IXGBE_WRITE_REG(hw, IXGBE_RXCTRL, rxctrl & ~IXGBE_RXCTRL_RXEN);
//使能接收广播,丢弃pause报文
/* Enable receipt of broadcasted frames */
fctrl = IXGBE_READ_REG(hw, IXGBE_FCTRL);
fctrl |= IXGBE_FCTRL_BAM; /* Broadcast Accept Mode */
fctrl |= IXGBE_FCTRL_DPF; /* Discard Pause Frame */
fctrl |= IXGBE_FCTRL_PMCF; /* Pass MAC Control Frames */
IXGBE_WRITE_REG(hw, IXGBE_FCTRL, fctrl);
/*
* Configure CRC stripping, if any.
*/
//设置硬件自动去掉crc
hlreg0 = IXGBE_READ_REG(hw, IXGBE_HLREG0);
if (rx_conf->hw_strip_crc)
hlreg0 |= IXGBE_HLREG0_RXCRCSTRP;
else
hlreg0 &= ~IXGBE_HLREG0_RXCRCSTRP;
/*
* Configure jumbo frame support, if any.
*/
//使能接收巨帧
if (rx_conf->jumbo_frame == 1) {
hlreg0 |= IXGBE_HLREG0_JUMBOEN;
maxfrs = IXGBE_READ_REG(hw, IXGBE_MAXFRS);
maxfrs &= 0x0000FFFF;
maxfrs |= (rx_conf->max_rx_pkt_len << 16);
IXGBE_WRITE_REG(hw, IXGBE_MAXFRS, maxfrs);
} else
hlreg0 &= ~IXGBE_HLREG0_JUMBOEN;
IXGBE_WRITE_REG(hw, IXGBE_HLREG0, hlreg0);
/* Setup RX queues */
for (i = 0; i < dev->data->nb_rx_queues; i++) {
rxq = dev->data->rx_queues[i];
//将接收队列硬件描述符的物理地址写到网卡接收描述符寄存器中
/* Setup the Base and Length of the Rx Descriptor Rings */
bus_addr = rxq->rx_ring_phys_addr;
IXGBE_WRITE_REG(hw, IXGBE_RDBAL(rxq->reg_idx), (uint32_t)(bus_addr & 0x00000000ffffffffULL));
IXGBE_WRITE_REG(hw, IXGBE_RDBAH(rxq->reg_idx), (uint32_t)(bus_addr >> 32));
//将用户请求的nb_tx_desc个数的接收队列硬件描述符长度写到寄存器
IXGBE_WRITE_REG(hw, IXGBE_RDLEN(rxq->reg_idx), rxq->nb_rx_desc * sizeof(union ixgbe_adv_rx_desc));
//头尾指针先设置为0
IXGBE_WRITE_REG(hw, IXGBE_RDH(rxq->reg_idx), 0);
IXGBE_WRITE_REG(hw, IXGBE_RDT(rxq->reg_idx), 0);
}
//根据设置选择不同的接收函数,后面会以 ixgbe_recv_pkts 为例说明
ixgbe_set_rx_function(dev);
...
return 0;
}
启动接收队列
申请mbuf,将mbuf存放报文的物理地址设置到接收队列硬件描述符(union ixgbe_adv_rx_desc)的pkt_addr字段,这样网卡就知道收到报文后将报文放在哪里了。
rte_eth_dev_start -> ixgbe_dev_start -> ixgbe_dev_rxtx_start -> ixgbe_dev_rx_queue_start
/*
* Start Receive Units for specified queue.
*/
int __attribute__((cold))
ixgbe_dev_rx_queue_start(struct rte_eth_dev *dev, uint16_t rx_queue_id)
{
struct ixgbe_hw *hw;
struct ixgbe_rx_queue *rxq;
uint32_t rxdctl;
int poll_ms;
hw = IXGBE_DEV_PRIVATE_TO_HW(dev->data->dev_private);
if (rx_queue_id < dev->data->nb_rx_queues) {
rxq = dev->data->rx_queues[rx_queue_id];
//分配mbuf,填充到 rxq->sw_ring 中
/* Allocate buffers for descriptor rings */
if (ixgbe_alloc_rx_queue_mbufs(rxq) != 0) {
PMD_INIT_LOG(ERR, "Could not alloc mbuf for queue:%d",
rx_queue_id);
return -1;
}
...
//头指针为0,指向第一个可用描述符
IXGBE_WRITE_REG(hw, IXGBE_RDH(rxq->reg_idx), 0);
//尾指针为最大描述符,指向最后一个可用描述符
IXGBE_WRITE_REG(hw, IXGBE_RDT(rxq->reg_idx), rxq->nb_rx_desc - 1);
dev->data->rx_queue_state[rx_queue_id] = RTE_ETH_QUEUE_STATE_STARTED;
}
return 0;
}
static int __attribute__((cold))
ixgbe_alloc_rx_queue_mbufs(struct ixgbe_rx_queue *rxq)
{
struct ixgbe_rx_entry *rxe = rxq->sw_ring;
uint64_t dma_addr;
unsigned int i;
/* Initialize software ring entries */
for (i = 0; i < rxq->nb_rx_desc; i++) {
volatile union ixgbe_adv_rx_desc *rxd;
//分配mbuf
struct rte_mbuf *mbuf = rte_mbuf_raw_alloc(rxq->mb_pool);
mbuf->data_off = RTE_PKTMBUF_HEADROOM;
mbuf->port = rxq->port_id;
//获取mbuf存放报文的物理地址,注意不是mbuf的首地址
dma_addr =
rte_cpu_to_le_64(rte_mbuf_data_iova_default(mbuf));
rxd = &rxq->rx_ring[i];
//清空接收描述符的DD位
rxd->read.hdr_addr = 0;
//将mbuf接收报文的物理地址赋给描述符
rxd->read.pkt_addr = dma_addr;
rxe[i].mbuf = mbuf;
}
return 0;
}
最后使能网卡的接收功能 hw->mac.ops.enable_rx_dma(hw, rxctrl);
正式的收包处理
正式收包流程,还以ixgbe驱动为例 rte_eth_rx_burst -> ixgbe_recv_pkts。
uint16_t
ixgbe_recv_pkts(void *rx_queue, struct rte_mbuf **rx_pkts, uint16_t nb_pkts)
struct ixgbe_rx_queue *rxq;
volatile union ixgbe_adv_rx_desc *rx_ring;
volatile union ixgbe_adv_rx_desc *rxdp;
struct ixgbe_rx_entry *sw_ring;
struct ixgbe_rx_entry *rxe;
struct rte_mbuf *rxm;
struct rte_mbuf *nmb;
union ixgbe_adv_rx_desc rxd;
uint64_t dma_addr;
uint32_t staterr;
uint32_t pkt_info;
uint16_t pkt_len;
uint16_t rx_id;
uint16_t nb_rx;
uint16_t nb_hold;
uint64_t pkt_flags;
uint64_t vlan_flags;
nb_rx = 0;
nb_hold = 0;
rxq = rx_queue;
rx_id = rxq->rx_tail;
rx_ring = rxq->rx_ring;
sw_ring = rxq->sw_ring;
vlan_flags = rxq->vlan_flags;
while (nb_rx < nb_pkts) {
/*
* The order of operations here is important as the DD status
* bit must not be read after any other descriptor fields.
* rx_ring and rxdp are pointing to volatile data so the order
* of accesses cannot be reordered by the compiler. If they were
* not volatile, they could be reordered which could lead to
* using invalid descriptor fields when read from rxd.
*/
//获取硬件描述符
rxdp = &rx_ring[rx_id];
//获取硬件描述符的 status_error
staterr = rxdp->wb.upper.status_error;
//判断DD位是否被硬件置1,为1说明有报文,不是1就break
if (!(staterr & rte_cpu_to_le_32(IXGBE_RXDADV_STAT_DD)))
break;
rxd = *rxdp;
//分配一个新的mbuf
nmb = rte_mbuf_raw_alloc(rxq->mb_pool);
nb_hold++;
//获取软件ring的当前元素
rxe = &sw_ring[rx_id];
//尾指针加1
rx_id++;
//如果达到最大值,则翻转为0,相当于环形效果
if (rx_id == rxq->nb_rx_desc)
rx_id = 0;
//从rxe->mbuf取出mbuf地址,此mbuf已经有报文内容
rxm = rxe->mbuf;
//rxe->mbuf被赋予一个新的mbuf
rxe->mbuf = nmb;
//获取新mbuf的物理地址
dma_addr =
rte_cpu_to_le_64(rte_mbuf_data_iova_default(nmb));
//hdr_addr清0,就会将DD位也清0,否则下次循环到此描述符就会错误的认为有报文
rxdp->read.hdr_addr = 0;
//将mbuf的物理地址赋给描述符,网卡就可以把新报文写到新mbuf中
rxdp->read.pkt_addr = dma_addr;
//从描述符的wb字段获取报文相关的信息,包括长度,vlanid等,并填到mbuf中
pkt_len = (uint16_t) (rte_le_to_cpu_16(rxd.wb.upper.length) - rxq->crc_len);
rxm->data_off = RTE_PKTMBUF_HEADROOM;
rte_packet_prefetch((char *)rxm->buf_addr + rxm->data_off);
rxm->nb_segs = 1;
rxm->next = NULL;
rxm->pkt_len = pkt_len;
rxm->data_len = pkt_len;
rxm->port = rxq->port_id;
pkt_info = rte_le_to_cpu_32(rxd.wb.lower.lo_dword.data);
/* Only valid if PKT_RX_VLAN set in pkt_flags */
rxm->vlan_tci = rte_le_to_cpu_16(rxd.wb.upper.vlan);
...
/*
* Store the mbuf address into the next entry of the array
* of returned packets.
*/
//将已经有报文的mbuf返回给调用者
rx_pkts[nb_rx++] = rxm;
}
//更新尾指针
rxq->rx_tail = rx_id;
//nb_hold表示本次调用成功读取的报文个数,也同时意味着本次调用重新可用mbuf的个数,
//因为读取一次报文,就会分配新的mbuf,并赋给描述符,这个描述符就可以被网卡再次使用。
//rxq->nb_rx_hold是累计可用的描述符个数。
nb_hold = (uint16_t) (nb_hold + rxq->nb_rx_hold);
//如果累计的可用描述符个数超过了阈值,就要更新网卡能看到的描述符尾指针了。
//如果不更新尾指针,随着收包头指针一直增加,和尾指针重合时,就没有可用描述符了。
if (nb_hold > rxq->rx_free_thresh) {
PMD_RX_LOG(DEBUG, "port_id=%u queue_id=%u rx_tail=%u "
"nb_hold=%u nb_rx=%u",
(unsigned) rxq->port_id, (unsigned) rxq->queue_id,
(unsigned) rx_id, (unsigned) nb_hold,
(unsigned) nb_rx);
rx_id = (uint16_t) ((rx_id == 0) ?
(rxq->nb_rx_desc - 1) : (rx_id - 1));
IXGBE_PCI_REG_WRITE(rxq->rdt_reg_addr, rx_id);
//清空计数
nb_hold = 0;
}
//更新nb_rx_hold
rxq->nb_rx_hold = nb_hold;
return nb_rx;
- 解析:
- 收包之前,先判断DD标志位。
staterr = rxdp->wb.upper.status_error; if (!(staterr & rte_cpu_to_le_32(IXGBE_RXDADV_STAT_DD))) break;
如果描述符的DD位不为1,则表明这个描述符网卡还没有准备好,也就是没有包!没有包,就跳出循环。
如果描述符准备好了,就取出对应的描述符,因为网卡已经把一些信息存到了描述符里,可以后面把这些信息填充到新分配的数据包里。
- 狸猫换太子
发包流程
发送报文时也需要将网卡和内存关联起来,即将要发送的报文地址告诉网卡,这也是通过发送硬件描述符来实现的。
网卡与内存关联
发送硬件描述符
-
数据结构
-
发送硬件描述符格式
如下,发送硬件描述符,也分为读和回写两种格式,都从网卡的角度来说。
- 读格式
对于读格式,驱动将报文的物理地址设置到第一个8字节的address字段,网卡读取此字段就能获取发送报文的物理地址;
同时驱动也会设置第二个8字节的相关字段,比如报文长度,是否是最后一个报文段,何时回写等,网卡根据这些信息正确的将报文发送出去。
- 回写格式
对于回写格式,只有一个字段有效,第二个8字节的第32位,此位代表DD(Descriptor Done)位,网卡完成报文发送后,并且此描述符设置了RS标志位,则会将此DD位设置为1;
PMD驱动读取此位就知道此描述符及它之前的描述符都可以被PMD驱动使用。注:
DCMD字段中的==RS(report status)==位用来控制网卡何时回写DD位。
注意和接收方向的区别:
- 接收方向:
在接收方向网卡每收到一个报文就会回写一次接收描述符,将报文长度等信息填写到接收描述符,这是必须的,否则驱动怎么知道接收的报文多长呢;- 发送方向:
发送方向网卡不需要每发送一个报文就回写一次DD位为1,并且每个报文回写会影响性能;PMD驱动只关心报文是否发送成功,对应的发送描述符是否可用,可以通过参数tx_rs_thresh设置网卡多久回写一次,如果发送报文个数超过tx_rs_thresh,就会设置DCMD的RS位。
- 描述符的回写
- 接收过程中的回写:
描述符的回写(write-back),回写是指把用过后的描述符,恢复其重新使用的过程。
在接收数据包过程中,DMA使用描述符标识包可读取(DD位为1),然后驱动程序读取数据包,读取之后,就会把DD位置0,同时进行回写操作(操作寄存器),这个描述符也就可以再次被网卡硬件使用了。
- 发送过程中的回写:
发送过程中,回写却不是立刻完成的。发送有两种方式进行回写:1.Updating by writing back into the Tx descriptor 2.Update by writing to the head pointer in system memory
第二种回写方式貌似针对的网卡比较老;对于82599,使用第一种回写方式。在下面三种情况下,才能进行回写操作(默认为第一种,回写取决于RS):
1.TXDCTL[n].WTHRESH = 0 and a descriptor that has RS set is ready to be written back. 2.TXDCTL[n].WTHRESH > 0 and TXDCTL[n].WTHRESH descriptors have accumulated. 3.TXDCTL[n].WTHRESH > 0 and the corresponding EITR counter has reached >>zero. The timer expiration flushes any accumulated descriptors and sets an interrupt >>event(TXDW).
发送时回写,有两种情况:
1、挂载每个包的最后一个分段时,若当前使用的描述符desc数大于上限(默认为32),设置RS。
2、burst发包的最后一个包的最后一个分段,设置RS。
PMD驱动/DPDK程序发包
发包过程寄存器的变化
- dpdk初始化完成时,tx_ring的队列为空
- DMA控制器通过head去判断当前的DD状态,如果为0,则可以执行发送动作
- CPU(PMD驱动)需要对tx_ring上网卡发送成功的描述符的缓存空间进行释放操作,待应用下次继续写入。
详细的发包过程
(1)CPU读取发送侧描述符 tx_ring 队列,检查DD标志是否为1,为1则说明发送完毕;
(2)针对发送完毕的描述符需要释放该描述符里对应的缓冲区(mbuf);
(3)CPU将准备发送的缓冲区mbuf的虚拟地址填充到描述符 sw_ring;
(4)CPU通过将准备发送的缓冲区mbuf的虚拟地址转换得到该mbuf里data数据部分的物理地址,填充到发送侧描述符 tx_ring 队列中,并将 DD 标志清0;
(5)DMA控制器读取base寄存器,获取发送侧描述符,根据发送侧描述符获取 tx_ring 队列地址,读取 head 指针里的元素,判断 DD 标志位是否为0,为0则从描述符中获取数据缓存区地址(mbuf硬件地址),通过PCIE总线将数据拷贝到网卡硬件 Tx_FIFO 缓存中,往外发送数据;
(6)DMA控制器回写该描述符中队列里DD的标志置1,通知CPU该缓存中数据已成功发送;
(7)至此,应用发送数据包完毕。
注:
这里有两个非常关键的队列tx_ring、sw_ring;
tx_ring 描述符里存放的是mbuf里data区的起始物理地址,供DMA控制器读取报文(硬件DMA直接操作物理地址,不需要cpu参与);
sw_ring 描述符里存放的是mbuf的起始虚拟地址,供应用写入数据包。
代码实现
发包代码
uint16_t
ixgbe_xmit_pkts(void *tx_queue, struct rte_mbuf **tx_pkts,
uint16_t nb_pkts)
{
...
txq = tx_queue;
sw_ring = txq->sw_ring;
txr = txq->tx_ring;
tx_id = txq->tx_tail; /* 相当于ixgbe的next_to_use */
txe = &sw_ring[tx_id]; /* 得到tx_tail指向的entry */
txp = NULL;
...
/*
若空闲的mbuf数小于下限(默认为32),清理空闲的mbuf
起始时:nb_tx_free=nb_tx_desc(比如1024),只有当nb_tx_free 比较少时,才需要检查,进行清理(设置DD位为0,以及设置nb_tx_free) 并且一次清理
tx_rs_thresh 个。
注:这样PMD驱动减少操作DD位的次数,因为DMA也在操作DD位。减少竞争?
tx_rs_thresh: rs(RS位: report status),一般默认32;
tx_free_thresh: 一般默认32;
*/
if (txq->nb_tx_free < txq->tx_free_thresh)
ixgbe_xmit_cleanup(txq);
...
/* TX loop */
for (nb_tx = 0; nb_tx < nb_pkts; nb_tx++) {
...
tx_pkt = *tx_pkts++; /* 待发送的mbuf */
pkt_len = tx_pkt->pkt_len; /* 待发送的mbuf的长度 */
...
nb_used = (uint16_t)(tx_pkt->nb_segs + new_ctx); /* 使用的desc数 */
...
tx_last = (uint16_t) (tx_id + nb_used - 1); /* tx_last指向最后一个desc */
...
if (tx_last >= txq->nb_tx_desc) /* 注意是一个环形缓冲区 */
tx_last = (uint16_t) (tx_last - txq->nb_tx_desc);
...
if (nb_used > txq->nb_tx_free) {
...
if (ixgbe_xmit_cleanup(txq) != 0) {
/* Could not clean any descriptors */
if (nb_tx == 0) /* 若是第一个包(未发包),return 0 */
return 0;
goto end_of_tx; /* 若非第一个包(已发包),停止发包,更新发送队列参数 */
}
...
}
...
/* 每个包可能包含多个分段,m_seg指向第一个分段 */
m_seg = tx_pkt;
do {
txd = &txr[tx_id]; /* desc */
txn = &sw_ring[txe->next_id]; /* 下一个entry */
...
/* 将老的已经发送的报文给释放了 */
if (txe->mbuf != NULL)
rte_pktmbuf_free_seg(txe->mbuf);
txe->mbuf = m_seg; /* 将m_seg挂载到txe */
...
slen = m_seg->data_len; /* m_seg的长度 */
buf_dma_addr = rte_mbuf_data_dma_addr(m_seg); /* m_seg的总线地址 */
txd->read.buffer_addr =
rte_cpu_to_le_64(buf_dma_addr); /* 总线地址赋给txd->read.buffer_addr */
txd->read.cmd_type_len =
rte_cpu_to_le_32(cmd_type_len | slen); /* 长度赋给txd->read.cmd_type_len */
...
txe->last_id = tx_last; /* last_id指向最后一个desc */
tx_id = txe->next_id; /* tx_id指向下一个desc */
txe = txn; /* txe指向下一个entry */
m_seg = m_seg->next; /* m_seg指向下一个分段 */
} while (m_seg != NULL);
...
/* 最后一个分段 */
cmd_type_len |= IXGBE_TXD_CMD_EOP;
txq->nb_tx_used = (uint16_t)(txq->nb_tx_used + nb_used); /* 更新nb_tx_used */
txq->nb_tx_free = (uint16_t)(txq->nb_tx_free - nb_used); /* 更新nb_tx_free */
...
if (txq->nb_tx_used >= txq->tx_rs_thresh) { /* 若使用的mbuf数大于上限(默认为32),设置RS */
...
cmd_type_len |= IXGBE_TXD_CMD_RS;
...
txp = NULL; /* txp为NULL表示已设置RS */
} else
txp = txd; /* txp非NULL表示未设置RS */
...
txd->read.cmd_type_len |= rte_cpu_to_le_32(cmd_type_len);
}
...
end_of_tx:
/* burst发包的最后一个包的最后一个分段 */
...
if (txp != NULL) /* 若未设置RS,设置RS */
txp->read.cmd_type_len |= rte_cpu_to_le_32(IXGBE_TXD_CMD_RS);
...
IXGBE_PCI_REG_WRITE_RELAXED(txq->tdt_reg_addr, tx_id); /* 将tx_id写入TDT */
txq->tx_tail = tx_id; /* tx_tail指向下一个desc */
...
return nb_tx;
}
Q: 已经发送的报文(mbuf)什么时候释放空间?
A: 在 ixgbe_xmit_pkts 中发包时会进行释放。
do {
txd = &txr[tx_id]; /* desc */
txn = &sw_ring[txe->next_id]; /* 下一个entry */
...
/* 将老的已经发送的报文给释放了 */
if (txe->mbuf != NULL)
rte_pktmbuf_free_seg(txe->mbuf);
txe->mbuf = m_seg; /* 将m_seg(要发送的报文)挂载到txe */
...
slen = m_seg->data_len;
buf_dma_addr = rte_mbuf_data_dma_addr(m_seg);
txd->read.buffer_addr =
rte_cpu_to_le_64(buf_dma_addr);
txd->read.cmd_type_len =
rte_cpu_to_le_32(cmd_type_len | slen);
...
txe->last_id = tx_last;
tx_id = txe->next_id;
txe = txn;
m_seg = m_seg->next;
} while (m_seg != NULL);
static inline int
ixgbe_xmit_cleanup(struct ixgbe_tx_queue *txq)
{
...
uint16_t last_desc_cleaned = txq->last_desc_cleaned;
...
/* 最后一个entry */
desc_to_clean_to = (uint16_t)(last_desc_cleaned + txq->tx_rs_thresh);
/*
一次清理 tx_rs_thresh 个,减少清理次数,没有发送完一个包 ,就设置DD位。
*/
if (desc_to_clean_to >= nb_tx_desc) /* 注意是环形缓冲区 */
desc_to_clean_to = (uint16_t)(desc_to_clean_to - nb_tx_desc);
...
/* 最后一个entry的最后一个desc */
desc_to_clean_to = sw_ring[desc_to_clean_to].last_id;
status = txr[desc_to_clean_to].wb.status;
/* 若最后一个desc的DD为0,return -1 */
if (!(status & rte_cpu_to_le_32(IXGBE_TXD_STAT_DD))) {
...
return -(1);
}
...
/* 将要清理的desc数 */
if (last_desc_cleaned > desc_to_clean_to) /* 注意是环形缓冲区 */
nb_tx_to_clean = (uint16_t)((nb_tx_desc - last_desc_cleaned) +
desc_to_clean_to);
else
nb_tx_to_clean = (uint16_t)(desc_to_clean_to -
last_desc_cleaned);
...
txr[desc_to_clean_to].wb.status = 0; /* 清零DD */
...
txq->last_desc_cleaned = desc_to_clean_to; /* 更新last_desc_cleaned */
txq->nb_tx_free = (uint16_t)(txq->nb_tx_free + nb_tx_to_clean); /* 更新nb_tx_free */
...
return 0;
}
收发包总结
-
在pmd中,对于接收方向(从网卡收数据)来说:
接收硬件描述符队列的初始状态head指针指向base,tail指向指向base+len。
网卡是生产者,通过移动head指针将数据放在mbuf中;
驱动是消费者,将接收ring(rx_ring)中buf_addr 更换成新mbuf的物理地址,旧的mbuf可以返回给应用程序来处理。驱动通过移动tail指针,将接收描述符还给网卡,但是并没有每次收包都更新收包队列尾部索引寄存器,而是在可释放的收包描述符数量达到一个阈值(rx_free_thresh)的时候才真正更新收包队列尾部索引寄存器。设置合适的可释放描述符数量阈值,可以减少没有必要的过多的收包队列尾部索引寄存器的访问,改善收包的性能。 -
对于发送方向来说:
发送硬件描述符队列的初始状态head和tail都指向base。
驱动是生产者,发包时,先将发送数据的物理地址赋值给发送描述符的txd->read.buffer_addr,最后通过移动tail指针通知网卡有数据要发送。
网卡是消费者,当获知tail指针移动就会发送数据,网卡发送完数据,会移动head指针。
QA
-
Q:硬件(DMA)和驱动程序(CPU)都在操作描述符(内存)中的DD位,存在竞争,或者伪共享吗?
A: -
Q: 网卡的一些异常统计的解释以及原因。
A:
参见:网卡调优
-
Q: pmd发包时,如何通知网卡有新数据需要发送?
A:更新tail指针(通过寄存器更新)时就会触发网卡发送数据。比如在ixgbe_xmit_pkts函数最后,都会更新tail指针: IXGBE_PCI_REG_WRITE_RELAXED(txq->tdt_reg_addr, tx_id);从网卡datasheet也能看到相关说明:
-
Q:网卡发送成功后,驱动怎么知道描述符可用?
从datasheet看到,有四种方法,默认采用第三种,即通过DD标志位获取。
-
Q: 网卡驱动发送方向,mbuf什么时候释放?
A: 许多驱动程序并没有在数据包传输后立即将mbuf释放回到mempool或本地缓存中。相反,他们将mbuf留在Tx环中,当需要在Tx环中插入,或者已经超过 tx_rs_thresh 时,执行批量释放。 -
Q: 网卡多队列和内存的关系?
A: 如果网卡有多个接收队列,则每个接收队列对应一块内存区域(mbuf缓冲区)。
对于Linux内核收包,每个CPU都有自己的MSI-X硬中断,数据包到达网卡的接收队列,如果是中断中断平衡,则可以将某个队列中的包均衡的分发到多个cpu中;如果设置了中断亲和性,则可以将某个队列收到的包,发送固定的中断,让指定的CPU进行处理。 -
Q: 网卡收到包后如何将包同步到PMD驱动?
A:桥梁:接收的 ring buffer(硬件描述符缓冲区) -
Q: 网卡的RX fifo队列大小和 rx_ring buffer(硬件描述符缓冲区队列)大小的关系?
A: 网卡中的缓冲区(接收FIFO缓冲区/发送FIFO缓冲区)既不属于内核空间,也不属于用户空间。它属于硬件缓冲。
接收FIFO缓冲区是用来通过系统总线传送数据到系统存储器之前,缓存从网卡上接收到的数据。接收网卡的FIFO缓冲区比较小,里面有数据便会尽量将数据存在内核缓冲(mbuf)中。
rx ring buffer是由驱动与网卡共享的,为接收硬件描述符队列。一个描述符中存储有Mbuf的硬件地址等信息。
同理:网卡的发送FIFO缓冲区,发送硬件描述符队列。
注:
一般不会设置网卡的FIFO缓冲区的大小。但是可以设置网卡的接收队列、发送队列的个数;以及设置 rx/tx ring buffer 的大小。。
- 设置网卡的接收队列、发送队列的个数。
ethtool -L 可以修改 RX/Tx queue 数量。
注意:对于大部分驱动,修改以上配置会使网卡先 down 再 up,因此会造成丢包。请酌情 使用。
参考
DPDK收发包流程分析1:
https://zhuanlan.zhihu.com/p/497232818
https://blog.csdn.net/armlinuxww/article/details/114009778
DPDK 收发包流程2:
https://www.jianshu.com/p/69aa4c99cdb0
代码级别讲解收发包:
https://blog.csdn.net/hz5034/article/details/88381486
DD位的理解:
https://www.cnblogs.com/yhp-smarthome/p/6705638.html
http://www.mouser.fr/pdfdocs/82599datasheet.pdf (datasheet 对于 DD的解释比较清楚;搜索:DD bit)
DMA是如何将数据包从网卡队列移到内存中的:
https://developer.aliyun.com/article/515827
网卡发送Tx丢包问题:
https://decodezp.github.io/2019/10/17/quickwords34-dpdk-tx-hang/