目录
在Linux用户级执行环境中进行EAL
初始化和核心启动
关机和清理
多进程支持
内存映射发现和内存保留
支持外部分配的内存
每个核心和共享变量
日志
CPU功能识别
用户空间中断事件
黑名单
杂项功能
IOVA模式检测
IOVA模式配置
在Linux用户空间环境中,DPDK应用程序使用pthread库作为用户空间应用程序运行。
EAL使用hugetlbfs中的mmap()执行物理内存分配(使用巨大的页面大小来提高性能)。此内存公开给DPDK服务层,例如Mempool库。
此时,将初始化DPDK服务层,然后通过pthread setaffinity调用,将每个执行单元分配给特定的逻辑核心,以作为用户级线程运行。
时间参考由CPU时间戳计数器(TSC)或HPET内核API通过mmap()调用提供。
初始化的一部分由glibc的start函数完成。在初始化时也会执行检查,以确保CPU支持在配置文件中选择的微体系结构类型。然后,调用main()函数。核心初始化和启动在rte_eal_init()中完成(请参阅API文档)。它包含对pthread库的调用(更具体地说,是pthread_self(),pthread_create()和pthread_setaffinity_np())。
Linux应用程序环境中的EAL初始化
对象的初始化,例如内存区域,ring,内存池,lpm表和哈希表,应作为主lcore上整个应用程序初始化的一部分进行。这些对象的创建和初始化函数不是多线程安全的。但是,一旦初始化,对象本身就可以安全地同时在多个线程中使用。
在EAL初始化期间,核心组件可以分配诸如大页支持的内存之类的资源。rte_eal_init()
通过调用该rte_eal_cleanup()
函数可以释放在分配期间分配的内存。有关详细信息,请参阅API文档。
Linux EAL允许多进程以及多线程(pthread)部署模型。有关更多详细信息,请参见多进程支持一章 。
大型连续物理内存的分配是使用hugetlbfs内核文件系统完成的。EAL提供了一个API,可以在此连续内存中保留命名的内存区域。该存储区的保留内存的物理地址也由存储区保留API返回给用户。
DPDK内存子系统可以在两种模式下运行:动态模式和旧模式。两种模式在下面说明。
使用rte_malloc提供的API进行的内存保留也由hugetlbfs文件系统的页面支持。
当前,仅在Linux上支持此模式。
在这种模式下,DPDK应用程序对大页面的使用将根据应用程序的请求而增加和减少。任何内存分配通过rte_malloc()
, rte_memzone_reserve()
或其它方法,可能潜在地导致更大页面正在从系统预留。同样,任何内存重新分配都可能导致大页面释放回系统。
不能保证在此模式下分配的内存是IOVA连续的。如果需要大块的IOVA连续块(“大”块定义为“一页以上”),建议对所有物理设备使用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应用程序可以使用的最大内存量。
内存子系统内部使用DPDK IPC,因此不能将内存分配/回调和IPC混合使用:在与内存相关的或IPC回调中分配/释放内存是不安全的,并且在与内存相关的回调中使用IPC是不安全的。有关DPDK IPC的更多详细信息,请参见“ 多进程支持”一章 。
通过将--legacy-mem
命令行开关指定为EAL 启用此模式。此开关对FreeBSD无效,因为无论如何FreeBSD仅支持传统模式。
此模式模仿EAL的历史行为。也就是说,EAL将在启动时保留所有内存,将所有内存分类为大的IOVA连续块,并且不允许在运行时从系统获取或释放大页面。
如果既未指定也-m
未--socket-mem
指定,则整个可用的大页内存将被预先分配。
通过指定--match-allocations
到EAL 的命令行开关来启用此行为。此开关仅适用于Linux,并且--legacy-mem
nor 不支持 --no-huge
。
某些使用内存事件回调的应用程序可能需要完全释放分配的大页。这些应用程序可能还要求来自malloc堆的任何分配都不能跨越与两个不同的内存事件回调相关联的分配。这些类型的应用程序可以使用大页分配匹配来满足这两个要求。这可能会导致某些内存使用量增加,这在很大程度上取决于应用程序的内存分配模式。
在32位模式下运行时,存在其他限制。在动态内存模式下,默认情况下最多将预分配2 GB的VA空间,并且所有空间都将位于主lcore NUMA节点上,除非使用了--socket-mem
标志。
在传统模式下,只会为请求的段预分配VA空间(加上填充以保持IOVA连续性)。
在启动时会预先分配可用于DPDK进程中的大页映射的所有可能的虚拟内存空间,从而对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进程在启动时都会预分配虚拟内存。以后可以将大页映射到该预分配的VA空间(如果启用了动态内存模式),并且可以选择在启动时映射到它。
在Linux上,大多数情况下,EAL会将段文件描述符存储在EAL中。当使用较小的页面大小时,由于glibc
库的基本限制,这可能会成为问题。例如,Linux API调用之类的select()
可能无法正常工作,因为glibc
它不支持超过一定数量的文件描述符。
有两种可能的解决方案。推荐的解决方案是使用--single-file-segments
模式,因为该模式将不会在每个页面上使用文件描述符,并且它将与带有vhost-user后端的Virtio保持兼容性。使用--legacy-mem
模式时此选项不可用。
另一种选择是使用更大的页面尺寸。由于需要较少的页面来覆盖相同的内存区域,因此EAL将在内部存储较少的文件描述符。
可以在DPDK中使用外部分配的内存。使用外部分配的内存有两种工作方式:malloc堆API和手动内存管理。
建议使用一组malloc堆API,以在DPDK中使用外部分配的内存。这样,通过重载套接字ID来实现对外部分配的内存的支持-外部分配的堆将具有套接字ID,在正常情况下,这些套接字ID将被视为无效。请求从指定的外部分配内存中进行分配是一个直接(例如通过调用rte_malloc
)或间接(通过特定于数据结构的分配API) 向DPDK分配器提供正确的套接字ID的问题rte_ring_create
。使用这些API还可以确保为添加到DPDK malloc堆的任何内存段也执行DMA的外部分配内存的映射。
由于DPDK无法验证内存是否可用或有效,因此该责任由用户承担。所有多进程同步也是用户的责任,并确保所有添加/附加/分离/删除内存的调用均以正确的顺序进行。不需要在所有过程中都附加到存储区-仅在需要时附加到存储区。
预期的工作流程如下:
获取指向存储区的指针
创建一个命名堆
将内存区域添加到堆
获取用于堆的套接字ID
使用提供的套接字ID使用正常的DPDK分配过程
如果不再需要内存区域,则可以将其从堆中删除
如果不再需要堆,请将其删除
有关更多信息,请参阅rte_malloc
API文档,特别rte_malloc_heap_*
是函数调用系列。
建议使用堆API,在DPDK中使用外部分配的内存,但在某些用例中,DPDK堆API的开销是不希望的-例如,在外部分配的区域执行手动内存管理时。为了支持在外部DPDK工作流中不使用外部分配的内存的用例,rte_extmem_*
命名空间下还有另一组API 。
这些API(顾名思义)旨在允许向DPDK的内部页表注册或从DPDK的内部页表中注销外部分配的内存,以允许API rte_mem_virt2memseg
等与外部分配的内存一起使用。以这种方式添加的内存将不适用于任何常规DPDK分配器;DPDK将保留此内存供用户应用程序管理。
预期的工作流程如下:
获取指向存储区的指针
在DPDK中注册内存
rte_dev_dma_map
根据需要执行DMA映射
在应用程序中使用存储区
如果不再需要存储区,则可以取消注册
由于这些外部分配的存储区域将不受DPDK的管理,因此,由用户应用程序决定如何注册它们以及如何使用它们。
lcore指处理器的逻辑执行单元,有时也称为硬件线程。
共享变量是默认行为。每线程核心变量是使用线程本地存储(TLS)实现的,以提供每线程本地存储。
EAL提供了一个日志记录API。默认情况下,在Linux应用程序中,日志既发送到syslog,也发送到控制台。但是,用户可以覆盖日志功能以使用其他日志记录机制。
跟踪和调试功能
有一些调试功能可以将堆栈转储到glibc中。rte_panic()函数可以自动引发SIG_ABORT,这可以触发gdb可读的核心文件的生成。
EAL可以在运行时查询CPU(使用rte_cpu_get_features()函数)以确定哪些CPU功能可用。
EAL创建一个主机线程以轮询UIO设备文件描述符以检测中断。回调可以由EAL函数针对特定的中断事件进行注册或取消注册,并在主机线程中异步调用。EAL还允许以与NIC中断相同的方式使用定时回调。
注意在DPDK PMD中,专用主机线程处理的唯一中断是那些用于链接状态更改(链接打开和链接关闭通知)和突然移除设备的中断。
每个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中断由ethdev API-'rte_eth_dev_rx_intr_ *'控制/启用/禁用。如果PMD还不支持它们,它们将返回失败。intr_conf.rxq标志用于打开每个设备的RX中断功能。
该事件由在总线级别卸下设备触发。它的基础资源可能已变得不可用(即,未映射PCI映射)。PMD必须确保在这种情况下,应用程序仍可以安全地使用其回调。
可以按照订阅链接状态更改事件的相同方式来订阅此事件。因此,执行上下文是相同的,即它是专用的中断主机线程。
考虑到这一点,应用程序很可能希望关闭已发出设备移除事件的设备。在这种情况下,调用 rte_eth_dev_close()
可以触发它取消注册自己的设备删除事件回调。必须注意不要从中断处理程序上下文中关闭设备。必须重新计划这种关闭操作。
EAL PCI设备黑名单功能可用于将某些NIC端口标记为黑名单,因此DPDK将忽略它们。使用PCIe *描述(Domain:Bus:Device.Function)标识要列入黑名单的端口。
锁和原子操作是基于体系结构的(i686和x86_64)。
通过考虑系统上当前可用的设备需要和/或支持的内容来选择IOVA模式。
在FreeBSD上,RTE_IOVA_PA始终是默认值。在Linux上,基于下面详细介绍的两步启发式检测IOVA模式。
第一步,EAL向每个总线询问有关IOVA模式的要求,然后确定首选的IOVA模式。
如果总线未表示要选择哪种IOVA模式,则使用以下逻辑选择默认值:
如果总线在其首选的IOVA模式上存在分歧,则由于该决定,部分总线将无法工作。
第二步检查首选模式是否符合“物理地址”可用性,因为这些仅适用于最新内核中的root用户。即,如果首选模式是RTE_IOVA_PA,但是无法访问物理地址,则EAL初始化会尽早失败,因为以后对设备的探测仍然会失败。
在大多数情况下,由于以下原因,首选使用RTE_IOVA_VA模式作为默认模式:
- 无论物理地址是否可用,所有驱动程序均应以RTE_IOVA_VA模式工作。
- 默认情况下,内存池首先使用来请求IOVA连续内存
RTE_MEMZONE_IOVA_CONTIG
。在RTE_IOVA_PA模式下,这很慢,并且可能会影响应用程序的启动时间。- 在VA模式下使用IOVA轻松启用大量IOVA连续内存用例。
预期所有PCI驱动程序都可以在RTE_IOVA_PA和RTE_IOVA_VA模式下工作。
如果PCI驱动程序不支持RTE_IOVA_PA模式,则该 RTE_PCI_DRV_NEED_IOVA_AS_VA
标志用于指示该PCI驱动程序只能在RTE_IOVA_VA模式下工作。
当检测到KNI内核模块时,首选RTE_IOVA_PA模式,因为在RTE_IOVA_VA模式下预期会降低性能。
当存在未直接连接到总线的虚拟设备时,基于探测总线和IOMMU配置的IOVA模式自动检测可能不会报告所需的寻址模式。为了便于将IOVA模式强制为特定值,--iova-mode
可以使用EAL命令行选项来选择物理寻址('pa')或虚拟寻址('va')。