本文档提供软件架构信息,开发环境及优化方案。
有关编程示例以及如何编译运行这些示例,请参阅《DPDK示例用户指南》。
有关编译运行应用程序的基本信息,请参阅《DPDK入门指南》。
以下是一份建议顺序阅读的DPDK参考文档列表:
以下文档提供了与使用DPDK开发应用程序相关的信息:
本章节给出了DPDK架构的一个全局概述。
DPDK的主要目的就是为数据面快速报文处理应用程序提供一个简洁完整的框架。用户可以通过代码来理解其中使用的一些技术,构建自己的应用程序或添加自己的协议栈。Alternative ecosystem options that use the DPDK are available。
通过创建环境抽象层(EAL),DPDK框架为每个特殊的环境创建了一组运行库。这个库特定于Intel架构(32或64位),Linux*用户空间编译器或其他特定的平台。这些环境通过一些makefile和配置文件创建。一旦EAL库编译完成,用户可以通过链接这些库来构建自己的应用程序。除了EAL,还有一些其他的库,包括哈希算法、最长前缀匹配、环形缓冲区等。DPDK提供了一些应用程序实例来指导如何使用这些特定来创建自己的应用程序。
DPDK实现了报文处理的RTC模型,在这种模型中,数据面应用程序在调用之前必须预先分配好所有的资源,并作为执行单元运行在逻辑核上。这种模型并不支持调度,且所有的设备通过轮询的方式访问。不使用中断方式的主要原因就是中断处理增加了性能开销。
作为RTC模型的扩展,通过使用Ring在不同逻辑核之间传递报文和消息,也可以实现报文处理的流水线模型(Pipeline)。流水线模型允许操作分阶段进行,在多核代码执行中可能更高效。
DPDK工程创建要求Linux环境及相关的工具链,例如一个或多个编译工具、汇编程序、make工具、编辑器及DPDK组件用到的库。
当指定环境和架构的库编译出来时,这些库就可以用于创建我们自己的数据面处理程序。
创建Linux用户空间应用程序时,需要用到glibc库。对于DPDK应用程序,必须使用两个全局环境变量(RTE_SDK和RTE_TARGET),这两个变量需要在编译应用程序之前配置好:
export RTE_SDK= /home/user/DPDK
export RTE_TARGET=x86_64-native-linuxapp-gcc
也可以参考《DPDK入门指南》来获取更多搭建开发环境的信息。
环境适配层为应用程序和库提供了通用的接口,隐藏了底层环境细节。EAL提供的服务有:
EAL更详细的描述请参阅本文档“环境适配层”章节。
核心组件指的是一系列库,用于为高性能包处理程序提供所有必须的元素。核心组件及其之间的关系如下图所示:
Figure 2 1 Core Components Architecture
Ring数据结构提供了一个无锁的多生产者,多消费者的FIFO表处理接口。相对于无锁队列来讲,它容易部署,适合大量的操作,而且更快。Ring库在“内存池库(librte_mempool)”中使用,而且,ring还用于不同逻辑核上处理单元之间的通信。
环形缓冲区及其使用可以参考章节“环形缓冲区库”描述。
内存池管理的主要职责就是在内存中分配指定数目对象的Pool。每个Pool以名称来唯一标识,并且使用一个Ring来存储空闲的对象节点。它还提供了一些其他的服务,如对象节点的每核缓存备份,及自动对齐以保证对象能够均衡分布到内存通道上。
内存池分配器的具体行为请参考章节“内存池库”描述。
报文缓冲区库提供了创建和销毁报文缓冲区的能力,DPDK应用程序中使用这些缓冲区来存储消息。这些缓冲区通常在程序开始时通过DPDK的内存池库(librte_mempool)申请并存储在内存池中。缓冲区库(librte_mbuf)提供了报文申请和释放的API,通常情况下,消息Buffer用于缓存消息,报文Buffer用于缓存网络报文。
报文缓冲区管理的具体行为请参考章节“缓冲区库”描述。
这个库位DPDK的执行单元提供了定时器服务,为函数异步执行提供支持。定时器可以设置成周期调用,或者只调用一次。使用EAL提供的接口可以获取高精度时钟,并且能在每个核上根据需要初始化。
具体请参考章节“定时器库”描述。
DPDK的PMD驱动支持1G、10G、40G。 同时DPDK提供了虚拟的以太网控制器,被设计成非异步,基于中断信号的模式。
详细内容参考 章节“轮询模式驱动”描述。
DPDK提供了哈希(librte_hash)、最长前缀匹配(librte_lpm)算法库用于支持相应的分组转发算法。
详细内容查看章节“哈希算法” 和“最长前缀匹配” 。
这个库包括IP协议的一些定义及常见的宏定义。这些定义都是基于FreeBSD*中IP协议栈的代码,包括协议号(用于IP头部)、IP相关的宏、IPv4/IPv6头部结构体以及TCP、UDP和STCP头部结构体。
环境抽象层(EAL)为底层资源如硬件和存储空间的访问提供了接口。这些接口为上层应用程序和库隐藏了不同环境的特殊性。初始化程序负责决定如何分配这些资源(即内存空间、PCI设备、计时器、控制台等)。
EAL提供的服务如下:
在Linux用户空间环境中,DPDK应用程序通过pthread库作为一个用户态程序运行。设备的PCI信息和地址空间通过/sys内核接口及内核模块如uio_pci_generic或igb_uio来发现的。详细信息请参阅Linux内核文档中UIO描述,设备的UIO信息是在程序中用mmap重新映射的。
EAL使用mmap接口从hugetlb中实现物理内存的分配。这部分内存暴露给DPDK服务层,如内存池库。
据此,DPDK服务层可以完成初始化,接着通过设置线程亲和性调用,每个执行单元将会分配给特定的逻辑核,以一个user-level等级的线程来运行。
定时器是通过CPU的时间戳计数器TSC或者通过mmap调用内核的HPET系统接口实现。
部分初始化操作从Glibc的开始函数处就执行了。初始化过程中还执行一个检查,用于保证配置文件所选择的微架构类型是本CPU所支持的,然后才开始调用main()函数。Core的初始化和运行是在rte_eal_init()接口上执行的(参考API文档)。它包括对pthread库的调用(更具体的说是pthread_self(),pthread_create()和pthread_setaffinity_np())。
Figure3‑1EAL Initialization in a Linux Application Environment
注意:对象的初始化,例如内存区间、ring、内存池、lpm表或hash表等,必须作为整个程序初始化的一部分,在主逻辑核上完成。创建和初始化这些对象的函数不是多线程安全的,但是,一旦初始化完成,这些对象本身可以作为安全线程运行。
在初始化EAL资源期间,诸如hugepage支持的内存可以由核心组件分配。 可以通过调用rte_eal_cleanup()函数释放在rte_eal_init()期间分配的内存。 有关详细信息,请参阅API文档。
Linux EAL允许多进程和多线程部署模式。详细信息请参阅“多进程支持”章节描述。
大型连续的物理内存分配是通过hugetlbfs内核文件系统来实现的。EAL提供了相应的接口用于预留指定名字的连续内存空间。这个API同时会将这段连续空间的地址返回给用户程序。
注意:内存申请是使用rte_malloc接口来做的,它也是hugetlbfs文件系统大页支持的。
现有的内存管理是基于Linux内核的大页机制。然而,Xen Dom0并不支持大页,所以要将一个新的内核模块rte_dom0_mem加载上,以便避开这个限制。
EAL使用IOCTL接口用于通告Linux内核模块rte_mem_dom0去申请指定大小的内存块,并从该模块中获取内存段的信息。EAL使用MMAP接口来映射这段内存。对于申请到的内存段,在其内的物理地址都是连续的,但是实际上,硬件地址只在2M内连续。
EAL使用Linux内核提供的文件系统/sys/bus/pci来扫描PCI总线上的内容。内核模块uio_pci_generic提供了/dev/uioX设备文件及/sys下对应的资源文件用于访问PCI设备。DPDK特有的igb_uio模块也提供了相同的功能用于PCI设备的访问。这两个驱动模块都用到了Linux内核提供的uio特性(用户空间驱动)。
注意: 逻辑核就是处理器的逻辑单元,有时也称为硬件线程。
默认的做法是使用共享变量。每逻辑核变量的实现则是通过线程局部存储技术TLS来实现的,它提供了每个线程本地存储的功能。
EAL提供了日志信息接口。默认情况下,在Linux应用程序中,日志信息被发送到syslog和console中。当然,用户可以通过使用不同的日志机制来重写DPDK中的日志函数。
3.1.7.1.跟踪与调试功能
Glibc中提供了一些调试函数用于打印堆栈信息。rte_panic()函数可以产生一个SIG_ABORT信号,这个信号可以触发产生coredump文件,我们可以通过gdb来加载调试。
EAL可以在运行时查询CPU状态(使用rte_cpu_get_feature()接口),用于判断哪个CPU特性可用。
3.1.9.1.主机线程中的用户空间中断和报警处理
EAL创建一个主机线程用于轮询UIO设备描述文件描述符以检测中断。可以通过EAL提供的函数为特定的中断事件注册或注销回调函数,回掉函数在主机线程中被异步调用。EAL同时也允许像NIC中断那样定时调用中断处理回调。
注意: 在DPDK的PMD中,主机线程只对连接状态改变的中断处理,例如网卡的打开和关闭,以及设备突然移除中断。
3.1.9.2.RX中断事件
PMD提供的报文收发程序并不只限制于轮询模式下执行。为了缓解小吞吐量下轮询模式对CPU资源的浪费,暂停轮询并等待唤醒事件发生是一种有效的手段。收包中断是这种场景的一种很好的选择,当然也不是唯一的。
EAL为事件驱动模式提供了相关的API。以Linux为例,其实现依赖于epoll技术。每个线程可以监控一个epoll实例,而在实例中可以添加所有需要的wake-up事件文件描述符。事件文件描述符根据UIO/VFIO规范创建并映射到指定的中断向量上。从FreeBSD’s角度看,可以使用kqueue来代替,但是目前尚未实现。
EAL初始化中断向量和事件文件描述符之间的映射关系,同时每个设备初始化中断向量和队列之间的映射关系,这样,EAL实际上并不知道在指定向量上发生的中断,由设备驱动负责执行后面的映射。
注意:每队列RX中断事件只有VFIO模式支持,VFIO支持多个MSI-X向量。在UIO中,RX中断和其他中断共享中断向量,因此,当RX中断和LSC(连接状态改变)中断同时发生时((intr_conf.lsc == 1 && intr_conf.rxq == 1),只有前者才有能力区分。RX中断由API(rte_eth_dev_rx_intr_*)来实现控制、使能、关闭。当PMD不支持时,这些API返回失败。Intr_conf.rxq标识用于打开每个设备的RX中断。
3.1.9.3.设备移除事件
当总线上的设备被移除时就出发该事件。设备底层资源可能不再可用(即PCI映射未完成)。PMD必须保证在这种情况下,应用程序仍然可以安全地使用其中断回调。
可以使用链接状态改变中断事件相同的方式来订阅这个中断事件。执行上下文是相同的,即专用的中断线程。
考虑到,应用程序可能想要关闭发出设备删除事件的设备,在这种情况下,调用rte_eth_dev_close()可能触发它注销自己的设备删除事件回调。因此,必须注意不要在中断处理程序上下文中关闭设备。必须重新安排这种关闭操作。
EAL PCI设备的黑名单功能是用于标识指定的NIC端口,以便DPDK忽略该端口。可以使用PCIe设备地址描述符(Domain:Bus:Device:Function)将对应端口标记为黑名单。
每个架构不同的锁和原子操作(i686和x86_64)。
物理内存映射就是通过EAL的这个特性实现的。物理内存块之间可能是不连续的,所有的内存通过一个内存描述符表进行管理,且表中的每个描述符(called rte_memseg )指向一块连续的物理内存。
基于此,内存区域分配器的作用就是保证分配到一块连续的物理内存。这些区域被分配出来时会用一个唯一的名字来标识。
rte_memzone描述符也在配置结构体中,可以通过rte_eal_get_configuration()接口来获取。通过名字访问一个内存区域会返回对应内存区域的描述符。
内存分配可以从指定开始地址和对齐方式来预留(默认是cache line大小对齐),对齐一般是以2的次幂来的,并且不小于高速缓存行的大小(64字节)对齐。内存区域也可以从2M或1G大小的内大页内存中获取,这两者系统都支持。
DPDK通常为每个Core指定一个线程,以避免任务切换的开销。这有利于性能的提升,但不总是有效的,并且缺乏灵活性。
电源管理通过限制CPU的运行频率来提升CPU的工作效率。当然,我们也可以通过充分利用CPU的空闲周期来使用CPU的全部功能。
通过使用cgroup技术,CPU的使用量可以很方便的分配,这也提供了新的方法来提升CPU性能,但是这里有个前提,DPDK必须处理每个核上多个线程的上下文切换。
想要更多的灵活性,就要设置线程的CPU亲和性是针对对CPU集合而不是CPU了。
术语“lcore”指一个EAL线程,这是一个真正意义上的Linux/FreeBSD pthread。“EAL pthread”由EAL创建和管理,并执行remote_launch发出的任务。在每个EAL pthread中,有一个称为_lcore_id的TLS(线程本地存储)用于唯一标识线程。由于EAL pthread通常将物理CPU绑定为1:1,所以_locore_id通常等于CPU ID。
但是,当使用多线程时,EAL pthread和指定的物理CPU之间的绑定不再总是1:1了。EAL pthread可能与一组CPU相关,因此_lcore_id将不同于CPU ID。基于这个原因,EAL有一个运行参数选项“-lcores”用来定义分配的CPU亲和性。对于执行的lcore ID或ID组,该选项允许设置该EAL pthread的CPU组。
The format pattern:
–lcores=’[@cpu_set][,[@cpu_set],...]’
‘lcore_set’ and ‘cpu_set’ can be a single number, range or a group.
A number is a “digit([0-9]+)”;
a range is “-”;
a group is “([,,...])”.
If a ‘@cpu_set’ value is not supplied, the value of ‘cpu_set’ will default to the value of ‘lcore_set’.
举例:
"--lcores='1,2@(5-7),(3-5)@(0,2),(0,6),7-8'"
表示启动了9个EAL pthread:
lcore 0运行于CPU组0x41,也就是CPU(0,6)
lcore 1运行于CPU组0x2,也就是CPU(1)
lcore 2运行于CPU组0xe0,也就是CPU(5,6,7)
lcore 3-5运行于CPU组0x5也就是CPU(0,2)
lcore 6运行于CPU组0x41,也就是CPU(0,6)
lcore 7运行于CPU组0x80,也就是CPU(7)
lcore 8运行于CPU组0x100,也就是CPU(8)
使用这个选项,对于给定的lcore ID,可以分配对应的CPU组。它也兼容corelist('- l')选项的模式。
可以在任何用户线程(non-EAL线程)上执行DPDK任务上下文。在non-EAL pthread中,_lcore_id始终是LCORE_ID_ANY,它标识一个no-EAL线程的有效、唯一的_lcore_id。有些库可会使用一个唯一的ID替代(如TID),有些库将不受影响,有些库则会受到限制(如定时器和内存池库)。
所有这些影响将在“已知问题”章节中提到。
DPDK为线程操作引入了两个公共API rte_thread_set_affinity() rte_pthread_get_affinity()。当他们在任何线程上下文中调用时,将获取或设置线程本地存储(TLS)。
这些TLS包括_cpuset和_socket_id:
注意:“非抢占”意味着:
在给定的ring上做入队操作的pthread不能被另一个在同一个ring上做入队的pthread抢占
在给定ring上做出对操作的pthread不能被另一个在同一ring上做出队的pthread抢占
绕过此约束则可能造成第二个进程自旋等待,知道第一个进程再次被调度为止。此外,如果第一个线程被优先级较高的上下文抢占,甚至可能造成死锁。
这意味着,涉及可抢占pthread的用例应该仔细考虑使用rte_ring。
或者,应用程序可以使用无锁堆栈mempool处理程序。 在考虑此处理程序时,请注意:
它目前仅限于x86_64平台,因为它使用的指令(16字节比较和交换)尚未在其他平台上提供。
它具有比非抢占式rte_ring更差的平均情况性能,但是软件缓存(例如,mempool缓存)可以通过减少堆栈访问次数来缓解这种情况。
以下是cgroup控件使用的简单示例,在同一个核心($CPU)上两个线程(t0 and t1)执行数据包I/O。我们期望只有50%的CPU消耗在数据包IO操作上。
mkdir /sys/fs/cgroup/cpu/pkt_io
mkdir /sys/fs/cgroup/cpuset/pkt_io
echo $cpu > /sys/fs/cgroup/cpuset/cpuset.cpus
echo $t0 > /sys/fs/cgroup/cpu/pkt_io/tasks
echo $t0 > /sys/fs/cgroup/cpuset/pkt_io/tasks
echo $t1 > /sys/fs/cgroup/cpu/pkt_io/tasks
echo $t1 > /sys/fs/cgroup/cpuset/pkt_io/tasks
cd /sys/fs/cgroup/cpu/pkt_io
echo 100000 > pkt_io/cpu.cfs_period_us
echo 50000 > pkt_io/cpu.cfs_quota_us
EAL提供了一个malloc API用于申请任意大小内存。
这个API的目的是提供类似malloc的功能,以允许从hugepage中分配内存并方便应用程序移植。《DPDK API参考手册》详细介绍了接口的功能。
通常,这些类型的分配操作不应该在数据面处理中进行,因为他们比基于池的分配慢,并且在分配和释放路径中使用了锁操作。但是,他们可以在配置代码中使用。
更多信息请参阅《DPDKAPI参考手册》中rte_malloc()函数描述。
当CONFIG_RTE_MALLOC_DEBUG开启时,分配的内存包括保护字段,这个字段用于帮助识别缓冲区溢出。
接口rte_malloc()传入一个对齐参数,该参数用于请求在该值的倍数上对齐的内存区域(这个值必须是2的幂次)。
在支持NUMA的系统上,对rte_malloc()接口调用将返回在调用函数的Core所在的插槽上分配的内存。DPDK还提供了另一组API,以允许在指定NUMA插槽上直接显式分配内存,或者分配另一个NUAM插槽上的内存。
这个API旨在由初始化时需要类似malloc功能的应用程序调用。
需要在运行时分配/释放数据,在应用程序的快速路径中,应该使用内存池库。
3.4.4.1.数据结构
Malloc库中内部使用两种数据结构类型:
3.4.4.1.1.malloc_heap
数据结构malloc_heap用于管理每个插槽上的可用内存空间。在内部,每个NUMA节点有一个堆结构,这允许我们根据此线程运行的NUMA节点为线程分配内存。虽然这并不能保证在NUMA节点上使用内存,但是它并不比内存总是在固定或随机节点上的方案更糟。
堆结构及其关键字段和功能描述如下:
Figure3‑2Example of a malloc heap and malloc elements within the malloclibrary
注意:数据结构malloc_heap并不会跟踪使用的内存块,因为除了要再次释放它们之外,它们不会i被接触,需要释放时,将指向块的指针作为参数传递给free函数。
3.4.4.1.2.malloc_elem
数据结构malloc_elem用作各种内存块的通用头结构。它以三种不同的方式使用,如上图所示:
结构中重要的字段和使用方法如下所述:
3.4.4.2.内存申请
在EAL初始化时,所有memseg都将作为malloc堆的一部分进行设置。这个设置包括在BUSY状态结束时放置一个虚拟结构,如果启用了CONFIG_RTE_MALLOC_DEBUG,它可能包含一个哨兵值,并在开始时为每个memseg指定一个适当的元素头。然后将FREE元素添加到malloc堆的空闲链表中。
当应用程序调用类似malloc功能的函数时,malloc函数将首先为调用线程索引lcore_config结构,并确定该线程的NUMA节点。NUMA节点将作为参数传给heap_alloc()函数,用于索引malloc_heap结构数组。参与索引参数还有大小、类型、对齐方式和边界参数。
函数heap_alloc()将扫描堆的空闲链表,尝试找到一个适用于所请求的大小、对齐方式和边界约束的内存块。
当已经识别出合适的空闲元素时,将计算要返回给用户的指针。紧跟在该指针之前的内存的高速缓存行填充了一个malloc_elem头部。由于对齐和边界约束,在元素的开头和结尾可能会有空闲的空间,这将导致已下行为:
从现有元素的末尾分配内存的优点是不需要调整空闲链表,空闲链表中现有元素仅调整大小指针,并且后面的元素使用“prev”指针重定向到新创建的元素位置。
3.4.4.3.内存释放
要释放内存,将指向数据区开始的指针传递给free函数。从该指针中减去malloc_elem结构的大小,以获得内存块元素头部。如果这个头部类型是PAD,那么进一步减去pad长度,以获得整个块的正确元素头。
从这个元素头中,我们获得指向块所分配的堆的指针及必须被释放的位置,以及指向前一个元素的指针,并且通过size字段,可以计算下一个元素的指针。这意味着我们永远不会有两个相邻的FREE内存块,因为他们总是会被合并成一个大的块。
如果支持在运行时释放页面,并且free元素包含一个或多个页面,则可以释放这些页面并将其从堆中删除。 如果使用命令行参数启动DPDK以预分配内存(-m或--socket-mem),那么在启动时分配的那些页面将不会被释放。
任何成功的释放事件都将触发回调,用户应用程序和其他DPDK子系统可以注册该回调。
https://www.jianshu.com/p/7546211cff62