DPDK Programmer’s Guide(3)环境抽象层(EAL)

官方文档查看地址:
http://doc.dpdk.org/guides/prog_guide/env_abstraction_layer.html
PDF下载地址:
https://www.intel.com/content/www/us/en/embedded/technology/packet-processing/dpdk/dpdk-programmers-guide.html

本篇难度系数:★★★★★

3.环境抽象层

环境抽象层(EAL)负责访问底层资源,如硬件和内存空间。它提供了一个通用接口(API),对应用程序和库隐藏环境细节。初始化例程的职责是决定如何分配这些资源(即内存空间、设备、计时器、控制台等等)。

EAL的典型服务包括:

  • 加载和启动DPDK: DPDK及其应用程序作为单个应用程序链接,必须通过某种方式加载。
  • 核心关联/分配过程:EAL提供了将执行单元分配给特定核心以及创建执行实例的机制。
  • 系统内存保留:EAL可以方便地保留不同的内存区域,例如用于设备交互的物理内存区域。
  • 跟踪和调试功能:日志、dump_stack、panic等等。
  • 实用函数:libc中没有提供的自旋锁和原子计数器。
  • CPU特性识别:在运行时确定是否支持特定的特性,例如Intel®AVX。确定当前CPU是否支持编译二进制文件所针对的特性集。
  • 中断处理:注册/取消注册回调到特定中断源的接口。
  • 报警功能:接口设置/删除回调要运行在特定的时间。

3.1在Linux-userland用户空间执行环境中的EAL
在Linux用户空间环境中,DPDK应用程序使用pthread库作为一个用户空间应用程序运行。

EAL在hugetlbfs中使用mmap()执行物理内存分配(使用huge page sizes巨页大小来提高性能)。该内存公开给DPDK服务层,比如Mempool库。

此时,DPDK服务层将被初始化,然后通过pthread setaffinity调用,将每个执行单元分配给一个特定的逻辑核心,作为用户级线程运行。

时间引用由CPU时间戳计数器(TSC)或HPET内核API通过mmap()调用提供。

3.1.1初始化和核心启动
部分初始化是由glibc的start函数完成的。在初始化时还会执行检查,以确保配置文件中选择的微体系结构类型得到CPU的支持。然后,调用main()函数。核心初始化和启动在rte_eal_init()中完成(请参阅API文档)。它包括对pthread库的调用(更具体地说,pthread_self()、pthread_create()和pthread_setaffinity_np())。
在这里插入图片描述
请注意

  • 对象的初始化,如内存区、环、内存池、lpm表和哈希表,应该作为主lcore上整个应用程序初始化的一部分来完成。这些对象的创建和初始化函数不是多线程安全的。然而,一旦初始化,对象本身就可以安全地同时在多个线程中使用。

3.1.2关闭和清理
在初始化EAL资源时,核心组件可以分配hugepage支持的内存等资源。通过调用rte_eal_cleanup()函数,可以释放在rte_eal_init()期间分配的内存。有关详细信息,请参阅API文档。

3.1.3多进程的支持
Linux EAL允许多进程和多线程(pthread)部署模型。有关更多细节,请参见第1章多进程支持。

3.1.4内存映射发现和内存保留
大型连续物理内存的分配是使用hugetlbfs内核文件系统完成的。EAL提供了一个API来在这个连续内存中保留命名的内存区域。内存区域保留API还将为该内存区域保留的内存的物理地址返回给用户。

DPDK内存子系统可以运行两种模式:动态模式和遗留模式。下面将解释这两种模式。

请注意

  • 使用rte_malloc提供的api完成的内存保留也由来自hugetlbfs文件系统的页面支持。

>动态内存模式

目前,这种模式只支持Linux。

在这种模式下,DPDK应用程序对hugepages的使用将根据应用程序的请求进行增减。通过rte_malloc()rte_memzone_reserve()或其他方法进行的任何内存分配都可能导致从系统中保留更多的大页面。类似地,任何内存释放位置都可能导致大量页面被释放回系统。

在此模式下分配的内存不能保证是 IOVA-连续的。如果需要大量的IOVA- continuous(将“large”定义为“不止一个页面”),建议对所有物理设备使用VFIO驱动程序(以便IOVA和VA地址可以相同,从而完全绕过物理地址),或者使用遗留内存模式。

对于必须是IOVA-连续的内存块,建议使用rte_memzone_reserve()函数,并指定RTE_MEMZONE_IOVA_CONTIG标志。这样,内存分配器将确保,无论使用何种内存模式,要么保留的内存满足需求,要么分配失败。

不需要在启动时使用-m--socket-mem命令行参数预先分配任何内存,但是仍然可以这样做,在这种情况下,预先分配的内存将被“固定”(即应用程序永远不会释放回系统)。可以分配更多的大页面,并释放它们,但是不会释放任何预先分配的页面。如果既没有指定-m也没有指定--socket-mem,那么就不会预先分配内存,而是根据需要在运行时分配所有内存。

在动态内存模式中使用的另一个可用选项是--single-file-segments命令行选项。这个选项将把页面放在单个文件中(每个memseg列表),而不是每个页面创建一个文件。这通常是不需要的,但是对于像userspace vhost这样的用例非常有用,因为只有有限的页面文件描述符可以传递给VirtIO

如果应用程序(或dpdk内部代码,例如设备驱动程序)希望接收关于新分配内存的通知,可以通过rte_mem_event_callback_register()函数注册内存事件回调。这将在DPDK的内存映射发生更改时调用回调函数。

如果应用程序(或dpdk内部代码,例如设备驱动程序)希望得到关于指定阈值以上内存分配的通知(并有机会拒绝它们),那么还可以通过rte_mem_alloc_validator_callback_register()函数使用分配验证器回调。

EAL提供了一个默认的验证器回调,它可以通过一个--socket-limit命令行选项启用,这是一种限制DPDK应用程序可以使用的最大内存量的简单方法。

>遗留内存模式

通过指定--legacy-mem命令行切换到EAL,可以启用此模式。这个切换对FreeBSD没有影响,因为FreeBSD只支持遗留模式。

这种模式模仿了EAL的历史行为。也就是说,EAL将在启动时保留所有内存,将所有内存排序为大的iova连续块,并且不允许在运行时从系统中获取或释放大型页面。

如果没有指定-m--socket-mem,那么将预先分配整个可用的hugepage内存。

>Hugepage分配匹配

通过将--match-allocations命令行开关指定到EAL,可以启用此行为。这个开关只支持linux,不支持--legacy-mem--no-huge

一些使用内存事件回调的应用程序可能要求释放与分配是完全相同的大页。这些应用程序还可能要求malloc堆中的任何分配不能跨与两个不同内存事件回调关联的分配。这些类型的应用程序可以使用Hugepage分配匹配来满足这两个需求。这可能导致一些内存使用量的增加,这在很大程度上取决于应用程序的内存分配模式。

>32-bit support

在32位模式下运行时还存在其他限制。在动态内存模式下,默认情况下将预先分配最大2G的VA空间,所有这些空间都将位于主lcore NUMA节点上,除非使用--socket-mem标志。

在遗留模式下,VA空间只会为被请求的段预先分配(加上填充,以保持iova的连续性)。

>最大存储量

DPDK进程中所有可能用于hugepage映射的虚拟内存空间都在启动时预先分配,从而为DPDK应用程序的内存容量设置了上限。DPDK内存存储在段列表中,每个段严格来说是一个物理页面。可以通过编辑以下配置变量来更改启动时预分配的虚拟内存数量:

  • CONFIG_RTE_MAX_MEMSEG_LISTS控制DPDK可以拥有多少段列表
  • CONFIG_RTE_MAX_MEM_MB_PER_LIST控制每个段列表可以处理多少兆内存
  • CONFIG_RTE_MAX_MEMSEG_PER_LIST控制每个段可以有多少段
  • CONFIG_RTE_MAX_MEMSEG_PER_TYPE控制每种内存类型可以拥有多少段(其中“类型”定义为“页面大小+ NUMA节点”组合)
  • CONFIG_RTE_MAX_MEM_MB_PER_TYPE控制每种内存类型可以处理多少兆字节的内存
  • CONFIG_RTE_MAX_MEM_MB为DPDK可以保留的内存总量设置了一个全局最大值

通常,这些选项不需要更改。

请注意
预先分配的虚拟内存不要与预先分配的内存混淆!所有DPDK进程在启动时预先分配虚拟内存。稍后可以将Hugepages映射到预先分配的VA空间(如果启用了动态内存模式),并且可以在启动时选择将其映射到该空间。

>段文件描述符

在Linux上,在大多数情况下,EAL将在EAL中存储段文件描述符。由于glibc库的潜在限制,当使用较小的页面大小时,这可能会成为一个问题。例如,像select()这样的Linux API调用可能无法正常工作,因为glibc不支持超过一定数量的文件描述符。

这个问题有两种可能的解决办法。推荐的解决方案是使用--single-file-segments模式,因为该模式不会为每个页面使用文件描述符,并且它将保持与Virtio和vhost-user后端之间的兼容性。当使用--legacy-mem模式时,此选项不可用。

另一个选择是使用更大的页面大小。由于覆盖相同内存区域所需的页面更少,所以EAL将在内部存储更少的文件描述符。

3.1.5支持外部分配的内存

可以在DPDK中使用外部分配的内存。使用外部分配的内存有两种方式:malloc堆API和手工内存管理。

>为外部分配的内存使用堆API

使用一组malloc堆API是使用DPDK中外部分配的内存的推荐方法。通过这种方式,通过重载套接字ID来实现对外部分配内存的支持——外部分配的堆将具有套接字ID,在正常情况下,这些套接字ID将被认为是无效的。请求从指定的外部分配内存中进行分配是向DPDK分配器提供正确的套接字ID的问题,可以直接(例如通过调用rte_malloc),也可以间接(通过数据结构特定的分配API,例如rte_ring_create)。使用这些API还可以确保在添加到DPDK malloc堆的任何内存段上也可以执行针对DMA的外部分配内存的映射。

由于DPDK无法验证内存是否可用或有效,所以这个责任就落在了用户的肩上。所有多进程同步也是用户的责任,并确保所有添加/附加/分离/删除内存的调用都按正确的顺序执行。它不需要附加到所有进程的内存区域—只需要根据需要附加到内存区域。

预期工作流程如下:

  • 获取指向内存区域的指针
  • 创建一个命名堆
  • 将内存区域添加到堆中
    • 如果没有指定IOVA表,则会假定IOVA地址不可用,并且不会执行DMA映射
    • 其他进程必须先附加到内存区域,然后才能使用它
  • 获取用于堆的套接字ID
  • 使用正常的DPDK分配过程,使用提供的套接字ID
  • 如果不再需要内存区域,则可以从堆中删除它
    • 其他进程必须从该内存区域中分离,然后才能删除它
  • 如果不再需要heap,则删除它
    • 套接字ID将变为无效且不能重用
      有关更多信息,请参考rte_malloc API文档,特别是rte_malloc_heap_*函数调用家族。

>使用没有DPDK API的外部分配内存

虽然使用堆API是在DPDK中使用外部分配内存的推荐方法,但是在某些用例中,DPDK堆API的开销是不受欢迎的——例如,当在外部分配的区域上执行手动内存管理时。为了支持不将外部分配的内存用作普通DPDK工作流的一部分的用例,在rte_extmem_*名称空间下还有另一组API。

这些API(顾名思义)允许注册或注销DPDK内部页表的外部分配内存,允许rte_virt2memseg等API处理外部分配的内存。以这种方式添加的内存将不能用于任何常规的DPDK分配器;DPDK将把这些内存留给用户应用程序来管理。

预期工作流程如下:

  • 获取指向内存区域的指针
  • 在DPDK中注册内存
    • 如果没有指定IOVA表,则假定IOVA地址不可用
    • 其他进程必须先附加到内存区域,然后才能使用它
  • 如果需要,使用rte_dev_dma_map执行DMA映射
  • 在应用程序中使用内存区域
  • 如果不再需要内存区域,则可以注销它
    • 如果为DMA映射了该区域,则必须在注销内存之前执行取消映射
    • 其他进程必须从内存区域中分离,然后才能注销

由于这些外部分配的内存区域不会由DPDK管理,因此由用户应用程序决定如何使用它们以及注册后如何处理它们。

3.1.6每个lcore和共享变量

请注意

lcore指处理器的逻辑执行单元,有时也称为硬件线程。

共享变量是默认行为。每个lcore变量使用线程本地存储(TLS)实现,以提供每个线程的本地存储。

3.1.7日志

EAL提供了一个日志API。默认情况下,在Linux应用程序中,日志被发送到syslog和控制台。但是,用户可以重写log函数来使用不同的日志记录机制。

3.1.7.1跟踪和调试功能

在glibc中有一些调试函数可以转储堆栈。rte_panic()函数可以自动触发SIG_ABORT,后者可以触发生成核心文件,gdb可以读取该文件。

3.1.8CPU功能鉴定

EAL可以在运行时查询CPU(使用rte_cpu_get_features()函数)来确定哪些CPU特性可用。

3.1.9用户空间中断事件

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

EAL创建一个主机线程来轮询UIO设备文件描述符来检测中断。回调可以由EAL函数为特定的中断事件注册或注销,并在主机线程中异步调用。EAL还允许以与NIC中断相同的方式使用定时回调。

请注意
在DPDK PMD中,专用主机线程处理的惟一中断是用于更改链接状态(向上链接和向下链接通知)和突然删除设备的中断。

  • RX中断事件

每个PMD提供的接收和传输例程不限制自己在轮询线程模式下执行。要使用较小的吞吐量来缓解空闲轮询,最好暂停轮询并等待唤醒事件的发生。RX中断是这种唤醒事件的首选,但可能不是惟一的。

EAL为这种事件驱动的线程模式提供了事件api。以Linux为例,实现依赖于epoll。每个线程都可以监视一个epoll实例,其中添加了所有唤醒事件的文件描述符。事件文件描述符是根据UIO/VFIO规范创建并映射到中断向量的。从FreeBSD的角度来看,kqueue是另一种方法,但尚未实现。

EAL初始化事件文件描述符和中断向量之间的映射,而每个设备初始化中断向量和队列之间的映射。这样一来,EAL实际上并不知道特定向量上的中断原因。eth_dev驱动程序负责为后者的映射编写程序。

请注意
每个队列RX中断事件只允许在支持多个MSI-X向量的VFIO中使用。在UIO中,RX中断与其他中断原因共享相同的向量。在本例中,当RX中断和LSC(链接状态更改)中断都启用时(intr_conf.lsc == 1 && intr_conf.rxq == 1),只有前者是可行的。

RX中断由APIs - ‘rte_eth_dev_rx_intr_*’控制/启用/禁用。如果PMD还没有支持它们,它们将返回失败。intr_conf。rxq标志用于打开每个设备的RX中断功能。

  • 设备移除事件

此事件由在总线级别删除的设备触发。它的底层资源可能已经不可用(即PCI映射unmapped)。PMD必须确保在发生这种情况时,应用程序仍然可以安全地使用它的回调。

可以以订阅链接状态更改事件的相同方式订阅此事件。因此,执行上下文是相同的,即它是专用的中断主机线程。

考虑到这一点,应用程序很可能希望关闭发出设备删除事件的设备。在这种情况下,调用rte_eth_dev_close()可以触发它注销自己的设备删除事件回调。必须注意不要从中断处理程序上下文中关闭设备。有必要重新安排这种关闭操作的时间。

3.1.10黑名单

EAL PCI设备黑名单功能可用于将某些NIC端口标记为黑名单,因此它们将被DPDK忽略。要列入黑名单的端口使用PCIe* description(Domain:Bus:Device.Function)进行标识。

3.1.11Misc功能

锁和原子操作是按体系结构进行的(i686和x86_64)。

3.1.12IOVA模式配置

基于探测总线和IOMMU配置的IOVA模式的自动检测,在没有直接连接到总线的虚拟设备存在时,可能不会报告所需的寻址模式。为了方便将IOVA模式强制为特定值,EAL命令行选项--iova-mode可用于选择物理寻址(’ pa ‘)或虚拟寻址(’ va ')。

3.2内存段和内存区域(memzone)

物理内存的映射是由EAL中的这个特性提供的。由于物理内存可能有间隙,内存在一个描述符表中进行描述,每个描述符(称为rte_memseg)描述一个物理页面。

除此之外,memzone分配器的作用是保留物理内存的连续部分。当内存被保留时,这些区域由一个惟一的名称标识。

rte_memzone描述符也位于配置结构中。使用rte_eal_get_configuration()访问该结构。内存区域的查找(按名称)返回包含内存区域物理地址的描述符。

通过提供align参数,可以使用特定的起始地址对齐来保留内存区域(默认情况下,它们与高速缓存线大小对齐)。对齐值应为2的幂,且不小于高速缓存线大小(64字节)。还可以从2mb或1gb的大页中保留内存区域,前提是这两个内存区域在系统上都可用。

memsegs和memzone都使用rte_fbarray结构存储。有关更多信息,请参考DPDK API Reference。

3.3多个pthread

DPDK通常为每个CPU核固定一个pthread,以避免任务切换的开销。这可以显著提高性能,但是缺乏灵活性,而且并不总是有效的。

电源管理通过限制CPU运行时频率来帮助提高CPU效率。但是,也可以利用可用的空闲周期来充分利用CPU的全部功能。

通过利用cgroup,可以简单地分配CPU利用率配额。这给了另一种提高CPU效率的方法,但是,有一个先决条件;DPDK必须处理每个CPU核的多个pthread之间的上下文切换。

为了获得更大的灵活性,不仅可以将pthread关联设置为CPU,还可以设置为CPU集。

3.3.1EAL pthread与lcore的亲和力

术语“lcore”指的是一个EAL线程,它实际上是一个Linux/FreeBSD pthread。“EAL pthreads”由EAL创建和管理,并执行由remote_launch发出的任务。在每个EAL pthread中,都有一个名为_lcore_id的TLS(线程本地存储)用於惟一标识。由于EAL pthreads通常以1:1的比例绑定到物理CPU,所以_lcore_id通常等于CPU ID。

然而,当使用多个pthread时,EAL pthread与指定的物理CPU之间的绑定不再总是1:1。EAL pthread可能与CPU集有亲缘关系,因此_lcore_id与CPU ID不相同。因此,定义了一个EAL long选项“- lcore”来分配lcore的CPU亲缘关系。对于指定的lcore ID或ID组,该选项允许为该EAL pthread设置CPU集。

格式模式:

–lcores=’<lcore_set>[@cpu_set][,<lcore_set>[@cpu_set],...]’

“lcore_set”和“cpu_set”可以是单个数字、范围或组。

A number is a “digit([0-9]+)”; a range is “-”; a group is “(<number|range>[,<number|range>,…])”.

如果没有提供’ @cpu_set ‘值,’ cpu_set ‘的值将默认为’ lcore_set '的值。

For example, "--lcores='1,2@(5-7),(3-5)@(0,2),(0,6),7-8'" which means start 9 EAL thread;
    lcore 0 runs on cpuset 0x41 (cpu 0,6);
    lcore 1 runs on cpuset 0x2 (cpu 1);
    lcore 2 runs on cpuset 0xe0 (cpu 5,6,7);
    lcore 3,4,5 runs on cpuset 0x5 (cpu 0,2);
    lcore 6 runs on cpuset 0x41 (cpu 0,6);
    lcore 7 runs on cpuset 0x80 (cpu 7);
    lcore 8 runs on cpuset 0x100 (cpu 8).

使用此选项,可以为每个给定的lcore ID分配关联的cpu。它还兼容corelist(’ -l ')选项的模式。

3.3.2non-EAL pthread支持

可以将DPDK执行上下文与任何用户pthread(aka. Non-EAL pthreads)一起使用。在non-EAL pthread中,_lcore_id始终是LCORE_ID_ANY,它标识它不是一个具有有效的、惟一的_lcore_id的EAL线程。一些库将使用一个可选的惟一ID(例如TID),一些库将完全不受影响,还有一些库可以工作,但有限制(例如timer和mempool库)。

所有这些影响都在“已知问题( Known Issues)”一节中提到。

3.3.3公共线程API

为线程引入了两个公共API rte_thread_set_affinity()rte_thread_get_affinity()。当它们在任何pthread上下文中使用时,将设置/获取线程本地存储(TLS)。
这些TLS包括_cpuset和_socket_id:

  • _cpuset存储pthread被仿射到的cpu位图。
  • _socket_id存储CPU集中的NUMA节点。如果CPU集中的CPU属于不同的NUMA节点,则将_socket_id设置为SOCKET_ID_ANY。

3.3.4控制线程API

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

这些线程将被调度在CPU上,这是原始进程CPU关联性的一部分,其中数据平面和服务lcore被排除在外。

例如,在8个cpu系统上,用- l2,3 (dataplane core)启动一个dpdk应用程序,然后根据可以使用taskset (Linux)或cpuset (FreeBSD)等工具控制的关联配置

  • 如果没有关联配置,控制线程最终将位于0-1,4-7cpu上。
  • 当关联限制为2-4时,控制线程将终止于CPU 4。
  • 当关联限制为2-3时,控制线程将终止于CPU 2(主lcore,这是在没有CPU可用时的默认值)。

3.3.5已知的问题

  • rte_mempoolrte_
    rte_mempool在mempool中使用每个lcore的缓存。对于non-EAL pthreads, rte_lcore_id()将不会返回一个有效的数字。因此,现在,当rte_mempool与non-EAL pthreads一起使用时,put/get操作将绕过默认的mempool缓存,由于这种绕过,性能会受到影响。只有用户拥有的外部缓存才能在non-EAL上下文中与接受显式缓存参数的rte_mempool_generic_put()rte_mempool_generic_get()一起使用。

  • rte_ring
    rte_ring支持多生产者进入队列和多消费者退出队列。然而,它是不可抢占的,这对使rte_mempool不可抢占有一定的影响。

请注意
“非抢占式”约束是指:在给定环上执行多生产者队列的pthread不能被在同一环上执行多生产者队列的pthread抢占。在一个给定的环上执行多消费者下队列操作的pthread不能被另一个在同一环上执行多消费者下队列操作的pthread抢占。绕过这个约束可能会导致第二个pthread旋转,直到第一个pthread再次被调度。此外,如果第一个pthread被具有更高优先级的上下文抢占,它甚至可能导致死锁。

这意味着,涉及可抢占pthreads的用例应该仔细考虑使用rte_ring。

  1. 它可以用于可抢占的单生产者和单消费者用例。
  2. 它可以用于不可抢占的多生产者和可抢占的单消费者用例。
  3. 它可以用于可抢占的单生产者和不可抢占的多消费者用例。
  4. 它可以由可抢占的多生产者和/或可抢占的多消费者pthread使用,这些pthread的调度策略都是SCHED_OTHER(cfs)、SCHED_IDLE或SCHED_BATCH。在使用它之前,用户应该意识到性能损失。
  5. 多生产者/消费者pthread不能使用它,它们的调度策略是SCHED_FIFO或SCHED_RR。

或者,应用程序可以使用无锁堆栈mempool处理程序。在考虑这个处理程序时,请注意:
它目前仅限于x86_64平台,因为它使用的指令(16字节比较和交换)在其他平台上还不可用。
它的平均情况性能比非抢占式rte_ring差,但是软件缓存(例如mempool缓存)可以通过减少堆栈访问次数来缓解这种情况。

  • rte_timer

不允许在非eal pthread上运行rte_timer_manage()。但是,允许从non-EAL pthread重置/停止计时器。

  • rte_log

在非eal pthread中,没有每个线程的日志级别和日志类型,而是使用全局日志级别。

  • mis

cnon-EAL pthread中不支持rte_ring、rte_mempool和rte_timer的调试统计信息。

3.3.6cgroup控制

下面是cgroup控件使用的一个简单示例,在同一个核心($CPU)上有两个pthread (t0和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

3.4Malloc

EAL提供了一个malloc API来分配任意大小的内存。

这个API的目标是提供类似于malloc-like的函数,允许从hugepage内存分配,并促进应用程序移植。DPDK API参考手册描述了可用的函数。

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

有关更多信息,请参考DPDK API参考手册中的rte_malloc()函数描述。

3.4.1信息记录程序

当启用CONFIG_RTE_MALLOC_DEBUG时,分配的内存包含覆盖保护字段,以帮助识别缓冲区溢出。

3.4.2对齐和NUMA约束

rte_malloc()接受一个align参数,该参数可用于请求在该值的倍数上对齐的内存区域(该值必须是2的幂)(which must be a power of two)。

在支持NUMA的系统上,对rte_malloc()函数的调用将返回在执行调用的CPU核的NUMA套接字上分配的内存。还提供了一组api,允许显式内存分配在NUMA直接套接字,或分配NUMA插座的另一个核心所在,如果所使用的内存是一个逻辑核心以外的内存分配。

3.4.3用例

这个API用于在初始化时需要类mallocs函数的应用程序。对于在运行时分配/释放数据,在应用程序的快速路径中,应该使用内存池库。

3.4.4内部实现

3.4.4.1数据结构

malloc库内部使用了两种数据结构类型:

  • struct malloc_heap—用于在每个套接字的基础上跟踪空闲空间
  • struct malloc_elem—库中分配和自由空间跟踪的基本元素。

3.4.4.1.1结构:malloc_heap

malloc_heap结构用于基于每个套接字管理空闲空间。在内部,每个NUMA节点有一个堆结构,这允许我们根据这个线程运行的NUMA节点将内存分配给一个线程。虽然这并不保证内存将在NUMA节点上使用,但它并不比总是在固定或随机节点上分配内存的方案差。

堆结构的关键字段及其功能描述如下(见上图):

  • lock——lock字段用于同步对堆的访问。假设使用链表跟踪堆中的空闲空间,我们需要一个锁来防止两个线程同时操作该列表。
  • free_head——指向这个malloc堆的空闲节点列表中的第一个元素。
  • first——指向堆中的第一个元素。
  • last——指向堆中的最后一个元素。
    在这里插入图片描述Fig. 3.2 Example of a malloc heap and malloc elements within the malloc library

3.4.4.1.2结构:malloc_elem

malloc_elem结构用作各种内存块的通用头结构。它有两种不同的用法——都在上面的图表中显示:

  1. 作为一个头块上的空闲或分配的内存-正常情况下
  2. 作为内存块内的填充头

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

Malloc堆是一个双链表,其中每个元素都跟踪它的前一个和下一个元素。由于大分页内存可以来来去去,所以相邻的malloc元素在内存中不一定是相邻的。此外,由于malloc元素可以跨多个页面,所以它的内容也不一定是iova连续的——每个malloc元素只保证是虚拟连续的。

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

  • heap——这个指针是对分配这个块的堆结构的引用。它用于释放普通内存块时,将新释放的内存块添加到堆的空闲列表中。
  • priv——这个指针指向内存中以前的头元素/块。当释放一个块时,这个指针用于引用前一个块,以检查该块是否也是空闲的。如果是这样,并且这两个块立即相邻,那么这两个空闲块合并成一个更大的块。
  • next——这个指针指向内存中的下一个头元素/块。当释放一个块时,这个指针用于引用下一个块,以检查该块是否也是空闲的。如果是这样,并且这两个块立即相邻,那么这两个空闲块合并成一个更大的块。
  • free_list——这是一个指向堆的空闲列表中的前一个和下一个元素的结构。它只在普通内存块中使用;在malloc()上查找要分配的适当空闲块,在free()上将新释放的元素添加到空闲列表。
  • state——该字段可以有三个值之一:FREEBUSYPAD。前两个是显示正常的内存块的分配状态,后者是表明元素结构是一个虚拟的结构最终start-of-block填充,即在数据块的开始不是块本身的开始时,由于一致性约束。在这种情况下,pad头用于定位块的实际malloc元素头。
  • pad——它包含块开始处的填充的长度。对于普通块标头,它被添加到标头末尾的地址中,以给出数据区域开始的地址,即在malloc上传递回应用程序的值。在填充内的虚拟标头中,存储相同的值,并从虚拟标头的地址中减去该值,以生成实际块标头的地址。
  • size——数据块的大小,包括头本身。

3.4.4.2内存分配

在EAL初始化时,所有预分配的内存段都设置为malloc堆的一部分。这种设置包括在每个几乎相邻的内存段的开始处放置一个带有FREE的元素标头。然后,FREE元素被添加到malloc堆的free_list中。

只要在运行时分配内存(如果支持),这种设置也会发生,在这种情况下,新分配的页面也会添加到堆中,如果有相邻的空闲段,则与它们合并。

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

heap_alloc()函数将扫描堆的free_list,并尝试找到一个适合存储具有请求对齐和边界约束的请求大小的数据的空闲块。

当确定了合适的空闲元素后,将计算返回给用户的指针。紧位于这个指针之前的内存缓存行被一个struct malloc_elem头文件填充。由于对齐和边界约束,元素的开始和/或结束处可能存在自由空间,导致如下行为:

  • 检查尾随空格。如果尾随空间足够大, i.e.即> 128字节,则分割空闲元素。如果不是,那么我们就忽略它(浪费空间)。
  • 检查元素开头是否有空格。如果开始的空间很小,例如<=128字节,那么使用pad头,剩余的空间将被浪费。但是,如果剩余的空间更大,则会分割空闲元素。

分配内存的优势从现有的空闲列表的元素是没有调整需要,现有的元素在空闲列表的大小值调整,和下一个/之前的元素“上一页”/“下一个”重定向到新创建的元素的指针。

如果堆中没有足够的内存来满足分配请求,EAL将尝试从系统中分配更多的内存(如果支持),并且在成功分配之后,将再次尝试保留内存。在多处理场景中,所有主进程和辅助进程将同步它们的内存映射,以确保任何指向DPDK内存的有效指针在当前运行的所有进程中始终有效。

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

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

请注意
任何新页面的分配都必须经过主进程。如果主进程不是活动的,那么即使理论上可以分配内存,也不会分配内存。这是因为主进程的进程映射充当应该映射或不应该映射什么的权威,而每个辅助进程都有自己的本地内存映射。辅助进程不更新共享内存映射,它们只将其内容复制到本地内存映射。

3.4.4.3释放内存
为了释放内存区域,指向数据区域开始的指针被传递给空闲函数。从这个指针中减去malloc_elem结构的大小,得到块的元素头。如果这个头是PAD类型的,则从指针中进一步减去PAD长度,以获得整个块的适当元素头。

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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章