博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
DPDK编程指南-3(环境抽象层)
阅读量:4055 次
发布时间:2019-05-25

本文共 14770 字,大约阅读时间需要 49 分钟。

作者:半天妖

链接:

3. 环境抽象层

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

EAL提供的服务如下:

  • DPDK的加载和启动:DPDK和特定的应用程序链接成一个独立进程,并以某种方式加载。
  • CPU亲和性设置:EAL提供了将执行单元分配给特定Core及创建执行实例的机制。
  • 系统内存预留:EAL实现了不同区域内存预留,例如用于设备交互的物理内存。
  • PCI地址抽象:EAL提供了对PCI地址空间的访问接口
  • 跟踪调试功能:日志信息,堆栈打印、异常挂起等等。
  • 公用功能:提供了标准Libc库缺失的自旋锁、原子计数器等。
  • CPU特征识别:运行时确定是否支持指定功能,如Intel AVX。确定当前CPU是否支持二进制编译的功能集。
  • 中断处理:提供接口用于向中断注册/解注册中断处理回调函数。
  • 告警功能:提供接口用于设置/取消指定时间环境下运行的毁掉函数。

3.1. Linux用户执行环境中的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系统接口实现。

3.1.1.初始化及核心启动

部分初始化操作从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表等,必须作为整个程序初始化的一部分,在主逻辑核上完成。创建和初始化这些对象的函数不是多线程安全的,但是,一旦初始化完成,这些对象本身可以作为安全线程运行。

3.1.2. 停止和清理

在初始化期间,DPDK核心组件可以分配EAL资源,比如巨页内存。运行rte_eal_init()分配的内存可以调用rte_eal_cleanup() 进行清除。详细信息请参阅API文档。

3.1.3.多进程支持

Linux  EAL允许多进程和多线程部署模式。详细信息请参阅“多进程支持”章节描述。

3.1.4.内存映射发现及内存预留

大型连续的物理内存分配是通过hugetlbfs内核文件系统来实现的。EAL提供了相应的接口用于预留指定名字的连续内存空间。这个API同时会将这段连续物理地址空间返回给用户程序。

DPDK内存子系统有两个模式可以操作:动态模式和传统模式。下面详细介绍了这两种模式。

注意:内存申请是使用rte_malloc接口来做的,它也是hugetlbfs文件系统大页支持的。

  • 动态内存模式

linux当前只支持这种模式。

在这种模式中,DPDK应用程序的巨页使用量会根据应用的需求增加或减少。通过rte_malloc()rte_memzone_reserve(),或其它方式分配的任何内存会导致更多的系统巨页被预留。

这种模式的内存分配不能保证是 IOVA-contiguous(连续的IO地址)。如果要求大块的IOVA-contiguous(这里的“large”意思是超过一页),推荐所有物理设备使用VFIO驱动(所以IOVA和VA地址可以相同,从而可以完全绕过物理地址),或者使用传统内存模式。

 分配IOVA-contiguous的大块内存,推荐使用rte_memzone_reserve()函数并带上RTE_MEMZONE_IOVA_CONTIG标识指定。通过这种方式,无论使用哪种内存模式,内存分配器确保返回满足需求的预留内存或者分配失败。

这种模式不需要在启动时通过-m--socket-mem命令行参数预分配任何内存,然而还是可以这么做,在这种情况下预分配内存将被固定(被固定的意思是永远不会返回给操作系统)。这种模式可以分配更多的巨页或者释放巨页,但是任何的预分配的内存是不会被释放的。如果不使用-m或--socket-mem指定,不会预分配内存,所有的内存会根据运行需求而分配。

动态内存模式中其他可用的选项是--single-file-segments命令行选项。这个选项将多个页面放进一个独立文件中(每个消息列表),而不是每个页面创建一个文件。这通常是不需要的,但在用户空间vhost这种情况下还是非常有用的,vhost传输给VirtIO的页文件描述符数量有限制。

如果应用程序(或者DPDK内部代码,比如设备驱动)希望收到新分配内存的通知,它可以调用rte_mem_event_callback_register()函数注册一个内存事件回调函数。任何DPDK内存映射发生改变都会调用此函数。

如果应用程序(或者DPDK内部代码,比如设备驱动)希望收到分配内存超过指定阈值的通知(并有机会拒绝),可以通过rte_mem_alloc_validator_callback_register() 函数注册配置验证回调函数。

EAL提供一个默认的验证回调函数,通过--socket-limit 命令行选项使能,DPDK应用通过这个参数设置一个最大内存分配量供应用使用。

  • 传统内存模式

通过使用指定--legacy-mem命令行参数使能这个模式。这个开关对FreeBSD不起作用,因为FreeBSD只支持传统内存模式。

这个模式是EAL的历史行为,EAL在启动的时候预留所有的内存,并分类所有的内存为 IOVA-contiguous 巨块,不允许在运行中分配或释放巨页。

如果-m--socket-mem没有指定,那么整个可用的巨页将被预分配。

  • 巨页分配匹配

EAL通过指定--match-allocations命令行参数使能这个动作。只有linux支持这个参数,且不能和--legacy-mem--no-huge一起使用。

一些使用内存事件回调的应用程序需要求巨页能够正确释放,正如它们被分配的那样。这些应用程序也可能要求任何的malloc堆分配不要跨越两个不同的相关内存事件回调。巨页分配匹配可以使用这些类型的应用程序来满足这些需求。非常依赖于应用程序的内存分配模式可能会导致内存使用量增加。

  • 32-bit支持

现在32位模式下存在额外的限制。在动态内存模式中,默认情况下最多能预分配2 g的VA(虚拟地址)空间,并且它们是从master lcore所在的的NUMA节点上分配的,除非使用--socket-mem参数指定每个NUMA节点分配指定大小内存。

在传统内存模式中,VA空间只会预分配请求指定大小的内存段(需保持IOVA-contiguousness)。

  • 最大内存量

所有能使用的虚拟地址空间都是DPDK程序在启动时预分配的。所以需要指定DPDK应用程序的最大内存限制量。DPDK内存存储于段页表中,每个段页表是一个物理页面。可以通过编辑以下配置变量更改启动时预分配的虚拟内存的数量:

CONFIG_RTE_MAX_MEMSEG_LISTS控制DPDK段页表数量

CONFIG_RTE_MAX_MEM_MB_PER_LIST控制每个段页表内存大小(M)
CONFIG_RTE_MAX_MEMSEG_PER_LISTcontrols how many segments each segment can have
CONFIG_RTE_MAX_MEMSEG_PER_TYPE控制每种内存类型的段页数(内存类型由页面大小和NUMA节点号组成)
CONFIG_RTE_MAX_MEM_MB_PER_TYPE控制每种内存类型的内存大小(M)
CONFIG_RTE_MAX_MEM_MB配置全局变量限制DPDK最大预留内存量

一般情况,这些选项不需要设置。

!注意:不能将预分配巨页内存和预分配虚拟内存相混淆!所有DPDK进程在启动时预分配虚拟内存。巨页可以迟些再映射到预分配虚拟地址空间(如果使能了动态内存模式),和可选择在启动时映射。

  • 段文件描述符

在linux中,在大多数情况下,EAL会存储段文件描述符。由于glibc潜在的局限性,当使用较小的页大小时会是一个问题。例如,linux API调用(比如select())不能正常工作,因为glibc不支持超过某个数量的文件描述符。

关于这个问题有两个解决方案。推荐使用--single-file-segments模式,因为这种模式不会为每个页面创建一个文件描述符。而且能保持对Virtio 和 vhost-user的向后兼容性。这个选项在使用--legacy-mem模式时无效。

另一个方案是使用更大的页尺寸。因为这样更少的页面会存储在相同的存储区,更少的文件描述符存在于EAL中。

3.1.5. 外部分配内存的支持

DPDK中可以使用外部分配的内存。两种方法分配的外部内存能正常工作:malloc堆栈API和手工内存管理。

  • 使用堆栈API分配外部内存

DPDK中推荐使用一系列堆栈API分配外部内存。通过这种方式,支持外部分配内存是通过重载套接字ID实现的-正常情况下外部分配堆栈的套接字ID被认为是无效的。从指定外部分配存储请求分配是向DPDK分配器提供正确的套接字ID既不是直接调用(通过调用rte_malloc)也不是间接调用(通过调用指定的数据结构API,比如rte_ring_create)。使用这些API必须确保对DMA映射的外部分配内存可以在任何已经添加到DPDK的分配堆栈里的内存段上执行。

因为DPDK没有办法检查内存是否有效,所以必须由用户来检查。所有多进程同步也需要用户负责,确保所有关于内存添加,关联,分离,移除的调用正确有序。不需要关联到所有进程的存储区-只需要关联到需要的区域。

预期的工作流程如下:

  • 获得存储区指针
  • 创建一个命名堆栈
  • 添加存储区到堆栈
    1、如果没有指定IOVA表,IOVA地址被认为是无效的,并且DMA影射将不能正常工作
    2、其他进程在用之前必须先关联到这些存储区
  • 获取堆栈套接字ID
  • 使用通用的DPDK分配程序(分配程序使用了提供的套接字ID)。
  • 如果存储区不再需要,可以从堆栈中移除
    移除之前,其他使用此存储区的进程必须先进行分离

更多详细信息,请参考rte_malloc API文档,特别参考rte_malloc_heap_*系列函数调用。

  • 非DPDK API使用外部分配内存

虽然在DPDK中推荐使用堆栈API操作外部分配内存,但在某些情况下DPDK的堆栈API开销是不必须要的-例如,当使用手动内存管理方式管理外部分配区域。为了支持这些情况,外部分配内存并不作为通用DPDK工作流的一部分,还有另外一系列在rte_extmem_*命名空间下的API。

这些API(顾名思义)意在允许在DPDK内部页表里注册和取消注册外部分配内存,比如像rte_virt2memseg等。为了和外部分配内存配合工作,内存添加这种方式不会被用于任何常规DPDK分配器,DPDK让用户应用来管理这种内存。

预期的工作流程如下:

  • 获得存储区指针
  • 注册内存到DPDK里
    1、如果没有指定IOVA表,IOVA地址被认为是无效的,并且DMA影射将不能正常工作
    2、其他进程在用之前必须先关联到这些存储区
  • 如果需要,使用rte_dev_dma_map执行DMA映射
  • 在你的应用里使用存储区
  • 如果存储区不再需要,可以取消注册
    1、如果是DMA映射区,取消注册之前必须先取消映射
    2、取消注册之前,其他使用此存储区的进程必须先进行分离

因为这些外部分配存储区不会被DPDK管理,所以这些存储区一旦被注册需要用户决定怎么使用和让他们做什么。

无Huge-TLB的Xen Domain 0支持

现有的内存管理是基于Linux内核的大页机制。然而,Xen Dom0并不支持大页,所以要将一个新的内核模块rte_dom0_mem加载上,以便避开这个限制。

EAL使用IOCTL接口用于通告Linux内核模块rte_mem_dom0去申请指定大小的内存块,并从该模块中获取内存段的信息。EAL使用MMAP接口来映射这段内存。对于申请到的内存段,在其内的物理地址都是连续的,但是实际上,硬件地址只在2M内连续。

PCI访问

EAL使用Linux内核提供的文件系统/sys/bus/pci来扫描PCI总线上的内容。内核模块uio_pci_generic提供了/dev/uioX设备文件及/sys下对应的资源文件用于访问PCI设备。DPDK特有的igb_uio模块也提供了相同的功能用于PCI设备的访问。这两个驱动模块都用到了Linux内核提供的uio特性(用户空间驱动)。

3.1.6.逻辑核变量和共享变量

注意: 逻辑核就是处理器的逻辑单元,有时也称为硬件线程。

默认的做法是使用共享变量。逻辑核变量的实现则是通过线程局部存储技术(TLS)来实现的,它提供了每个线程本地存储的功能。

3.1.7.日志

EAL提供了日志信息接口。默认情况下,在Linux应用程序中,日志信息被发送到syslog和console中。当然,用户可以通过使用不同的日志机制来重写DPDK中的日志函数。

3.1.7.1.跟踪与调试功能

Glibc中提供了一些调试函数用于打印堆栈信息。Rte_panic()函数可以产生一个SIG_ABORT信号,这个信号可以触发产生coredump文件,我们可以通过gdb来加载调试。

3.1.8.CPU特性标识

EAL可以在运行时查询CPU状态(使用rte_cpu_get_feature()接口),用于判断哪个CPU特性可用。

3.1.9.用户空间中断事件

  • 3.1.9.1.主机线程中的用户空间中断和报警处理

EAL创建一个主机线程用于轮询UIO设备描述文件描述符以检测中断。可以通过EAL提供的函数为特定的中断事件注册或注销回调函数,回掉函数在主机线程中被异步调用。EAL同时也允许像NIC中断那样定时调用中断处理回调。

注意: 在DPDK的PMD中,主机线程只对连接状态改变的中断处理,例如网卡的打开和关闭,以及设备突然移除中断。

  • 3.1.9.2.RX中断事件

PMD提供的报文收发程序并不只限制于轮询模式下执行。为了缓解小吞吐量下轮询模式对CPU资源的浪费,暂停轮询并等待唤醒事件发生是一种有效的手段。收包中断是这种场景的一种很好的选择,当然也不是唯一的。

EAL为事件驱动模式提供了相关的API。以Linuxapp为例,其实现依赖于epoll技术。每个线程可以监控一个epoll实例,而在实例中可以添加所有需要的wake-up事件文件描述符。事件文件描述符根据UIO/VFIO规范创建并映射到指定的中断向量上。从bspapp角度看,可以使用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()可能触发它注销自己的设备删除事件回调。因此,必须注意不要在中断处理程序上下文中关闭设备。必须重新安排这种关闭操作。

3.1.10.黑名单

EAL PCI设备的黑名单功能是用于标识指定的NIC端口,以便DPDK忽略该端口。可以使用PCIe设备地址描述符(Domain:Bus:Device:Function)将对应端口标记为黑名单。

3.1.11.杂项功能

每个架构不同的锁和原子操作(i686和x86_64)。

3.1.12. IOVA 模式配置

基于探测总线和IOMMU的配置自动检测IOVA模式,当存在未直接连接到总线的虚拟设备时,可能不会报告所需的寻址模式。通过给EAL命令行选项--iova-mode指定特定的值选择是使用物理地址(pa)或是使用虚拟地址(va)。

3.2.内存段和内存区域

物理内存映射就是通过EAL的这个特性实现的。物理内存块之间可能是不连续的,所有的内存通过一个内存描述符表进行管理,且表中的每个描述符指向一块连续的物理内存。

基于此,内存区域分配器的作用就是保证分配到一块连续的物理内存。这些区域被分配出来时会用一个唯一的名字来标识。

Rte_memzone描述符也在配置结构体中,可以通过rte_eal_get_configuration()接口来获取。通过名字访问一个内存区域会返回对应内存区域的描述符。

内存分配可以从指定开始地址和对齐方式来预留(默认是cache line大小对齐),对齐一般是以2的次幂来的,并且不小于高速缓存行的大小(64字节)对齐。内存区域也可以从2M或1G大小的内大页内存中获取,这两者系统都支持。

内存段和内存区域都是rte_fbarray数据结构进行存储,详情请参考DPDK API参考手册

3.3.多线程

DPDK通常为每个Core指定一个线程,以避免任务切换的开销。这有利于性能的提升,但不总是有效的,并且缺乏灵活性。

电源管理通过限制CPU的运行频率来提升CPU的工作效率。当然,我们也可以通过充分利用CPU的空闲周期来使用CPU的全部功能。

通过使用cgroup技术,CPU的使用量可以很方便的分配,这也提供了新的方法来提升CPU性能,但是这里有个前提,DPDK必须处理每个核上多个线程的上下文切换。

想要更多的灵活性,就要设置线程的CPU亲和性是针对对CPU集合而不是CPU。

3.3.1.EAL线程与逻辑核亲和性

术语“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组。

设置格式如下:

注意:-lcores=’[@cpu_set][,[@cpu_set],…]”其中lcore_set和cpu_set可以是单个数值,区间或者组。数值可以是“digit([0-9]+)”区间可以是“-”组可以是“([,,...])”

如果‘@cpu_set’值未指定,‘cpu_set’的值默认与‘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)使用这个选项,对于给定的lcoreID,可以分配对应的CPU组。它也兼容corelist(' - l')选项的模式。

3.3.2.非EAL线程支持

可以在任何用户线程(non-EAL线程)上执行DPDK任务上下文。在non-EAL pthread中,_lcore_id始终是LCORE_ID_ANY,它标识一个no-EAL线程的有效、唯一的_lcore_id。有些库可会使用一个唯一的ID替代(如TID),有些库将不受影响,有些库则会受到限制(如定时器和内存池库)。

所有这些影响将在“已知问题”章节中提到。

3.3.3.公用线程API

DPDK为线程操作引入了两个公共API rte_thread_set_affinity()rte_pthread_get_affinity()。当他们在任何线程上下文中调用时,将获取或设置线程本地存储(TLS)。

这些TLS包括_cpuset和_socket_id:

  • _cpuset存储了与线程亲和的CPU位图。
  • _socket_id存储了CPU set所在的NUMA节点。如果CPU set中的cpu属于不同的NUMA节点, _socket_id将设置为SOCKET_ID_ANY。

3.3.4. 控制线程API

可以使用公共API rte_ctrl_thread_create()创建控制线程。这些线程被用于管理或是基础任务,在DPDK内部支持多进程,处理中断。

这些线程在最初的亲和性CPU上调度,不包括数据平面和服务逻辑核。

例如,在一个8 cpu的系统上,启动一个DPDK应用带参数-l 2,3(指定数据平面核),通过taskset(Linux)和cpuset (FreeBSD)设置亲和性。

  • 没有配置亲和性,控制线程运行在0-1,4-7CPU上。
  • 配置亲和性限制在2-4,控制线程运行在CPU 4上。
  • 配置亲和性限制在2-3,控制线程运行在CPU 2上(在这种情况,master逻辑核默认没有CPU可用)。

3.3.5.已知问题

  • lrte_mempool
    rte_mempool在mempool中使用per-lcore缓存。对于non-EAL pthread,rte_lcore_id()无法返回一个合法的值。因此,当rte_mempool与non-EAL线程一起使用时,put/get操作将绕过默认的mempool缓存,这个旁路操作将造成性能损失。结合rte_mempool_generic_put()和rte_mempool_generic_get()可以在non-EAL线程中使用用户拥有的外部缓存。
  • rte_ring
    rte_ring支持多生产者入队和多消费者出队操作。然而,这是非抢占的,这使得rte_mempool操作都是非抢占的。
    注意:“非抢占”意味着:
  • 在给定的ring上做入队操作的pthread不能被另一个在同一个ring上做入队的pthread抢占
  • 在给定ring上做出对操作的pthread不能被另一个在同一ring上做出队的pthread抢占
    绕过此约束则可能造成第二个进程自旋等待,知道第一个进程再次被调度为止。此外,如果第一个线程被优先级较高的上下文抢占,甚至可能造成死锁。

这并不意味着不能使用它,简单讲,当同一个core上的多线程使用时,需要缩小这种情况.

  1. 它可以用于任一单一生产者或者单一消费者的情况。
  2. 它可以由多生产者/多消费者使用,要求调度策略都是SCHED_OTHER(cfs)。用户需要预先了解性能损失。
  3. 它不能被调度策略是SCHED_FIFO或SCHED_RR的多生产者/多消费者使用。
  • lrte_timer
    不允许在non-EAL pthread上运行rte_timer_manager()。但是,允许在non-EAL pthread上重置/停止定时器。
  • lrte_log
    在non-EAL pthread上,没有per thread
    loglevel和logtype,但是global
    loglevels可以使用。
  • lMisc
    在non-EAL pthread上不支持rte_ring,
    rte_mempool和rte_timer的调试统计信息。

3.3.6.cgroup控制

以下是cgroup控件使用的简单示例,在同一个核心($CPU)上两个线程(t0 and t1)执行数据包I/O。我们期望只有50%的CPU消耗在数据包IO操作上。

mkdir /sys/fs/cgroup/cpu/pkt_iomkdir  /sys/fs/cgroup/cpuset/pkt_ioecho $cpu  > /sys/fs/cgroup/cpuset/cpuset.cpusecho $t0  > /sys/fs/cgroup/cpu/pkt_io/tasksecho $t0  > /sys/fs/cgroup/cpuset/pkt_io/tasksecho $t1  > /sys/fs/cgroup/cpu/pkt_io/tasksecho $t1  > /sys/fs/cgroup/cpuset/pkt_io/taskscd  /sys/fs/cgroup/cpu/pkt_ioecho 100000  > pkt_io/cpu.cfs_period_usecho50000 > pkt_io/cpu.cfs_quota_us

3.4.内存申请操作

EAL提供了一个malloc API用于申请任意大小内存。

这个API的目的是提供类似malloc的功能,以允许从hugepage中分配内存并方便应用程序移植。《DPDK API参考手册》详细介绍了接口的功能。

通常,这些类型的分配操作不应该在数据面处理中进行,因为他们比基于池的分配慢,并且在分配和释放路径中使用了锁操作。但是,他们可以在配置代码中使用。

更多信息请参阅《DPDKAPI参考手册》中rte_malloc()函数描述。

3.4.1.Cookies

当CONFIG_RTE_MALLOC_DEBUG开启时,分配的内存包括保护字段,这个字段用于帮助识别缓冲区溢出。

3.4.2.对齐和NUMA限制

接口rte_malloc()传入一个对齐参数,该参数用于请求在该值的倍数上对齐的内存区域(这个值必须是2的幂次)。

在支持NUMA的系统上,对rte_malloc()接口调用将返回在调用函数的Core所在的插槽上分配的内存。DPDK还提供了另一组API,以允许在指定NUMA插槽上直接显式分配内存,或者分配另一个NUAM插槽上的内存。

3.4.3.用例

这个API旨在由初始化时需要类似malloc功能的应用程序调用。

需要在运行时分配/释放数据,在应用程序的快速路径中,应该使用内存池库。

3.4.4.内部实现

3.4.4.1.数据结构

Malloc库中内部使用两种数据结构类型:

  • lstruct malloc_heap:用于在每个插槽上跟踪可用内存空间
  • lstruct malloc_elem:库内部分配和释放空间跟踪的基本要素

3.4.4.1.1.malloc_heap

数据结构malloc_heap用于管理每个插槽上的可用内存空间。在内部,每个NUMA节点有一个堆结构,这允许我们根据此线程运行的NUMA节点为线程分配内存。虽然这并不能保证在NUMA节点上使用内存,但是它并不比内存总是在固定或随机节点上的方案更糟。

堆结构及其关键字段和功能描述如下:

  • lock:需要锁来同步对堆结构的访问。假定使用链表来跟踪堆中的可用空间,我们需要一个锁来防止多个线程同时处理该链表。
  • free_head:指向这个malloc堆的空闲结点链表中的第一个元素。
  • first:指向堆中的第一个元素
  • last:指向堆中的最后一个元素

 

Figure3‑2Example of a malloc heap and malloc elements within the malloclibrary

注意:数据结构malloc_heap并不会跟踪使用的内存块,因为除了要再次释放它们之外,它们不会被接触,需要释放时,将指向块的指针作为参数传递给free函数。

Fig. 3.2 Example of a malloc heap and malloc elements within the malloc library

3.4.4.1.2.malloc_elem

数据结构malloc_elem用作各种内存块的通用头结构。它以两种不同的方式使用,如上图所示:

  • 作为一个释放/申请内存的头部,正常使用
  • 作为内存块内部填充头
  • 作为内存结尾标记

结构中重要的字段和使用方法如下所述。

malloc堆是双链表,每个元素保留它前后元素的索引。由于来回分配大页内存,malloc相邻元素不一定在内存中也是相邻的,也因为malloc元素可能跨越多个页面,它的内容不一定是IOVA-contiguous——每个malloc元素只保证几乎是连续的。

!注意:如果没有描述上述三种用法之一中特定字段的用法,则可以假定该字段在这种情况下具有未定义的值,例如,对于填充头,只有“state”和“pad”字段具有有效值。

  • heap:这个指针指向了该内存块从哪个堆申请。它被用于正常的内存块,当他们被释放时,将新释放的块添加到堆的空闲列表中。
  • prev:这个指针用于指向紧跟着当前memseg的头元素。当释放一个内存块时,该指针用于引用上一个内存块,检查上一个块是否也是空闲。如果空闲,则将两个空闲块合并成一个大块。
  • next_free:这个指针用于将空闲块列表连接在一起。它用于正常的内存块,在malloc()接口中用于找到一个合适的空闲块申请出来,在free()函数中用于将内存块添加到空闲链表。
  • state:该字段可以有三个可能值:FREE, BUSY或PAD。前两个是指示正常内存块的分配状态,后者用于指示元素结构是在块开始填充结束时的虚拟结构,即,由于对齐限制,块内的数据开始的地方不在块本身的开始处。在这种情况下,pad头用于定位块的实际malloc元素头。对于结尾的结构,这个字段总是BUSY,它确保没有元素在释放之后搜索超过memseg的结尾以供其它块合并到更大的空闲块。
  • pad:这个字段为块开始处的填充长度。在正常块头部情况下,它被添加到头结构的结尾,以给出数据区的开始地址,即在malloc上传回的地址。在填充虚拟头部时,存储相同的值,并从虚拟头部的地址中减去实际块头部的地址。
  • size:数据块的大小,包括头部本身。对于结尾结构,这个大小需要指定为0,虽然从未使用。对于正在释放的正常内存块,使用此大小值替代“next”指针,以标识下一个块的存储位置,在FREE情况下,可以合并两个空闲块。

3.4.4.2.内存申请

在EAL初始化时,所有预分配的memseg都将作为malloc堆的一部分进行设置。这个设置是将每一段几乎连续的内存的一个头元素设置为free。然后将这个free的元素添加到malloc堆的free_list上。

运行时分配内存也会发生这个设置(如果支持),在这种情况下,新分配的内存页也会被添加到堆,和相邻的free内存段进行合并。

当应用程序调用类似malloc功能的函数时,malloc函数将首先为调用线程索引lcore_config结构,并确定该线程的NUMA节点。NUMA节点将作为参数传给heap_alloc()函数,用于索引malloc_heap结构数组。参与索引参数还有大小、类型、对齐方式和边界参数。

函数heap_alloc()将扫描堆的空闲链表,尝试找到一个适用于所请求的大小、对齐方式和边界约束的内存块。

当已经识别出合适的空闲元素时,将计算要返回给用户的指针。紧跟在该指针之前的内存的高速缓存行填充了一个malloc_elem头部。由于对齐和边界约束,在元素的开头和结尾可能会有空闲的空间,这将导致已下行为:

  • 检查尾随空间。如果尾部空间足够大,例如> 128字节,那么空闲元素将被分割。否则,仅仅忽略它(浪费空间)。
  • 检查元素开始处的空间。如果起始处的空间很小,<=128字节,那么使用填充头,这部分空间被浪费。但是,如果空间很大,那么空闲元素将被分割。

从现有元素的末尾分配内存的优点是不需要调整空闲链表 - 空闲链表中的现有元素仅调整其大小值,并且下一个/前一个元素具有“prev” “/”next“指针重定向到新创建的元素。

如果堆中没有足够的内存来满足分配请求,EAL会尝试从系统申请更多的内存(如果支持),如果成功分配后,将再次重试保留内存。在多进程场景中,所有primary和secondary进程将同步他们的内存映射,以确保任何有效的DPDK内存指针在所有当前正在运行的进程中始终保证有效。

其中一个进程中内存映射同步失败将导致分配失败,即使某些进程可能已成功分配内存。 除非primary进程确保所有其他进程已成功映射此内存,否则不会将内存添加到malloc堆中。

任何成功的分配事件都将触发回调,用户应用程序和其他DPDK子系统可以为其注册。 此外,如果新分配的内存超过用户设置的阈值,则会在分配之前触发验证回调,从而有机会允许或拒绝分配。

!注意:任何新页面的分配都必须经过primary进程。 如果主进程未激活,即使理论上可以这样做,也不会分配任何内存。 这是因为primary进程负责什么该映射什么不该映射,而每个Secondary进程都有自己的本地内存映射。 Secondary进程不更新共享内存映射,它们只将其内容复制到本地内存映射。

3.4.4.3.内存释放

要释放内存,将指向数据区开始的指针传递给free函数。从该指针中减去malloc_elem结构的大小,以获得内存块元素头部。如果这个头部类型是PAD,那么进一步减去pad长度,以获得整个块的正确元素头。

从这个元素头中,我们获得指向块所分配的堆的指针及必须被释放的位置,以及指向前一个元素和后一个元素的指针,然后检查这些下一个和前一个元素以查看它们是否也是FREE并且紧邻当前元素,如果是,则它们与当前元素合并。这意味着我们永远不会有两个相邻的FREE内存块,因为他们总是会被合并成一个大的块。

如果支持在运行时释放页面,并且free元素包含一个或多个页面,则可以释放这些页面并将其从堆中删除。 如果使用命令行参数启动DPDK以预分配内存(-m--socket-mem),那么在启动时分配的那些页面将不会被释放。

任何成功的释放事件都将触发回调,用户应用程序和其他DPDK子系统可以注册该回调。

转载地址:http://odqci.baihongyu.com/

你可能感兴趣的文章
CodeForces #196(Div. 2) 337D Book of Evil (树形dp)
查看>>
uva 12260 - Free Goodies (dp,贪心 | 好题)
查看>>
uva-1427 Parade (单调队列优化dp)
查看>>
【设计模式】学习笔记13:组合模式(Composite)
查看>>
hdu 1011 Starship Troopers (树形背包dp)
查看>>
hdu 1561 The more, The Better (树形背包dp)
查看>>
【设计模式】学习笔记14:状态模式(State)
查看>>
poj 1976 A Mini Locomotive (dp 二维01背包)
查看>>
斯坦福大学机器学习——因子分析(Factor analysis)
查看>>
项目导入时报错:The import javax.servlet.http.HttpServletRequest cannot be resolved
查看>>
不一定会执行finally代码块的两个例子
查看>>
LRUCache
查看>>
linux对于没有写权限的文件如何保存退出vim
查看>>
Windows下安装ElasticSearch6.3.1以及ElasticSearch6.3.1的Head插件
查看>>
IntelliJ IDEA 下的svn配置及使用的非常详细的图文总结
查看>>
【IntelliJ IDEA】idea导入项目只显示项目中的文件,不显示项目结构
查看>>
itellij idea导入web项目并部署到tomcat
查看>>
ssh 如何方便的切换到其他节点??
查看>>
JSP中文乱码总结
查看>>
AspectJ下载和安装
查看>>