DPDK CPU管理 多线程
DPDK 通常将每个 pthread
(线程)绑定到一个 CPU 核心上,以避免任务切换带来的开销。
这种方式能显著提升性能,但缺乏灵活性,且在某些情况下效率并不高。
电源管理功能可以通过限制 CPU 的运行频率来提升能效。然而,作为替代方案,也可以利用 CPU 空闲周期,进一步发挥其全部性能。
借助 cgroup
(控制组),可以简单地分配 CPU 使用配额,这为提升 CPU 效率提供了另一种方式。然而,这种方式有一个前提:
DPDK 必须能在一个核心上处理多个 pthread
之间的上下文切换。
为了获得更高的灵活性,不仅可以将 pthread
绑定到单个 CPU,也可以绑定到一组 CPU(CPU set)。
- 单线程绑定核心(pinned pthreads)
- DPDK 默认做法是:一个线程对应一个核心,不让线程发生调度。
- 这种做法保证了低延迟和高吞吐,因为避免了上下文切换带来的缓存污染与调度开销。
- 但它也带来问题:比如CPU资源利用率低,尤其是某些线程负载不高时,绑定核心就可能浪费资源。
- 电源管理与空闲周期利用
- 传统方式是降低频率来省电。
- 更高级的做法是:在主线程空闲时运行其他任务(例如低优先级线程),从而提升整体利用率。
- 使用 cgroup 控制 CPU 使用配额
cgroup
是 Linux 控制资源分配的机制,可以限制某个线程或线程组的 CPU 使用上限(比如 50%)。- DPDK 如果想利用这个机制,就必须在同一个核心上运行多个线程,并能正确进行上下文切换(这是 DPDK 默认不会做的事情,因此需要额外支持)。
- 绑定到 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 分为两类:
- 已注册的非 EAL pthread:
这类线程通过调用rte_thread_register()
成功注册,并分配了一个有效的_lcore_id
。 - 未注册的非 EAL pthread:
没有注册,_lcore_id
被设置为LCORE_ID_ANY
。
对于 未注册的非 EAL 线程(_lcore_id = LCORE_ID_ANY
):
- 某些 DPDK 库会使用线程 ID(如 TID)作为替代唯一标识;
- 某些库完全不受影响;
- 某些库仍能运行,但功能有限,比如
timer
和mempool
库。
所有这些影响会在 “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()
创建。
控制线程绑定机制逻辑:
- 控制线程只会跑在当前进程的 CPU affinity 掩码范围内(进程启动那一刻决定);
- 然后排除掉数据面核心(通过
-l
或--lcores
指定的); - 如果排除后无可用 CPU,则默认绑定到 main lcore 所在 CPU。
本质上,DPDK 尝试将控制线程“放在不影响数据处理性能的 CPU 上”。
场景 | 控制线程用途 |
---|---|
多进程通信 | 接收来自其他进程的 DPDK 消息 |
异步控制 | 处理 socket 命令、配置更新 |
中断处理 | 管理 vfio、eventdev 等中断响应 |
状态上报 | 定期将当前收发状态、环状缓冲区状态发送到外部系统 |
控制线程默认不会使用 rte_lcore_id() 等 DPDK 数据面特性,除非你显式注册;
它与非 EAL pthread 类似,但使用的是 DPDK 提供的线程调度逻辑,避免与数据面线程冲突;
若进程运行在容器中,需小心 taskset/cgroup 限制过度导致控制线程无处可跑。