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%。

NUMA感知的CPU亲和性绑定流程 步骤1 获取NUMA拓扑结构 步骤2 选择目标NUMA节点 步骤3 分配同节点核心 进程级绑定 sched_setaffinity 线程级绑定 pthread_setaffinity_np 步骤4 验证绑定结果(/proc/self/status) 提示:绑定前务必通过 lscpu 或 numactl --hardware 确认NUMA拓扑,避免跨节点绑定。

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线程的亲和性绑定逻辑,核心流程如下:

  1. 解析 -l 或 -c 参数,确定启用的lcore列表
  2. 为每个lcore创建pthread线程
  3. 在线程入口函数中调用 pthread_setaffinity_np 绑定到指定核心
  4. 通过 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”三者同节点,达到最优性能。