当前位置: 首页 > 工具软件 > DPDK > 使用案例 >

DPDK技术简介

咸玄天
2023-12-01

1. DPDK技术介绍

1) 简介

DPDK全称Intel Data Plane Development Kit,是intel提供的数据平面开发工具集,为Intel architecture(IA)处理器架构下用户空间高效的数据包处理提供库函数和驱动的支持。通俗地说,就是一个用来进行包数据处理加速的软件库。

DPDK不同于Linux系统以通用性设计为目的,而是专注于网络应用中数据包的高性能处理。具体体现在DPDK应用程序是运行在用户空间上利用自身提供的数据平面库来收发数据包,绕过了Linux内核协议栈对数据包处理过程。它不是一个用户可以直接建立应用程序的完整产品,不包含需要与控制层(包括内核和协议堆栈)进行交互的工具。

相比原生 Linux(Native Linux),采用Intel DPDK技术后能够大幅提升IPV4的转发性能,可以让用户在迁移包处理应用时(从基于NPU的硬件迁移到基于Intel x86的平台上),获得更好的成本和性能优势。同时可以采用统一的平台部署不同的服务,如应用处理,控制处理和包处理服务。

2) 技术优点

 通过UIO技术将报文拷贝到应用空间处理,规避不必要的内存拷贝和系统调用,便于快速迭代优化。

 通过大页内存HUGEPAGE,降低cache miss(访存开销),利用内存多通道交错访问提高内存访问有效带宽,即提高命中率,进而提高cpu访问速度。

 通过CPU亲和性,绑定网卡和线程到固定的core,减少cpu任务切换。特定任务可以被指定只在某个核上工作,避免线程在不同核间频繁切换,保证更多的cache命中。

 通过无锁队列,减少资源竞争。cache行对齐,预取数据,多元数据批量操作。

 通过轮询可在包处理时避免中断上下文切换的开销。

3) DPDK、网卡、用户应用程序、内核之间的关系

 PMD:Pool Mode Driver,轮询模式驱动,通过非中断,以及数据帧进出应用缓冲区内存的零拷贝机制,提高发送/接受数据帧的效率。

 流分类:Flow Classification,为N元组匹配和LPM(最长前缀匹配)提供优化的查找算法。

 环队列:Ring Queue,针对单个或多个数据包生产者、单个数据包消费者的出入队列提供无锁机制,有效减少系统开销。

 MBUF缓冲区管理:分配内存创建缓冲区,并通过建立MBUF对象,封装实际数据帧,供应用程序使用。

 EAL:Environment Abstract Layer,环境抽象(适配)层,PMD初始化、CPU内核和DPDK线程配置/绑定、设置HugePage大页内存等系统初始化。

2. 源程序包组成1) Makefile &&CONFIG

MakeFile文件主要位于位于 $(RTE_SDK)/mk 中。此处留在后面第5节进行讨论

配置模板位于 $(RTE_SDK)/config。这些模板描述了为每个目标启用的选项。 配置文件许多可以为DPDK库启用或禁用的选项,包括调试选项。用户应该查看配置文件并熟悉这些选项。配置文件同样也用于创建头文件,创建的头文件将位于新生成的目录中。一般可以根据用户编译的编译器和操作系统来直接选择配置项。

2) Lib库

库文件源码位于目录$(RTE_SDK)/lib中。按照惯例,库指的是为应用程序提供API的任何代码。通常,它会生成一个(.a)文件,这个目录中可能也保存一些内核模块。

Lib常用库文件包含以下内容

lib

+-- librte_cmdline # 命令行接口

+-- librte_distributor # 报文分发器

+-- librte_eal # 环境抽象层

+-- librte_ether # PMD通用接口

+-- librte_hash # 哈希库

+-- librte_ip_frag # IP分片库

+-- librte_kni # 内核NIC接口

+-- librte_kvargs # 参数解析库

+-- librte_lpm # 最长前缀匹配库

+-- librte_mbuf # 报文及控制缓冲区操作库

+-- librte_mempool # 内存池管理器

+-- librte_meter # QoS metering 库

+-- librte_net # IP相关的一些头部

+-- librte_power # 电源管理库

+-- librte_ring # 软件无锁环形缓冲区

+-- librte_sched # QoS调度器和丢包器库

+-- librte_timer # 定时器库

3) 应用程序

应用程序是包含 main() 函数的源文件。 他们位于 $(RTE_SDK)/app 和 $(RTE_SDK)/examples 目录中。

常用示例文件:

examples

+-- cmdline # Example of using the cmdline library

+-- exception_path # Sending packets to and from Linux TAP device

+-- helloworld # Basic Hello World example

+-- ip_reassembly # Example showing IP reassembly

+-- ip_fragmentation # Example showing IPv4 fragmentation

+-- ipv4_multicast # Example showing IPv4 multicast

+-- kni # Kernel NIC Interface (KNI) example

+-- l2fwd # L2 forwarding with and without SR-IOV

+-- l3fwd # L3 forwarding example

+-- l3fwd-power # L3 forwarding example with power management

+-- l3fwd-vf # L3 forwarding example with SR-IOV

+-- link_status_interrupt # Link status change interrupt example

+-- load_balancer # Load balancing across multiple cores/sockets

+-- multi_process # Example apps using multiple DPDK processes

+-- qos_meter # QoS metering example

+-- qos_sched # QoS scheduler and dropper example

+-- timer # Example of using librte_timer library

+-- vmdq_dcb # Example of VMDQ and DCB receiving

+-- vmdq # Example of VMDQ receiving

+-- vhost # Example of userspace vhost and switch

3. DPDK架构分析

4. Dpdk基础库介绍

1) EAL 环境适配层

环境抽象层为底层资源如硬件和内存空间的访问提供了接口。 这些通用的接口为APP和库隐藏了不同环境的特殊性。 EAL负责初始化及分配资源(内存、PCI设备、定时器、控制台等等)。

典型函数:rte_eal_init

抄自dpdk网站:

EAL提供的典型服务有:

• DPDK的加载和启动:DPDK和指定的程序链接成一个独立的进程,并以某种方式加载

• CPU亲和性和分配处理:DPDK提供机制将执行单元绑定到特定的核上,就像创建一个执行程序一样。

• 系统内存分配:EAL实现了不同区域内存的分配,例如为设备接口提供了物理内存。

• PCI地址抽象:EAL提供了对PCI地址空间的访问接口

• 跟踪调试功能:日志信息,堆栈打印、异常挂起等等。

• 公用功能:提供了标准libc不提供的自旋锁、原子计数器等。

• CPU特征辨识:用于决定CPU运行时的一些特殊功能,决定当前CPU支持的特性,以便编译对应的二进制文件。

• 中断处理:提供接口用于向中断注册/解注册回掉函数。

• 告警功能:提供接口用于设置/取消指定时间环境下运行的毁掉函数。

2) Ring 库

环形缓冲区支持队列管理。rte_ring并不是具有无限大小的链表,它具有如下属性:

先进先出(FIFO)

最大大小固定,指针存储在表中

无锁实现

多消费者或单消费者出队操作

多生产者或单生产者入队操作

批量出队 - 如果成功,将指定数量的元素出队,否则什么也不做

批量入队 - 如果成功,将指定数量的元素入队,否则什么也不做

突发出队 - 如果指定的数目出队失败,则将最大可用数目对象出队

突发入队 - 如果指定的数目入队失败,则将最大可入队数目对象入队

单生产者入队:

单消费者出队

3) Mempool 库

DPDK提供了内存池机制,使得内存的管理的使用更加简单安全。在设计大的数据结构时,都可以使用mempool分配内存,同时,mempool也提供了内存的获取和释放等操作接口。对于数据包mempool甚至提供了更加详细的接口-rte_pktmbuf_pool_create()

 mempool的创建

内存池的创建使用的接口是rte_mempool_create()。在仔细分析代码之前,先说明一下mempool的设计思路:在DPDK中,总体来说,mempool的组织是通过3个部分实现的

 mempool头结构。mempool由名字区分,挂接在struct rte_tailq_elem rte_mempool_tailq全局队列中,可以根据mempool的名字进行查找,使用rte_mempool_lookup()接口即可。这只是个mempool的指示结构,mempool分配的内存区并不在这里面,只是通过物理和虚拟地址指向实际的内存地址。

 mempool的实际空间。这就是通过内存分配出来的地址连续的空间,用来存储mempool的obj对象。主要利用rte_mempool_populate_default()进行创建

 ring队列。其作用就是存放mempool中的对象指针,提供了方便存取使用mempool的空间的办法。

 mempool的常见使用是获取元素空间和释放空间。

 rte_mempool_get可以获得池中的元素,其实就是从ring取出可用元素的地址。

 rte_mempool_put可以释放元素到池中。

 rte_mempool_in_use_count查看池中已经使用的元素个数

 rte_mempool_avail_count 查看池中可以使用的元素个数

4) 定时器库

定时器库为DPDK执行单元提供定时器服务,使得执行单元可以为异步操作执行回调函数。定时器库的特性如下:

定时器可以周期执行,也可以执行一次。

need-to-insert-img

定时器可以在一个核心加载并在另一个核心执行。但是必须在调用rte_timer_reset()中指定它。

定时器提供高精度(取决于检查本地核心的定时器到期的rte_timer_manage()的调用频率)。

如果应用程序不需要,可以在编译时禁用定时器,并且程序中不调用rte_timer_manage()来提高性能。

具体使用参考:http://blog.csdn.net/linzhaolover/article/details/9410529

5. 库的编译(Makefile)及使用(API使用方案)

need-to-insert-img

makefile位于目录: /examples/xxx/Makefile。 文件中真正必须的为两个变量APP和SRCS-y,前者为示例程序编译生成的目标文件名称,后者为要编译的源文件。

另外两个必须的为makefile文件rte.vars.mk和rte.extapp.mk。前者定义一些全局的编译打包链接用到的选项,如CFLAGS、ASFLAGS、LDFLAGS等变量,头文件位置和库路径等和体系架构相关编译选项。后者rte.extapp.mk内部又包含了重要的mk/rte.app.mk文件,首先变量LDLIBS初始化为DPDK核心编译生成的所有静态库文件。

makefile编写

在DPDK中,Makefiles的套路是

1.在文件开头,包含$(RTE_SDK)/mk/rte.vars.mk

2.设置RTE构建系统的变量,比如设置RTE_SDK和RTE_TARGET环境变量

3.包含指定的 $(RTE_SDK)/mk/rte.XYZ.mk,其中XYZ 可以填写app, lib, extapp, extlib, obj,依赖构建的目标类型.

3.1 应用程序类型(application)

- rte.app.mk

- rte.extapp.mk

- rte.hostapp.mk

3.2 库类型 (library)

- rte.lib.mk

- rte.extlib.mk

- rte.hostlib.mk

3.3 安装类型(install)

rte.install.mk

没有生成任何文件,仅仅用于创建链接和将文件拷贝到安装目录.

3.4 内核模块(Kernel Module )

rte.module.mk

用于构建内核模块,在dpdk开发包框架中.

3.5 Objects类型

- rte.obj.mk

- rte.extobj.mk

3.6 Misc类型

- rte.doc.mk

- rte.gnuconfigure.mk

- rte.subdir.mk

4.包含用户自定义的规则和变量

常用的变量

系统构建变量

RTE_SDK

RTE_TARGET

编译变量(库文件,库目录,C编译标志,C++编译标志)

CFLAGS: C编译标志. 使用 += 为变量添加内容

LDFLAGS: 链接标志

need-to-insert-img

CPPFLAGS: C++编译标志. 使用 += 为变量添加内容

LDLIBS: 待添加的库文件的列表

SRC-y: 源文件列表

警告变量

WERROR_CFLAGS:在dpdk示例makefile中是加上此标志的.

CFLAGS += $(WERROR_CFLAGS)

6. 性能优化1) 内存

 内存拷贝:不要在数据面程序中使用libc

通过Linux应用程序环境,DPDK中可以使用许多libc函数。 这可以简化应用程序的移植和控制平面的开发。 但是,这些功能中有许多不是为了性能而设计的。 诸如memcpy() 或 strcpy() 之类的函数不应该在数据平面中使用。 要复制小型结构体,首选方法是编译器可以优化一个更简单的技术。

对于经常调用的特定函数,提供一个自制的优化函数也是一个好主意,该函数应声明为静态内联。DPDK API提供了一个优化的rte_memcpy() 函数。

 内存申请

need-to-insert-img

libc的其他功能,如malloc(),提供了一种灵活的方式来分配和释放内存。 在某些情况下,使用动态分配是必要的,但是建议不要在数据层面使用类似malloc的函数,因为管理碎片堆可能代价高昂,并且分配器可能无法针对并行分配进行优化。

如果您确实需要在数据平面中进行动态分配,最好使用固定大小对象的内存池。 这个API由librte_mempool提供。 这个数据结构提供了一些提高性能的服务,比如对象的内存对齐,对对象的无锁访问,NUMA感知,批量get/put和percore缓存。 rte_malloc() 函数对mempools使用类似的概念。

 内存区域的并发访问

need-to-insert-img

几个lcore对同一个内存区域进行的读写(RW)访问操作可能会产生大量的数据高速缓存未命中,这代价非常昂贵。 通常可以使用per-lcore变量来解决这类问题。例如,在统计的情况下。 至少有两个解决方案:

使用 RTE_PER_LCORE 变量。注意,在这种情况下,处于lcore x的数据在lcore y上是无效的。

使用一个表结构(每个lcore一个)。在这种情况下,每个结构都必须缓存对齐。

如果在同一缓存行中没有RW变量,那么读取主要变量可以在不损失性能的情况下在内核之间共享。

 NUMA

need-to-insert-img

在NUMA系统上,由于远程内存访问速度较慢,所以最好访问本地内存。 在DPDK中,memzone,ring,rte_malloc和mempool API提供了在特定内存槽上创建内存池的方法。

有时候,复制数据以优化速度可能是一个好主意。 对于经常访问的大多数读取变量,将它们保存在一个socket中应该不成问题,因为数据将存在于缓存中。

 跨存储器通道分配

need-to-insert-img

现代内存控制器具有许多内存通道,可以支持并行数据读写操作。 根据内存控制器及其配置,通道数量和内存在通道中的分布方式会有所不同。 每个通道都有带宽限制,这意味着如果所有的存储器访问都在同一通道上完成,则存在潜在的性能瓶颈。

默认情况下, Mempool Library 分配对象在内存通道中的地址。

2) lcore之间的通信

need-to-insert-img

为了在内核之间提供基于消息的通信,建议使用提供无锁环实现的DPDK ring API。

该环支持批量访问和突发访问,这意味着只需要一次昂贵的原子操作即可从环中读取多个元素(请参阅 Ring 库 )。

使用批量访问操作时,性能会大大提高。

出队消息的代码算法可能类似于以下内容:

#define MAX_BULK 32

while (1) {

/* Process as many elements as can be dequeued. */

count = rte_ring_dequeue_burst(ring, obj_table, MAX_BULK, NULL);

if (unlikely(count == 0))

continue;

my_process_bulk(obj_table, count);

}

3) PMD 驱动

DPDK轮询模式驱动程序(PMD)也能够在批量/突发模式下工作,允许在发送或接收功能中对每个呼叫的一些代码进行分解。

避免部分写入。 当PCI设备通过DMA写入系统存储器时,如果写入操作位于完全缓存行而不是部分写入操作,则其花费较少。 在PMD代码中,已采取了尽可能避免部分写入的措施。

 低报文延迟

need-to-insert-img

传统上,吞吐量和延迟之间有一个折衷。 可以调整应用程序以实现高吞吐量,但平均数据包的端到端延迟通常会因此而增加。 类似地,可以将应用程序调整为平均具有低端到端延迟,但代价是较低的吞吐量。

为了实现更高的吞吐量,DPDK尝试通过突发处理数据包来合并单独处理每个数据包的成本。

以testpmd应用程序为例,突发大小可以在命令行上设置为16(也是默认值)。 这允许应用程序一次从PMD请求16个数据包。 然后,testpmd应用程序立即尝试传输所有接收到的数据包,在这种情况下是全部16个数据包。

在网络端口的相应的TX队列上更新尾指针之前,不发送分组。 当调整高吞吐量时,这种行为是可取的,因为对RX和TX队列的尾指针更新的成本可以分布在16个分组上, 有效地隐藏了写入PCIe 设备的相对较慢的MMIO成本。 但是,当调优为低延迟时,这不是很理想,因为接收到的第一个数据包也必须等待另外15个数据包才能被接收。 直到其他15个数据包也被处理完毕才能被发送,因为直到TX尾指针被更新,NIC才知道要发送数据包,直到所有的16个数据包都被处理完毕才被发送。

为了始终如一地实现低延迟,即使在系统负载较重的情况下,应用程序开发人员也应避免处理数据包。 testpmd应用程序可以从命令行配置使用突发值1。 这将允许一次处理单个数据包,提供较低的延迟,但是增加了较低吞吐量的成本。

4) 锁和原子操作

need-to-insert-img

原子操作意味着在指令之前有一个锁定前缀,导致处理器的LOCK#信号在执行下一条指令时被断言。 这对多核环境中的性能有很大的影响。

可以通过避免数据平面中的锁定机制来提高性能。 它通常可以被其他解决方案所取代,比如percore变量。 而且,一些锁定技术比其他锁定技术更有效率。 例如,Read-Copy-Update(RCU)算法可以经常替换简单的rwlock

7. 遇到的问题及解决方法

https://www.xuebuyuan.com/1149388.html

https://blog.csdn.net/hz5034/article/details/78811445



链接:https://www.jianshu.com/p/86af81a10195
 

 类似资料: