4、CPU亲和性概念:进程/线程绑定、sched_setaffinity、pthread_setaffinity_np
CPU亲和性(CPU Affinity)是操作系统调度器将进程或线程绑定到特定CPU核心(或一组核心)上运行的机制。在NUMA架构下,正确设置亲和性可以避免进程/线程在跨NUMA节点间迁移,从而降低内存访问延迟、减少缓存失效,是DPDK性能优化的核心手段之一。
核心思想: 将计算任务“钉”在固定的CPU核心上,避免上下文切换和跨NUMA节点访问,实现零拷贝、零中断的数据面处理。
4.1 亲和性的两种粒度
| 粒度 | 绑定对象 | 系统调用 | 典型场景 |
|---|---|---|---|
| 进程级 | 整个进程及其所有线程 | sched_setaffinity |
控制面管理进程、轻量级控制线程 |
| 线程级 | 单个POSIX线程 | pthread_setaffinity_np |
DPDK数据面转发线程、中断线程 |
DPDK通常采用线程级绑定,因为每个lcore(逻辑核心)运行一个独立的线程,需要精确控制每个线程所在的物理核心和NUMA节点。
4.2 进程级绑定:sched_setaffinity
sched_setaffinity 用于设置进程的CPU亲和性掩码(affinity mask)。掩码中的每一位代表一个逻辑CPU,置1表示允许该进程在该CPU上运行。
#include <sched.h>
#include <stdio.h>
int main() {
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(2, &mask); // 绑定到 CPU 2
CPU_SET(3, &mask); // 同时绑定到 CPU 3
if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
perror("sched_setaffinity");
return 1;
}
printf("进程已绑定到 CPU 2 和 CPU 3\n");
return 0;
}
注意:
sched_setaffinity 的掩码是进程级的,子进程会继承父进程的亲和性设置。在多线程程序中,该调用会影响所有线程的调度范围,但不会覆盖线程自身的亲和性设置。
4.3 线程级绑定:pthread_setaffinity_np
pthread_setaffinity_np 是POSIX线程库的扩展(_np 表示non-portable),用于设置单个线程的CPU亲和性。DPDK的lcore线程正是通过此接口实现精确绑定。
#include <pthread.h>
#include <sched.h>
void *worker(void *arg) {
int core_id = *(int *)arg;
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(core_id, &mask);
if (pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) != 0) {
perror("pthread_setaffinity_np");
return NULL;
}
printf("线程已绑定到 CPU %d\n", core_id);
// 执行数据面处理...
return NULL;
}
int main() {
pthread_t t1, t2;
int core1 = 0, core2 = 1;
pthread_create(&t1, NULL, worker, &core1);
pthread_create(&t2, NULL, worker, &core2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
4.4 亲和性绑定流程与NUMA感知
在NUMA架构下,绑定CPU核心时必须考虑该核心所属的NUMA节点。错误的绑定(如将线程绑定到远程节点核心)会导致跨NUMA内存访问,性能下降30%~50%。
4.5 绑定验证与调试
绑定完成后,可以通过以下方式验证亲和性是否生效:
- 查看进程/线程的CPU掩码:
cat /proc/<PID>/status | grep Cpus_allowed - 查看线程运行的核心:
ps -eLo pid,tid,psr,comm | grep <进程名> - 使用taskset工具:
taskset -p <PID>显示当前亲和性掩码 - DPDK自带的lcore绑定检查:
rte_lcore_is_enabled()和rte_get_main_lcore()
常见陷阱: 在NUMA系统上,如果只绑定CPU核心而不考虑内存分配策略,线程可能仍然访问远程内存。必须配合
mbind() 或 numactl --membind 将内存分配限制在同一NUMA节点。
4.6 亲和性掩码的位操作
CPU亲和性掩码使用 cpu_set_t 数据结构,通过以下宏进行操作:
| 宏/函数 | 功能 | 使用示例 |
|---|---|---|
CPU_ZERO() |
清空掩码 | CPU_ZERO(&mask); |
CPU_SET() |
将指定CPU加入掩码 | CPU_SET(0, &mask); |
CPU_CLR() |
将指定CPU从掩码移除 | CPU_CLR(1, &mask); |
CPU_ISSET() |
检查CPU是否在掩码中 | if (CPU_ISSET(2, &mask)) |
CPU_COUNT() |
统计掩码中的CPU数量 | int n = CPU_COUNT(&mask); |
4.7 DPDK中的亲和性绑定实践
DPDK在 eal_thread.c 中实现了lcore线程的亲和性绑定逻辑,核心流程如下:
- 解析
-l或-c参数,确定启用的lcore列表 - 为每个lcore创建pthread线程
- 在线程入口函数中调用
pthread_setaffinity_np绑定到指定核心 - 通过
rte_eal_remote_launch()将任务分发到已绑定的lcore
// DPDK EAL线程绑定核心代码(简化)
static int eal_thread_set_affinity(unsigned lcore_id) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(lcore_config[lcore_id].core_id, &cpuset);
if (pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset) != 0) {
RTE_LOG(ERR, EAL, "Cannot set affinity for lcore %u\n", lcore_id);
return -1;
}
return 0;
}
DPDK最佳实践: 在NUMA系统上,建议将数据面线程绑定到与网卡所在NUMA节点相同的CPU核心上,同时使用
rte_malloc() 分配本地内存,实现“计算+内存+IO”三者同节点,达到最优性能。