《深入浅出DPDK》读书笔记(七):PCIe与包处理I/O

本文内容为读书笔记,摘自《深入浅出DPDK》


74.PCIe概览

PCI Express(Peripheral Component Interconnect Express)又称PCIe,它是一种高速串行通信互联标准。格式说明由外设组件互联特别兴趣小组PCI-SIG(PCI Special Interest Group)维护,以取代传统总线通信架构,如PCI、PCI-X以及AGP。

理解包在PCIe上如何传输,首先需要了解PCIe是一种怎样的数据传输协议规范。

PCIe规范遵循开放系统互联参考模型(OSI),自上而下分为事务传输层、数据链路层、物理层,如图6-1a所示。对于特定的网卡(如图6-1b所示), PCIe一般作为处理器外部接口,把物理层朝PCIe根组件(Root Complex)方向的流量叫做上游流量(upstream或者inbound),反之叫做下游流量(downstream或者outbound)。

图6-1 PCIe协议栈及网卡视图


75.PCIe事务传输

如果在PCIe的线路上抓取一个TLP(Transaction Layer Packet,事务传输层数据包),其格式就如图6-2所示,它是一种分组形式,层层嵌套,事务传输层也拥有头部、数据和校验部分。应用层的数据内容就承载在数据部分,而头部定义了一组事务类型。表6-1列出了所有支持的TLP包类型。对于CPU从网卡收发包来说,用到的PCIe的事务类型主要以Memory Read/Write(MRd/MWr)和Completion with Data(CpID)为主。

图6-2 PCIe包格式示意图

表6-1 TLP类型

应用层数据作为有效载荷被承载在事务传输层之上,网卡从线路上接收的以太网包整个作为有效载荷在PCIe的事务传输层上进行内部传输。当然,对于PCIe事务传输层操作而言,应用层数据内容是透明的。一般网卡采用DMA控制器通过PCIe Bus访问内存,除了对以太网数据内容的读写外,还有DMA描述符操作相关的读写,这些操作也由MRd/MWr来完成。

既然应用层数据只是作为有效载荷,那么PCIe协议的三层栈有多少额外开销呢?图6-3列出了每个部分的长度。物理层开始和结束各有1B的标记,整个数据链路层占用6B。TLP头部64位寻址占用16B(32位寻址占用12B), TLP中的ECRC为可选位。所以,对于一个完整的TLP包来说,除去有效载荷,额外还有24B的开销(TLP头部以16B计算)。

图6-3 TLP包开销


76.网卡DMA描述符环形队列

DMA(Direct Memory Access,直接存储器访问)是一种高速的数据传输方式,允许在外部设备和存储器之间直接读写数据。数据既不通过CPU,也不需要CPU干预。整个数据传输操作在DMA控制器的控制下进行。除了在数据传输开始和结束时做一点处理外,在传输过程中CPU可以进行其他的工作。


77.图6-6所示为Intel®82599网卡的收发描述符环形队列。

图6-6 Intel®82599网卡的收发描述符

无论进行收包还是发包,网卡驱动软件需要完成最基本的操作包括,1)填充缓冲区地址到描述符;2)移动尾指针;3)判断描述符中的完成状态位。


78.在收发上要追求卓越的性能,奏出美妙的和弦,全局地认识合奏双方每一个交互动作对后续的调优是一个很好的知识铺垫。这里先抛开控制环形队列的控制寄存器访问,单从数据内容在CPU、内存以及网卡(NIC)之间游走的过程,全局地认识一下收发的底层故事。考虑到Intel®处理器DDIO技术的引入,DMA引擎可直接对CPU内部的Cache进行操作,将数据存放在LLC(Last Level Cache)。下面的示例中,我们采用了理想状态下整个包处理过程都在LLC中完成的情况。

图6-7是全景转发操作交互示意,其所编序号并不严格表征执行顺序。例如,接收侧的重填和发送侧的回收并不一定在接收或者发送的当前执行序列中。

接收方向:

  • 1)CPU填充缓冲地址到接收侧描述符。
  • 2)网卡读取接收侧描述符获取缓冲区地址。
  • 3)网卡将包的内容写到缓冲区地址处。
  • 4)网卡回写接收侧描述符更新状态(确认包内容已写完)。
  • 5)CPU读取接收侧描述符以确定包接收完毕。
  • 6)CPU读取包内容做转发判断。
  • 7)CPU填充更改包内容,做发送准备。
  • 8)CPU读发送侧描述符,检查是否有发送完成标志。
  • 9)CPU将准备发送的缓冲区地址填充到发送侧描述符。
  • 10)网卡读取发送侧描述符中地址。
  • 11)网卡根据描述符中地址,读取缓冲区中数据内容。
  • 12)网卡写发送侧描述符,更新发送已完成标记。

其中,1)和5)是CPU读写LLC的访存操作;2)是PCIe downstream方向的操作;而3)和4)是PCIe upstream方向的操作。

其中,6)和7)属于转发操作,并不是收发的必要操作,都只是CPU的访存操作,不涉及PCIe。)发送方向:

其中,8)和9)是CPU读写LLC的访存操作;10)和11)是PCIe downstream方向的操作;而12)是PCIe upstream方向的操作。

图6-7 转发操作交换示意

  • 1. CPU填充缓冲地址到接收侧描述符
  • 2. 网卡读取接收侧描述符获取缓冲区地址
  • 3. 网卡将包的内容写到缓冲区地址处
  • 4. 网卡回写接收侧描述符更新状态(确认包内容已写完)
  • 5. CPU读取接收侧描述符以确定包接收完毕
  • 6. CPU读取包内容做转发判断
  • 7. CPU填充更改包内容,做发送准备
  • 8. CPU读发送侧描述符,检查是否有发送完成标志
  • 9. CPU将准备发送的缓冲区地址填充到发送侧描述符
  • 10. 网卡读取发送侧描述符中地址
  • 11. 网卡根据描述符中地址,读取缓冲区中数据内容
  • 12. 网卡写发送侧描述符,更新发送已完成标记

这里有意地将访存操作和PCIe操作进行了区分,{2,10,11}属于PCIe downstream操作,{3,4,12}属于PCIe upstream操作,其余均是CPU访问内存操作。如果考虑到控制寄存器(TAIL register)的MMIO,其属于PCIe的downstream操作。这里有一点需要注意,由于读请求和完成确认是成对出现的,因此对于downstream方向的读操作其实仍旧有upstream方向上的完成确认消息。这也是upstream方向上的带宽压力更大的原因。

分析理论接口带宽的最大值,对性能优化很重要。它能作为标尺,真实地反映还有多少空间可以优化。PCIe有很高的物理带宽,以PCIe Gen2×8为例,其提供了4GB的带宽,但净荷带宽远没有那么高,那净荷带宽有多高呢?由于包通过DMA交互操作在主存和设备之间移动,因此其对PCIe的消耗并不只有包内容本身,还包括上面介绍的描述符的读写{2,4,10,12}。

只有知道了每个操作的开销后,才能推算出一个理论的净荷带宽。6.5节将会结合示例讲述如何粗略地计算这样的理论净荷带宽。


79.优化的考虑

通过DMA收发的全景介绍,读者对这些活动的细节已经有所理解。访问内存操作的调优放到7.2节去描述,本节主要从PCIe带宽调优的角度讲述可以用到的方法。

(1)减少MMIO访问的频度。

高频度的寄存器MMIO访问,往往是性能的杀手。接收包时,尾寄存器(tail register)的更新发生在新缓冲区分配以及描述符重填之后。只要将每包分配并重填描述符的行为修改为滞后的批量分配并重填描述符,接收侧的尾寄存器更新次数将大大减少。DPDK是在判断空置率小于一定值后才触发重填来完成这个操作的。发送包时,就不能采用类似的方法。因为只有及时地更新尾寄存器,才会通知网卡进行发包。但仍可以采用批量发包接口的方式,填充一批等待发送的描述符后,统一更新尾寄存器。

(2)提高PCIe传输的效率。

每个描述符的大小是固定的,例如16Byte。每次读描述符或者写描述符都触发一次PCIe事务,显然净荷小,利用率低。如果能把4个操作合并成整Cache Line大小来作为PCIe的事务请求(PCIe净荷为64Byte),带宽利用率就能得到提升。

另外,在发送方向,发送完成后回写状态到描述符。避免每次发送完成就写回,使用批量写回方式(例如,网卡中的RS bit),可以用一次PCIe的事务来完成批量(例如,32个为一组)的完成确认。

(3)尽量避免Cache Line的部分写。

DMA引擎在写数据到缓冲区的过程中,如果缓冲区地址并不是Cache Line对齐或者写入的长度不是整个Cache Line,就会发生Cache Line的部分写。Cache Line的部分写会引发内存访问read-modify-write的合并操作,增加额外的读操作,也会降低整体性能。所以,DPDK在Mempool中分配buffer的时候,会要求对齐到Cache Line大小。

一个很直观的例子就是对于64B包的处理,网卡硬件去除报文CRC(报文净荷60B),与网卡硬件不去除报文CRC(净荷64B)相比,收发性能要更差一些。


6.5 PCIe的净荷转发带宽

了解完所有DMA控制器在PCIe上的操作后,离真实理论有效带宽就又近了一步。本节就一步步来计算这个理论值,这里以2×10GE的82599为例,其Gen2×8的PCIe提供4000MB/s的单向带宽。根据上节分析,我们知道upstream带宽压力要高于downstream,所以转发瓶颈主要需要分析upstream方向。对于净荷的大小,这里选取对带宽压力最大的64B大小的数据包。

首先,找出所有upstream方向上有哪些操作。它们有{3,4,12}的upstream写操作和{2,10,11}所对应的读请求。然后,列出每个操作实际占用的字节数,最后计算出每包转发实际所消耗的字节数。

(1)接收方向,包数据写到内存。

由于82599的TLP会对齐偶数周期,且从LANE0起始每个TLP,所以64B的数据内容会占用96B(12个周期)大小的字节。

(2)接收方向,描述符回写。

考虑到82599倾向于整Cache Line(64B)写回,占用96B。一个Cache Line可以容纳4个描述符。所以,对于单个包,只占1/4的开销,为96B/4=24B。

(3)发送方向,描述符回写。

由于发送方向采用了RS bit,所以每32个包才回写一次,开销很小可以忽略。

(4)接收方向,描述符读请求。

对于读请求,TLP的数据部分为空,故只有24B字节开销。因为82599的偶数时钟周期对齐,所以实际占用32B。同样一次整Cache Line的读,获取4个描述符。计算占用开销:32B/4=8B。

(5)发送方向,描述符读请求。

同上,占用8B。

(6)发送方向,包数据读请求。

每个包都会有一次读请求,占用32B。

根据上面的介绍,每转发一个64字节的包的平均转发开销接近于168字节(96+24+8+8+32)。如果计算包转发率,就会得出64B报文的最大转发速率为4000MB/s/168B=23.8Mp/s。


80.Mbuf

为了高效访问数据,DPDK将内存封装在Mbuf(struct rte_mbuf)结构体内。Mbuf主要用来封装网络帧缓存,也可用来封装通用控制信息缓存(缓存类型需使用CTRL_MBUF_FLAG来指定)。Mbuf结构报头经过精心设计,原先仅占1个Cache Line。随着Mbuf头部携带的信息越来越多,现在Mbuf头部已经调整成两个Cache Line,原则上将基础性、频繁访问的数据放在第一个Cache Line字节,而将功能性扩展的数据放在第二个Cache Line字节。Mbuf报头包含包处理所需的所有数据,对于单个Mbuf存放不下的巨型帧(Jumbo Frame), Mbuf还有指向下一个Mbuf结构的指针来形成帧链表结构。所有应用都应使用Mbuf结构来传输网络帧。

对网络帧的封装及处理有两种方式:将网络帧元数据(metadata)和帧本身存放在固定大小的同一段缓存中;或将元数据和网络帧分开存放在两段缓存里。前者的好处是高效:对缓存的申请及释放均只需要一个指令,缺点是因为缓存长度固定而网络帧大小不一,大部分帧只能使用填0(padding)的方式填满整个缓存,较为耗费内存空间。后者的优点则是相对自由:帧数据的大小可以任意,同时对元数据和网络帧的缓存可以分开申请及释放;缺点是低效,因为无法保证数据存在于一个Cache Line中,可能造成Hit Miss。

为保持包处理的效率,DPDK采用了前者。网络帧元数据的一部分内容由DPDK的网卡驱动写入。这些内容包括VLAN标签、RSS哈希值、网络帧入口端口号以及巨型帧所占的Mbuf个数等。对于巨型帧,网络帧元数据仅出现在第一个帧的Mbuf结构中,其他的帧该信息为空。具体内容请参见第7章。Mbuf的结构如图6-8所示。

图6-8 单帧Mbuf结构

图6-8包含了一个Mbuf的基本组成。其中,Mbuf头部的大小为两个Cache Line,之后的部分为缓存内容,其起始地址存储在Mbuf结构的buffer_addr指针中。在Mbuf头部和实际包数据之间有一段控制头空间(head room),用来存储和系统中其他实体交互的信息,如控制信息、帧内容、事件等。head room的长度可由RTE_PKTMBUF_HEADROOM定义。

head room的起始地址保存在Mbuf的buff_addr指针中,在lib/librte_port/rte_port.h中也有实用的宏,用来获得从buff_addr起始特定偏移量的指针和数据,详情请参考rte_port.h源码中RTE_MBUF_METADATA_UINT8_PTR以及RTE_MBUF_METADATA_UINT8等宏。数据帧的起始指针可通过调用rte_pktmbuf_mtod(Mbuf)获得。

数据帧的实际长度可通过调用rte_pktmbuf_pktlen (Mbuf)或rte_pktmbuf_datalen (Mbuf)获得,但这仅限于单帧Mbuf。巨型帧的单帧长度只由rte_pktmbuf_datalen(Mbuf)返回,而rte_pktmbuf_pktlen(Mbuf)用于访问巨型帧所有帧长度的总和,如图6-9所示。

图6-9 巨型帧Mbuf结构

创建一个新的Mbuf缓存需从所属内存池(关于内存池的信息见6.6.2节)申请。创建的函数为rte_pktmbuf_alloc ()或rte_ctrlmbuf_alloc (),前者用来创建网络帧Mbuf,后者用来创建控制信息Mbuf。初始化该Mbuf则由rte_pktmbuf_init()或rte_ctrlmbuf_init()函数完成。这两个函数用来初始化一些Mbuf的关键信息,如Mbuf类型、所属内存池、缓存起始地址等。初始化函数被作为rte_mempool_create的回调函数。

释放一段Mbuf实际等于将其放回所属的内存池,其缓存内容在被重新创建前不会被初始化。

除了申请和释放外,对Mbuf可执行的操作包括:

  • ❑获得帧数据长度——rte_pktmbuf_datalen()
  • ❑获得指向数据的指针——rte_pktmbuf_mtod()
  • ❑在帧数据前插入一段内容——rte_pktmbuf_prepend()
  • ❑在帧数据后增加一段内容——rte_pktmbuf_append()
  • ❑在帧数据前删除一段内容——rte_pktmbuf_adj()
  • ❑将帧数据后截掉一段内容——rte_pktmbuf_trim()
  • ❑连接两段缓存——rte_pktmbuf_attach(),此函数会连接两段属于不同缓存区的缓存,称为间接缓存(indirect buffer)。对间接缓存的访问效率低于直接缓存(意为一段缓存包含完整Mbuf结构和帧数据),因此请仅将此函数用于网络帧的复制或分段。
  • ❑分开两段缓存——rte_pktmbuf_detach()
  • ❑克隆Mbuf——rte_pktmbuf_clone(),此函数作为rte_pktmbuf_attach的更高一级抽象,将正确设置连接后Mbuf的各个参数,相对rte_pktmbuf_attach更为安全。

81.Mempool

DPDK的内存管理与硬件关系紧密,并为应用的高效存取服务。在DPDK中,数据包的内存操作对象被抽象化为Mbuf结构,而有限的rte_mbuf结构对象则存储在内存池中。内存池使用环形缓存区来保存空闲对象。内存池在内存中的逻辑表现如图6-10所示。

当一个网络帧被网卡接收时,DPDK的网卡驱动将其存储在一个高效的环形缓存区中,同时在Mbuf的环形缓存区中创建一个Mbuf对象。当然,两个行为都不涉及向系统申请内存,这些内存已经在内存池被创建时就申请好了。Mbuf对象被创建好后,网卡驱动根据分析出的帧信息将其初始化,并将其和实际帧对象逻辑相连。对网络帧的分析处理都集中于Mbuf,仅在必要的时候访问实际网络帧。这就是内存池的双环形缓存区结构。

图6-10 内存池的双环形缓存区结构

为增加对Mbuf的访问效率,内存池还拥有内存通道/Rank对齐辅助方法。内存池还允许用户设置核心缓存区大小来调节环形内存块读写的频率。

实践证明,在内存对象之间补零,以确保每个对象和内存的一个通道和Rank起始处对齐,能大幅减少未命中的发生概率且增加存取效率。在L3转发和流分类应用中尤为如此。内存池以更大内存占有量的代价来支持此项技术。在创建一个内存池时,用户可选择是否启用该技术。

多核CPU访问同一个内存池或者同一个环形缓存区时,因为每次读写时都要进行Compare-and-Set操作来保证期间数据未被其他核心修改,所以存取效率较低。DPDK的解决方法是使用单核本地缓存一部分数据,实时对环形缓存区进行块读写操作,以减少访问环形缓存区的次数。单核CPU对自己缓存的操作无须中断,访问效率因而得到提高。当然,这个方法也并非全是好处:该方法要求每个核CPU都有自己私用的缓存(大小可由用户定义,也可为0,或禁用该方法),而这些缓存在绝大部分时间都没有能得到百分之百运用,因此一部分内存空间将被浪费。


系列文章

《深入浅出DPDK》读书笔记(一):基础部分知识点

《深入浅出DPDK》读书笔记(二):网卡的读写数据操作

《深入浅出DPDK》读书笔记(三):NUMA - Non Uniform Memory Architecture 非统一内存架构

《深入浅出DPDK》读书笔记(四):并行计算-SIMD是Single-Instruction Multiple-Data(单指令多数据)

《深入浅出DPDK》读书笔记(五):同步互斥机制

《深入浅出DPDK》读书笔记(六):报文转发(run to completion、pipeline、精确匹配算法、最长前缀匹配LPM)

 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页