DPDK CPU管理 多线程

DPDK 通常将每个 pthread(线程)绑定到一个 CPU 核心上,以避免任务切换带来的开销。
这种方式能显著提升性能,但缺乏灵活性,且在某些情况下效率并不高。

电源管理功能可以通过限制 CPU 的运行频率来提升能效。然而,作为替代方案,也可以利用 CPU 空闲周期,进一步发挥其全部性能。

借助 cgroup(控制组),可以简单地分配 CPU 使用配额,这为提升 CPU 效率提供了另一种方式。然而,这种方式有一个前提:
DPDK 必须能在一个核心上处理多个 pthread 之间的上下文切换。

为了获得更高的灵活性,不仅可以将 pthread 绑定到单个 CPU,也可以绑定到一组 CPU(CPU set)。

  1. 单线程绑定核心(pinned pthreads)
    • DPDK 默认做法是:一个线程对应一个核心,不让线程发生调度。
    • 这种做法保证了低延迟和高吞吐,因为避免了上下文切换带来的缓存污染与调度开销。
    • 但它也带来问题:比如CPU资源利用率低,尤其是某些线程负载不高时,绑定核心就可能浪费资源。
  2. 电源管理与空闲周期利用
    • 传统方式是降低频率来省电。
    • 更高级的做法是:在主线程空闲时运行其他任务(例如低优先级线程),从而提升整体利用率。
  3. 使用 cgroup 控制 CPU 使用配额
    • cgroup 是 Linux 控制资源分配的机制,可以限制某个线程或线程组的 CPU 使用上限(比如 50%)。
    • DPDK 如果想利用这个机制,就必须在同一个核心上运行多个线程,并能正确进行上下文切换(这是 DPDK 默认不会做的事情,因此需要额外支持)。
  4. 绑定到 CPU 集合(CPU set)
    • 更灵活的做法不是绑定单个 CPU,而是绑定一组 CPU(比如 CPU 2、3、4)。
    • 这样操作系统可以在这组 CPU 内调度该线程,既保持性能,又提升了可调度性和弹性。

EAL pthread 与 lcore 的绑定关系(Affinity)

术语 “lcore” 指的是一个 EAL 线程,它实质上就是一个 Linux/FreeBSD 的 pthread(线程)
“EAL pthreads” 是由 DPDK 的 EAL(Environment Abstraction Layer) 创建和管理的线程,专门执行通过 remote_launch 派发的任务。

在每个 EAL 线程中,都有一个线程局部存储(TLS)变量:_lcore_id,用来进行唯一标识。
由于 EAL 的线程通常是1:1 绑定到物理 CPU 核心的,因此这个 _lcore_id 通常就等于物理 CPU 的 ID。

但是,当使用多个 pthread 时,EAL pthread 与物理 CPU 的绑定不再是固定的 1:1 关系。
EAL 线程可能会绑定到一个 CPU 集合(CPU set) 上,此时 _lcore_id不等于实际的 CPU ID。

为了解决这个问题,EAL 提供了一个长参数选项:
--lcores,用于灵活地为 lcore 设置 CPU affinity(绑定关系)

这个选项的格式如下:

bash


复制编辑
--lcores='<lcore_set>[@cpu_set][,<lcore_set>[@cpu_set],…]'

其中:

  • lcore_set:可以是一个数字、一个范围,或者是由逗号分隔的组;
  • cpu_set:和 lcore_set 格式类似,用来指定某些 lcore 要绑定的 CPU 集合;
  • 如果没有显式指定 @cpu_set,则默认把 lcore_set 的值作为 cpu_set

格式定义:

  • 单个数字:3
  • 范围:1-4
  • 组合:(0,2,4-6)

示例解析:

--lcores='1,2@(5-7),(3-5)@(0,2),(0,6),7-8'

这个命令将创建 9 个 EAL 线程,具体分配如下:

lcore ID 绑定 CPU 集合 二进制位掩码表示(cpuset)
0 CPU 0 和 6 0x41(2⁰ + 2⁶ = 65)
1 CPU 1 0x2(2¹ = 2)
2 CPU 5,6,7 0xe0(2⁵ + 2⁶ + 2⁷ = 224)
3,4,5 CPU 0,2 0x5(2⁰ + 2² = 5)
6 CPU 0 和 6(与 lcore 0 相同) 0x41
7 CPU 7 0x80(2⁷ = 128)
8 CPU 8 0x100(2⁸ = 256)

说明:

  • lcore ID 是逻辑线程 ID,可以自由定义;
  • 每个 lcore 可以绑定到多个 CPU,形成 CPU 集合;
  • 通过这种方式,可以更灵活地调度线程,提高资源利用率
  • 它也兼容传统 -l 参数(指定 core list) 的使用习惯。

非 EAL pthread 支持

DPDK 的执行上下文(execution context)也可以用于任意用户线程(即非 EAL pthread,non-EAL pthread)。

非 EAL pthread 分为两类:

  1. 已注册的非 EAL pthread
    这类线程通过调用 rte_thread_register() 成功注册,并分配了一个有效的 _lcore_id
  2. 未注册的非 EAL pthread
    没有注册,_lcore_id 被设置为 LCORE_ID_ANY

对于 未注册的非 EAL 线程_lcore_id = LCORE_ID_ANY):

  • 某些 DPDK 库会使用线程 ID(如 TID)作为替代唯一标识;
  • 某些库完全不受影响;
  • 某些库仍能运行,但功能有限,比如 timermempool 库。

所有这些影响会在 “Known Issues”(已知问题)章节中有详细说明。

EAL pthread

DPDK 使用 rte_eal_remote_launch() 创建的线程,由 EAL 管理。

名称 说明
EAL pthread DPDK 使用 rte_eal_remote_launch() 创建的线程,由 EAL 管理。
non-EAL pthread 用户自己创建的 pthread,未通过 DPDK 启动。
_lcore_id DPDK 中用于标识线程逻辑核心 ID 的变量。
LCORE_ID_ANY 特殊值,表示该线程没有绑定到具体的 _lcore_id

DPDK 如何处理 non-EAL pthread?

1. 注册(推荐做法):

  • 用户可以通过 rte_thread_register() 显式告诉 DPDK:“这个线程我要让它参与到 DPDK 框架中”。
  • DPDK 会为其分配 _lcore_id,注册 TLS 变量等;
  • 可以使用大多数 DPDK 子系统(mempool、ring、timer、mbuf 等)而无兼容性问题。

2. 不注册(兼容模式):

  • 某些库仍能工作,但会遇到功能缺失或潜在问题;
  • 例如:
    • mempool 可能会无法正确统计或分配;
    • timer 可能无法使用 _lcore_id 管理定时器;
    • 日志、事件调度器等无法追踪该线程;
  • 某些库通过线程 ID(如 gettid())作为 fallback,但不是统一行为。

公共线程 API

DPDK 提供了两个用于线程的公开 API 接口:

  • rte_thread_set_affinity()
  • rte_thread_get_affinity()

这两个函数可以在任意 pthread 线程上下文中使用,用于设置或获取线程的亲和性(CPU affinity)。
使用这些函数时,DPDK 会设置或获取线程的线程局部存储(TLS)

这些 TLS(Thread Local Storage)变量包括:

  • _cpuset:存储该线程绑定的 CPU 集合(bitmap);
  • _socket_id:存储该 CPU 集合所在的 NUMA 节点 ID
    如果 CPU 集合中的 CPU 来自不同的 NUMA 节点,那么 _socket_id 会被设置为 SOCKET_ID_ANY(表示不特定于某个 NUMA 节点)。
项目 说明
_cpuset 表示该线程允许在哪些 CPU 上运行,是一个位图(bitmask)。例如绑定在 CPU 0 和 1 上就是 0x3
_socket_id 表示 _cpuset 所属的 NUMA 节点 ID;如果跨 NUMA 节点,则为 SOCKET_ID_ANY

控制线程 API

DPDK 提供了一个公开的 API —— rte_thread_create_control(),可以用于创建 控制线程(Control Threads)

这些线程通常用于管理或基础设施类任务,比如:

  • 多进程支持;
  • 中断处理(interrupt handling);
  • 其他后台任务。

控制线程将被调度到原始进程的 CPU affinity 范围内
但会排除 数据面核心(dataplane lcore)服务核心(service lcore) 所占用的 CPU。

假设你有一个 8 核 CPU 系统(CPU 0 ~ CPU 7):

# 启动 DPDK 应用时指定数据面核心为 CPU 2 和 3
./my_dpdk_app -l 2,3
CPU 亲和性配置(affinity) 控制线程实际运行在哪些 CPU 上?
没有额外配置(默认) 控制线程会被分配到 CPU 0-1、4-7
使用 taskset 限制在 CPU 2-4 控制线程只能跑在 CPU 4
使用 taskset 限制在 CPU 2-3 没有非数据面核心,控制线程将运行在 CPU 2(默认选择 main lcore)

什么是控制线程?

  • DPDK 中默认所有线程基本是数据面线程(rte_eal_remote_launch() 启动),专注于报文处理。
  • 但有些线程不参与报文处理,如:
    • 中断响应;
    • 多进程间通信(如 rte_mp_channel);
    • 配置同步等后台任务;
  • 这些就是“控制线程”,使用 rte_thread_create_control() 创建。

控制线程绑定机制逻辑:

  1. 控制线程只会跑在当前进程的 CPU affinity 掩码范围内(进程启动那一刻决定);
  2. 然后排除掉数据面核心(通过 -l--lcores 指定的);
  3. 如果排除后无可用 CPU,则默认绑定到 main lcore 所在 CPU。

本质上,DPDK 尝试将控制线程“放在不影响数据处理性能的 CPU 上”。

场景 控制线程用途
多进程通信 接收来自其他进程的 DPDK 消息
异步控制 处理 socket 命令、配置更新
中断处理 管理 vfio、eventdev 等中断响应
状态上报 定期将当前收发状态、环状缓冲区状态发送到外部系统

控制线程默认不会使用 rte_lcore_id() 等 DPDK 数据面特性,除非你显式注册;

它与非 EAL pthread 类似,但使用的是 DPDK 提供的线程调度逻辑,避免与数据面线程冲突;

若进程运行在容器中,需小心 taskset/cgroup 限制过度导致控制线程无处可跑

posted @ 2025-04-04 16:38  Tohomson  阅读(66)  评论(0)    收藏  举报