DPDK - CPU管理 - 01 EAL 1.1 EAL 在 Linux 用户态执行环境中的表现

EAL是什么

EAL:Environment Abstraction Layer,环境抽象层。EAL 是 DPDK 最核心的基础模块之一,它负责整个运行环境的初始化,包括 CPU、内存、设备的配置与绑定。通过提供统一的抽象接口,EAL 隐藏了操作系统与硬件的复杂性,为上层应用程序提供了一个干净、可移植、可配置的运行环境。

EAL 提供的典型服务包括:

  1. DPDK 加载与启动:DPDK 和其应用程序被链接为一个单独的应用程序,必须通过某种方式进行加载。
  2. 核心亲和性 / 分配机制:EAL 提供将执行单元绑定到特定 CPU 核心上的机制,并支持创建执行实例(如线程或进程)。
  3. 系统内存预留:EAL 支持预留不同的内存区域,例如为设备交互预留的物理内存。
  4. 跟踪与调试功能:如日志记录、堆栈转储(dump_stack)、程序崩溃信息(panic)等。
  5. 通用工具函数:提供 spinlock(自旋锁) 和 atomic counter(原子计数器)等在标准 C 库(libc)中没有的功能。
  6. CPU 特性识别:运行时判断 CPU 是否支持特定特性(例如 Intel AVX),以及是否支持当前编译的二进制所需的特性集。
  7. 中断处理:提供接口注册/注销对特定中断源的回调处理函数。
  8. 定时器功能:提供接口设置或移除将在特定时间点运行的回调函数。

在 Linux 用户态执行环境中的 EAL

在 Linux 用户空间环境中,DPDK 应用作为一个用户态应用程序运行,并使用 pthread 库(POSIX 线程)来管理线程。

EAL 使用 mmap() 函数在 hugetlbfs 文件系统中分配物理内存(通过使用大页(huge page)来提升性能)。这些内存随后会暴露给 DPDK 的服务层,例如内存池库(Memory Pool Library)。

在这个阶段,DPDK 的服务层会被初始化。随后,通过 pthread_setaffinity 系统调用,每个执行单元(通常是一个线程)会被绑定到特定的逻辑 CPU 核心,以用户态线程的形式运行。

时间基准(time reference)由 CPU 的时间戳计数器(TSC) 或通过 mmap() 映射的 HPET(高精度定时器)内核 API 提供。

执行流程

  1. 应用调用 rte_eal_init() 初始化 EAL;
  2. EAL 映射 Hugepage 内存(mmap() + hugetlbfs);
  3. 初始化 DPDK 服务层(如内存池、定时器、设备抽象等);
  4. 创建 pthread 执行单元,绑定到特定逻辑核心;
  5. 执行主循环 lcore_main() 或等效逻辑;
  6. 使用 TSC 或 HPET 计时器作为定时参考;
  7. 开始处理数据包或执行指定的业务逻辑。

初始化与核心启动

一部分初始化工作是由 glibc 的 start 函数完成的。在初始化阶段,还会进行一个检查,以确保配置文件中选择的微架构类型是当前 CPU 所支持的。随后会调用 main() 函数。

核心的初始化与启动由 rte_eal_init() 完成(参见 API 文档)。该过程包含对 pthread 库的调用,特别是:

  • pthread_self():获取当前线程句柄;
  • pthread_create():创建新线程(即每个 lcore 执行单元);
  • pthread_setaffinity_np():将线程绑定到特定的逻辑核心上。

DPDK 初始化流程概述(Linux 用户空间)

  1. 进程启动(glibc 的 start 函数)
    • 在调用 main() 之前,glibc 执行一系列初始化操作,包括运行全局构造函数、环境变量设置等。
    • DPDK 的 EAL 初始化也从这里开始做一些 CPU 特性检查,比如当前配置是否适配运行环境。
  2. 调用 main() 函数
    • main() 中,DPDK 程序调用 rte_eal_init(argc, argv) 来初始化整个运行环境。
  3. 内存初始化:mmap + hugepage
    • EAL 使用 mmap() 映射位于 hugetlbfs 的大页内存,将其作为 DPDK 的共享物理内存区域(例如供 rte_mempoolrte_ring 使用)。
  4. 线程创建与核心绑定:pthread
    • 每个逻辑核心(lcore)由一个独立的 pthread 线程表示。
    • 通过 pthread_create() 创建线程后,使用 pthread_setaffinity_np() 将线程绑定到特定的 CPU 核心,以确保线程始终在指定核心运行,避免上下文切换带来的性能损耗。
  5. 时间参考初始化
    • EAL 初始化 TSC(时间戳计数器)或 HPET 定时器,以用于之后的延迟控制、计时器事件等。

为什么要进行核心线程绑定:DPDK 是 轮询式架构(polling model),每个 lcore 上的线程通常运行一个主循环,这样可以:

  • 减少 cache miss(缓存一致性问题);
  • 降低调度器带来的上下文切换;
  • 保证数据路径稳定和可预测性。

TSC 与 HPET 的选择:

TSC(Time Stamp Counter)

  • 高速、低延迟,适合计时精度要求高的场景,是 DPDK 默认的计时器。
  • 但在多 CPU 插槽或某些节能模式下,TSC 不一定可靠(跨核时钟可能不同步)。

HPET(High Precision Event Timer)

  • 稳定精确,但访问成本高(读取开销远大于 TSC)。
  • 通常只在 TSC 不可用或要求非常高时间精度时使用。
../_images/linuxapp_launch.svg

像内存区域(memory zones)、环形队列(rings)、内存池(memory pools)、LPM 表(最长前缀匹配表)以及哈希表(hash tables)这样的对象,应该在主逻辑核心(main lcore)上,作为整个应用程序初始化的一部分进行初始化。这些对象的创建和初始化函数不是线程安全的

但是,一旦初始化完成,这些对象本身就可以在多个线程中安全地并发使用

关闭与清理

在 EAL 初始化期间,诸如 Hugepage 支持的内存等资源可能会被核心组件分配。通过调用 rte_eal_init() 分配的内存资源,可以通过调用 rte_eal_cleanup() 函数来释放。具体细节请参考 API 文档。

为什么需要清理?

  • rte_eal_init() 在启动阶段会分配大量资源,包括:
    • Hugepage 内存
    • 注册的 memzone、mempool、ring
    • 线程、定时器、中断回调
    • 内部结构体与全局状态信息

这些资源在程序退出时,如果不释放,会导致内存泄漏或资源残留,尤其是在 重复运行 DPDK 应用程序 时可能造成:

  • Hugepage 无法重新分配;
  • 共享对象名冲突(如 ring, memzone 名字在 /dev/hugepages 中残留);
  • VFIO 绑定失败等。

rte_eal_cleanup() 的作用:

  • 清理 EAL 初始化过程中注册的全局资源;
  • 释放 Hugepage 映射的内存;
  • 注销 memzone、mempool、ring 等资源;
  • 还原线程、中断、定时器等状态。

注意:不是所有资源都能完全自动释放,手动创建的 mempool、ring 等资源,仍然建议应用程序自行销毁

多进程支持(Multi-process Support)

Linux 平台下的 EAL 同时支持 多进程(multi-process)和 多线程(pthread) 的部署模型。详情请参阅章节《Multi-process Support》。

内存映射发现与内存预留

大块连续物理内存的分配是通过 Hugepage 实现的。EAL 提供了一套 API,用于在这块连续内存中预留具名的内存区域(memory zone)。该内存预留 API 同时也会返回该 memory zone 对应的 物理地址 给用户。

DPDK 的内存子系统可以在两种模式下运行:

  1. 动态模式(dynamic mode)
  2. 传统模式(legacy mode)

动态模式

在此模式下,DPDK 应用对 hugepage 的使用量会根据应用的请求 动态增长或收缩。任何通过 rte_malloc()rte_memzone_reserve() 或其他方法的内存分配操作,都可能会导致系统中新增 hugepage;而任何内存释放操作,也可能会将 hugepage 归还给系统。

在此模式下分配的内存不保证 IOVA 连续(I/O 虚拟地址连续)
如果你需要大块的 IOVA 连续内存(“大块”指的是“超过一页”),建议:

  • 要么使用 VFIO 驱动绑定所有物理设备(IOVA = VA,绕过物理地址);
  • 要么使用 legacy 模式。

对于确实要求 IOVA 连续的内存块,建议使用 rte_memzone_reserve() 并指定 RTE_MEMZONE_IOVA_CONTIG 标志。这样,内存分配器将确保无论当前使用哪种内存模式,要么分配满足要求,要么直接失败。

在该模式下,无需通过 -m--socket-mem 命令行参数预分配内存,但仍可以这么做。预分配的内存会被 “锁定”(pinned),即不会被应用释放回系统。即使如此,仍然可以动态申请/释放更多内存,但预分配的部分是不可回收的。

如果没有指定 -m--socket-mem,那么不会预先分配任何内存,所有内存会在运行时按需分配。

此模式下,还可使用 --single-file-segments 命令行选项。该选项会将一个 memseg list 中的页放到一个文件里,而不是每页一个文件。虽然通常不需要,但在如 userspace vhost 这类场景下可能有用(此类应用对可传递的页文件描述符数量有限制)。

如果应用(或 DPDK 内部代码,例如设备驱动)希望在内存新增时收到通知,可以通过 rte_mem_event_callback_register() 注册回调函数。当 DPDK 的内存映射发生变化时,会调用该函数。

如果应用希望在内存分配 超过指定阈值前 被通知,并有机会拒绝该分配,可以使用 rte_mem_alloc_validator_callback_register() 注册验证器回调函数。

EAL 提供了默认的验证器回调函数,可以通过 --socket-limit 命令行参数启用,以限制 DPDK 应用可用的最大内存量。

警告:

DPDK 的内存子系统内部使用了 IPC,因此内存分配/回调 与 IPC 不能混用

  • 不可在内存相关的回调函数中执行内存分配或释放操作;
  • 也不可在这些回调中使用 IPC;
  • 同样,在 IPC 回调中执行内存操作也不安全。

传统模式

该模式通过向 EAL 添加命令行参数 --legacy-mem 来启用。该选项在 FreeBSD 上无效,因为 FreeBSD 仅支持传统模式

此模式模拟了 EAL 的历史行为,即:

  • 在启动时一次性预留所有内存
  • 将所有内存整理成大的 IOVA 连续块
  • 运行时不允许再从系统中申请或释放 hugepage 内存

如果没有指定 -m--socket-mem 参数,则会预分配 系统中所有可用的 hugepage 内存

动态模式与传统模式对比

对比项 动态模式 传统模式
Hugepage 分配时机 运行时按需分配 启动时一次性分配
内存释放 支持运行时释放 不可释放
IOVA 连续性 不保证(除非特殊指定) 默认保证大块 IOVA 连续
多进程友好度 相对复杂(映射需一致) 更稳定,适合多进程
灵活性
命令行启用方式 默认 --legacy-mem
是否需要预设 -m 可选 可选,默认全分配

Hugepage 分配匹配

该行为通过在 EAL 启动参数中指定 --match-allocations 启用。此选项 仅在 Linux 上受支持,并且 --legacy-mem--no-huge 参数不兼容

某些应用程序在使用 内存事件回调(memory event callbacks) 时,可能需要 hugepage 在释放时的方式与其分配时完全一致。这些应用还可能要求,任何来自 malloc 堆的内存分配,不应跨越两个属于不同回调事件的分配区域

对于此类应用,hugepage allocation matching 机制可以满足这两项需求。启用后,可能会增加内存使用量,这取决于应用的内存分配模式。

32bit支持

当以 32 位模式 运行时,会有一些额外的限制。

  • 动态内存模式(dynamic memory mode)下,默认最多会预分配 2GB 的虚拟地址(VA)空间,并且这部分内存将 全部分配在主逻辑核心所处的 NUMA 节点上,除非通过 --socket-mem 参数指定了具体的 NUMA 分布。
  • 传统内存模式(legacy mode)下,只有被明确请求的内存段(segment)才会被预分配 VA 空间(加上一些填充,以确保 IOVA 连续性)。

最大内存限制

DPDK 进程在启动时,会预先分配所有可能用于 hugepage 映射的虚拟内存空间,这就为 DPDK 应用程序可使用的内存量设定了一个上限。

DPDK 的内存是以“段列表(segment lists)”的形式进行管理的。每个“段(segment)”严格对应一个物理页。

你可以通过修改以下配置变量,来改变启动时预分配的虚拟内存数量:

配置变量 含义
RTE_MAX_MEMSEG_LISTS 控制 DPDK 最多可以拥有多少个段列表
RTE_MAX_MEM_MB_PER_LIST 控制每个段列表最多能管理多少 MB 内存
RTE_MAX_MEMSEG_PER_LIST 控制每个段列表中最多能包含多少个 segment(页)
RTE_MAX_MEMSEG_PER_TYPE 控制每种内存类型最多能拥有多少个 segment(其中“类型”是 “页大小 + NUMA 节点” 的组合)
RTE_MAX_MEM_MB_PER_TYPE 控制每种内存类型最多能管理多少 MB 内存
RTE_MAX_MEM_MB 设置 DPDK 应用可预留的内存总量上限(全局)

通常情况下,无需修改这些配置。


注意:

“预分配虚拟内存” 与 “预分配 hugepage 内存” 是两回事!

虚拟地址空间:每个 DPDK 进程在启动时都会预留一大片 VA 空间(用于未来映射 hugepage),即使你不立即分配物理页;

hugepage:指实际的物理内存页,它们会在运行时(如启用动态模式时)被映射到预分配的 VA 空间中,或在启动时直接映射(如使用 legacy 模式)。

巨页映射

Linux 支持以下映射方式组合(可以任意组合):

项目 默认行为 可选行为
映射来源 使用 hugetlbfs 中的文件 或使用匿名映射(--in-memory
文件组织方式 每个 hugepage 对应一个文件 或使用单个文件映射多个页(--single-file-segments

hugetlbfs 映射(默认)

  • 适用于多进程架构,因为 从进程必须映射与主进程相同的 hugepage 文件
  • EAL 在指定的 --huge-dir 目录(或对应 hugepage size 的挂载点)中创建名为 rtemap_0 等文件。
  • 文件前缀(如 rte)可通过 --file-prefix 修改,用于多主进程场景(同一挂载点避免冲突)。
  • 每个 backing 文件通常对应一个 hugepage,使用期间始终保持打开与锁定状态。

如果 hugepage 太多,会导致打开文件数量过多,超出 ulimit -n 的 NOFILE 限制

动态内存模式下的行为(Linux)

  • 当从某个 backing 文件映射的全部页都被释放时,EAL 会移除该文件;
  • 但如果程序崩溃或内存未正确释放(如未调用 rte_free()),文件会残留,导致:
    • /sys/kernel/mm/hugepages/hugepages-*/free_hugepages 报告可用 hugepage 数减少;
    • 其他进程无法重新使用这些 hugepage。

为避免 hugetlbfs 污染,可使用 --huge-unlink 启动参数,令 EAL 在映射前就删除文件。

--huge-unlink=never 模式

  • 用于加快 EAL 启动(通常在应用重启时使用);
  • 跳过内核清零 hugepage 的步骤,直接 remap 旧文件,保留页中的“脏数据”;
  • 此模式下映射的内存段会打上 RTE_MEMSEG_FLAG_DIRTY 标记;
  • 内存分配器会检测脏段,并在使用如 rte_zmalloc() 分配时自动清除内存;
  • 此模式下,即使所有页已被释放,EAL 也不会删除 backing 文件,以便 应用重启时复用

匿名映射(--in-memory)模式

  • 不使用 hugetlbfs,因此:
    • 不支持多进程(因为其他进程无法访问相同的匿名映射);
    • 不需要 root 权限(但仍受限于 MEMLOCK,即最大锁定内存);
    • 不会留下文件,也无文件名冲突问题
  • 如果系统支持 memfd_create(2)(构建时与运行时都支持):
    • DPDK 内存管理器仍可为 memory segments 提供文件描述符;
    • 适用于 vhost-user 使用 VirtIO 时传递 fd;
    • 仍可能因打开文件数量过多而受限于 NOFILE
    • 可参考 Segment File Descriptors 部分优化。

段文件描述符

在 Linux 系统中,大多数情况下 EAL 会为每个内存段存储对应的 文件描述符(file descriptor,简称 FD)
当使用较小页面(page size)时,这可能会引发问题,主要是由于 glibc 库的底层限制。例如,如果文件描述符数量太多,某些 Linux API(如 select())可能无法正常工作,因为 glibc 不支持超过一定数量的 FD。

为了解决这个问题,有两个可行方案:

  1. 推荐方案:使用 --single-file-segments 模式。
    • 此模式不会为每一页都分配一个文件描述符;
    • 同时仍与 Virtio 的 vhost-user 后端保持兼容;
    • 注意:该选项不能与 --legacy-mem 模式同时使用
  2. 替代方案:使用更大的页大小(如 1GB hugepage)。
    • 因为使用较大的页面可以用更少的页覆盖相同内存区域;
    • 从而减少 EAL 内部存储的文件描述符数量。

Hugepage 工作线程栈

当指定了 --huge-worker-stack[=size] EAL 启动参数时,工作线程的栈(stack)会从 NUMA 节点本地的 hugepage 内存中分配

如果没有显式指定 size 参数,线程栈大小将默认为系统的 pthread 栈大小。

从 hugepage 分配的线程栈 不受保护页(guard pages)保护

如果启用了此选项,必须确保线程栈大小足够,以防止栈溢出;

与普通线程栈一样,从 hugepage 分配的线程栈大小是 固定的,不会动态扩展。

支持外部分配内存

DPDK 支持使用外部分配的内存。主要有两种方式可将外部内存纳入 DPDK 的内存管理体系:

  1. 通过 malloc heap API 使用外部分配内存(推荐方式)
  2. 手动管理外部分配内存,不使用 DPDK 分配器

方式一:使用 malloc heap API 来管理外部分配内存(推荐)

这种方式将外部内存集成进 DPDK 的 malloc heap 管理机制中。其原理是:

  • 为外部内存分配一个特殊的 socket ID(DPDK 默认认为无效的 ID)
  • 在调用 rte_malloc() 或其他支持 socket ID 的接口时,指定这个 socket ID 即可使用外部分配内存;
  • 包括像 rte_ring_create() 等结构体创建 API 也可间接使用该内存;
  • 这样还能支持 DMA 映射,即自动为外部内存段建立 DMA 映射关系。

由于 DPDK 无法验证外部分配内存是否有效,正确性与同步由用户自己负责。多进程同步也需用户维护。

推荐流程如下:

  1. 获取外部内存区域的指针(如来自 mmap、hugetlbfs、共享内存等)
  2. 创建一个具名的 malloc heap:
    rte_malloc_heap_create("my_ext_heap")
  3. 将内存区域加入该 heap:
    rte_malloc_heap_add_memory(...)
    • 若未提供 IOVA 表,则假定不可用于 DMA。
  4. 多进程使用时:其他进程需手动 attach 内存区域:
    rte_malloc_heap_attach_memory(...)
  5. 获取该 heap 对应的 socket ID:
    rte_malloc_heap_get_socket()
  6. 像使用普通 DPDK heap 一样使用该 socket ID:
    rte_malloc("my_ext_heap", size, align, socket_id)
  7. 不再使用时:先让其他进程 detach,再从 heap 中移除内存段,最后销毁 heap。

优点:

  • 使用 DPDK 原生分配器;
  • 支持所有基于 socket ID 的 DPDK 数据结构;
  • 支持 DMA 映射;
  • 更容易与现有代码集成。

方式二:不通过 DPDK 分配器,手动管理外部内存

有些应用不希望引入 DPDK 的 heap 系统(例如自己管理内存),这时可使用 rte_extmem_* 系列 API,仅将外部分配内存注册到 DPDK 页表中,以便部分 DPDK API(如地址转换)识别它。

这类内存不会参与 rte_malloc,完全由用户管理。

流程如下:

  1. 获取外部内存区域指针
  2. 注册内存:
    rte_extmem_register(...)
    • 若不提供 IOVA 表,则假定无法用于 DMA
  3. 多进程中:其他进程需 attach 内存区域
  4. 需要 DMA 映射时:使用 rte_dev_dma_map() 显式映射设备
  5. 直接在应用中使用内存
  6. 不再使用时:
    • 若映射了 DMA,先 rte_dev_dma_unmap()
    • 然后调用 rte_extmem_unregister()
    • 其他进程需先 detach。

开发者须知:

  • 你必须自己负责分配的内存是否有效、是否被多个进程正确 attach/detach、是否对 DMA 有效。
  • 多进程间共享时,在主进程注册内存后,从进程需要使用 attach API 连接该内存区域,否则访问会失败。
  • 不能随意释放外部分配的内存,除非已确保没有其他进程正在使用。

每个 lcore 和共享变量

默认情况下,静态变量、在 DPDK 堆上分配的内存块、以及其他类型的内存,会被所有 DPDK 线程共享。不过,DPDK 应用程序、库或 PMD(Poll Mode Driver)可以选择为每个线程(或每个 lcore)维护独立的状态信息

管理每线程(lcore)数据的方式有三种主要方法:

  1. lcore 变量
  2. 线程本地存储(TLS,Thread-Local Storage)
  3. 静态数组(RTE_MAX_LCORE 元素的数组)

方法 1:lcore 变量(推荐)

  • 使用 __rte_cache_aligned 修饰 + RTE_DECLARE_PER_LCORE() 定义;
  • 编译期静态分配;
  • 每个带有 lcore ID 的线程(无论是 EAL 创建的线程,还是手动注册的)都可以独立访问;
  • 生命周期独立于线程,可在线程创建前初始化;
  • 适合小对象和性能敏感场景;
#include <rte_per_lcore.h>

RTE_DECLARE_PER_LCORE(int, my_lcore_var);

void init(void) {
    RTE_PER_LCORE(my_lcore_var) = rte_lcore_id();
}

void worker(void) {
    printf("Thread %d sees %d\n", rte_lcore_id(), RTE_PER_LCORE(my_lcore_var));
}

方法 2:线程本地存储(TLS)

  • 使用 __thread__declspec(thread) 声明;
  • 在线程创建时分配,在线程结束时释放;
  • 可用于所有类型的线程,不限于 lcore;
  • 仅适用于非常小的对象
    • 大对象会导致线程创建变慢;
    • 使用 TLS 线程多时,会极大地增加内存占用;

方法 3:静态数组 [RTE_MAX_LCORE](旧式做法)

  • 声明形如:static struct obj my_data[RTE_MAX_LCORE];

  • 使用 rte_lcore_id() 作为索引访问;

  • 早期 DPDK 中常见,但 现在已不推荐,原因包括:

    问题 描述
    假共享(false sharing) 多核线程访问相邻数据时会影响彼此的缓存行
    内存碎片严重 每个元素需加 RTE_CACHE_GUARD + 对齐,造成浪费
    访问不安全 如果未初始化 lcore ID,访问索引可能越界
static struct my_struct {
    int val;
    char name[32];
} __rte_cache_aligned data[RTE_MAX_LCORE];

void run() {
    int id = rte_lcore_id();
    data[id].val = id;
}

建议使用方式对比

方法 特点 优点 缺点 推荐场景
lcore 变量 预编译绑定,每个 lcore 独立变量 快速、安全,生命周期独立 只适合 DPDK 线程 高性能数据路径中
TLS 每线程变量 灵活、兼容 POSIX 慢、资源占用大 小规模线程变量
RTE_MAX_LCORE 数组 通过 lcore ID 显式访问 简单易懂 假共享、碎片多 老代码兼容或调试用

日志(Logs)

虽然日志系统最初属于 EAL,现在已经由 Log Library(日志库) 提供。

CPU 特性识别

EAL 提供 rte_cpu_get_features() 接口在 运行时查询 CPU 支持的特性(如 AVX、AES-NI 等);

可用于判断当前 CPU 是否支持编译时目标特性集,确保运行兼容性。

用户态中断与事件处理

主线程轮询中断(Host Thread for Interrupt)

  • EAL 创建一个 Host Thread 线程监听设备文件(UIO、VFIO);
  • 可注册回调用于响应:
    • 链路状态变更(Link up/down);
    • PCI 设备热拔插。

定时器与定时回调

  • 支持异步定时回调(可与中断线程机制复用);
  • 例如:链路检测定时器、报文收发调度器。

RX 中断事件

  • 默认收发是轮询模型,但在低吞吐时可启用 RX 中断做 event-driven 唤醒
  • 依赖 epoll + 中断 FD;
  • VFIO 支持每队列独立中断,UIO 不支持;
  • 启用方式:
    • 配置 intr_conf.rxq = 1
    • 使用 rte_eth_dev_rx_intr_enable() 等 API;

设备移除事件

  • 检测设备被热拔插或资源不可用;
  • 应避免在中断上下文直接关闭设备,需异步延后处理。

PCI 设备阻止列表(Block List)

  • EAL 支持通过 PCI 设备地址(Domain:Bus:Device.Function)屏蔽指定网卡端口;
  • 被阻止的端口将不会被 DPDK 探测或使用。

杂项函数(Misc Functions)

  • 锁与原子操作是架构相关的(例如 i686 和 x86_64 分别实现);
  • 提供基础并发原语支持。

锁注解(Lock Annotations)

  • 使用 clang 的 Thread Safety 检查机制为:
    • spinlockread/write locksequence lock 添加线程安全注解;
  • 可在 meson.build 中关闭注解检查;
  • 避免盲目使用 __rte_no_thread_safety_analysis,若需关闭请先讨论

IOVA 模式检测

  • IOVA 模式决定设备使用的地址类型:物理地址(PA)或虚拟地址(VA);
  • 默认采用两步启发式判断:

第 1 步:总线报告需求

  • 若所有总线支持 PA → 使用 RTE_IOVA_PA
  • 若所有支持 VA → 使用 RTE_IOVA_VA
  • 若有冲突 → 使用 RTE_IOVA_DC(延后决策);

第 2 步:系统可用性判断

  • 若最终选定 RTE_IOVA_PA,但系统 无权限读取物理地址(如非 root 用户),则 初始化失败
  • /sys/kernel/iommu_groups 存在时 → 使用 VA 模式;
  • 否则默认尝试 PA 模式。

VA 模式更推荐:

​ 所有驱动都应兼容 VA;

​ 内存分配更快,减少启动时间;

​ 支持 IOVA 连续内存(如大页需求)更灵活;

​ 若驱动只支持 VA 模式,会设置 RTE_PCI_DRV_NEED_IOVA_AS_VA 标志。

IOVA 模式配置

  • 如果自动检测不符合期望(如存在虚拟设备),可通过:
    • --iova-mode=va 强制使用虚拟地址;
    • --iova-mode=pa 强制使用物理地址。

最大 SIMD 向量位宽配置(Max SIMD Bitwidth)

  • 控制 DPDK 使用的最大 SIMD 指令集位宽,决定是否启用向量化路径(如 AVX2、AVX512);
  • 设置方式:
    • 启动前调用:rte_vect_set_max_simd_bitwidth(uint16_t bitwidth)
    • 或使用启动参数:--force-max-simd-bitwidth=256
  • 查询当前值:rte_vect_get_max_simd_bitwidth()

支持值定义如下:

enum rte_vect_max_simd {
    RTE_VECT_SIMD_DISABLED = 64,
    RTE_VECT_SIMD_128      = 128,
    RTE_VECT_SIMD_256      = 256,
    RTE_VECT_SIMD_512      = 512,
    RTE_VECT_SIMD_MAX      = INT16_MAX + 1,
};

判断向量路径示例:

if (rte_vect_get_max_simd_bitwidth() >= RTE_VECT_SIMD_512)
    // 使用 AVX-512 路径
else if (rte_vect_get_max_simd_bitwidth() >= RTE_VECT_SIMD_256)
    // 使用 AVX2 路径
posted @ 2025-04-02 17:26  Tohomson  阅读(54)  评论(0)    收藏  举报