4. CPU亲和性原理:软亲和性与硬亲和性、sched_setaffinity系统调用、CPU位图(cpumask_t)

CPU亲和性(CPU Affinity)是DPDK多核调度优化的核心基础。它决定了进程或线程在哪些CPU核心上运行,直接影响缓存命中率、NUMA访存延迟和任务间干扰。本章从亲和性的两种类型出发,深入剖析系统调用接口与内核数据结构,为后续的任务绑定脚本编写打下坚实基础。

4.1 软亲和性与硬亲和性

Linux内核将CPU亲和性分为两个层次:软亲和性(Soft Affinity)和硬亲和性(Hard Affinity)。两者在调度策略中的约束力不同,理解其差异是正确使用绑定机制的前提。

特性 软亲和性 硬亲和性
定义 调度器优先尝试将进程放在上次运行的CPU上,但不强制 通过系统调用强制进程只能在指定的CPU集合内运行
实现机制 内核调度器(CFS)的负载均衡算法自动维护 通过 sched_setaffinity / pthread_setaffinity_np 设置
约束力 弱——调度器可能因负载均衡将进程迁移到其他CPU 强——违反硬亲和性的CPU迁移会被内核阻止
DPDK场景 不适用——DPDK要求确定性绑定,不能容忍迁移 必须使用——每个lcore固定绑定到指定物理核心
缓存影响 可能因迁移导致L1/L2缓存频繁失效 绑定后缓存局部性稳定,命中率显著提升
关键认知:DPDK应用必须使用硬亲和性。软亲和性仅适用于通用负载,对于数据面转发这类对延迟和吞吐有严格要求的场景,任何一次CPU迁移都会造成性能抖动和丢包。
CPU亲和性类型对比:软亲和性 vs 硬亲和性 软亲和性 进程A 上次运行 CPU 0 进程A 下次运行 CPU 1(迁移) 调度器可能因负载均衡迁移进程 缓存失效,性能不稳定 硬亲和性 进程B 绑定设置 CPU 2(固定) 进程B 始终运行 CPU 2(不变) 内核强制绑定,禁止迁移 缓存局部性稳定,性能可预测

4.2 sched_setaffinity系统调用

Linux提供了两组系统调用来设置和获取进程/线程的CPU亲和性:一组针对进程(PID),另一组针对线程(TID)。DPDK通常使用线程级绑定,因为每个lcore对应一个pthread。

4.2.1 进程级接口

#include <sched.h>

/* 设置进程的CPU亲和性 */
int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);

/* 获取进程的CPU亲和性 */
int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);
  • pid:目标进程ID。传入0表示当前进程。
  • cpusetsize:cpu_set_t 数据结构的大小,通常使用 sizeof(cpu_set_t)。
  • mask:指向CPU位图的指针,指定允许运行的CPU集合。
  • 返回值:成功返回0,失败返回-1并设置errno。

4.2.2 线程级接口(POSIX线程)

#define _GNU_SOURCE
#include <pthread.h>

/* 设置线程的CPU亲和性 */
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize,
                           const cpu_set_t *cpuset);

/* 获取线程的CPU亲和性 */
int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize,
                           cpu_set_t *cpuset);
DPDK实践要点:DPDK内部使用 pthread_setaffinity_np 将每个lcore线程绑定到指定物理核心。在 eal_thread_loop() 中,每个从线程启动后立即调用此接口完成绑定,确保后续所有数据面操作都在固定核心上执行。

4.2.3 典型绑定流程

#define _GNU_SOURCE
#include <sched.h>
#include <pthread.h>
#include <stdio.h>

int bind_thread_to_cpu(pthread_t thread, int cpu_id) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);          // 清空位图
    CPU_SET(cpu_id, &cpuset);   // 将指定CPU加入位图

    int ret = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
    if (ret != 0) {
        perror("pthread_setaffinity_np failed");
        return -1;
    }
    return 0;
}

// 使用示例:将当前线程绑定到CPU 2
// bind_thread_to_cpu(pthread_self(), 2);
常见错误:忘记调用 CPU_ZERO() 初始化位图。如果直接使用未初始化的 cpu_set_t 变量,位图中可能包含随机数据,导致绑定到错误的CPU集合,甚至绑定到不存在的CPU上。

4.3 CPU位图(cpumask_t)

CPU位图是内核和用户空间描述CPU集合的核心数据结构。在内核中表现为 cpumask_t,在用户空间通过 cpu_set_t 宏操作。理解位图的操作方式,是编写正确绑定代码的关键。

4.3.1 数据结构与宏操作

宏/函数 作用 使用示例
CPU_ZERO(cpuset) 清空位图,将所有CPU位设为0 CPU_ZERO(&mask);
CPU_SET(cpu, cpuset) 将指定CPU加入位图(置1) CPU_SET(2, &mask);
CPU_CLR(cpu, cpuset) 将指定CPU从位图中移除(置0) CPU_CLR(2, &mask);
CPU_ISSET(cpu, cpuset) 检查指定CPU是否在位图中 if (CPU_ISSET(2, &mask))
CPU_COUNT(cpuset) 返回位图中置1的CPU数量 int n = CPU_COUNT(&mask);
CPU_AND(dst, src1, src2) 计算两个位图的交集 CPU_AND(&dst, &src1, &src2);
CPU_OR(dst, src1, src2) 计算两个位图的并集 CPU_OR(&dst, &src1, &src2);

4.3.2 内核中的cpumask_t

在内核源码中,cpumask_t 定义为位数组,其大小由 NR_CPUS 编译常量决定:

/* include/linux/cpumask.h */
typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;

/* 内核常用操作 */
void cpumask_set_cpu(unsigned int cpu, struct cpumask *dstp);
void cpumask_clear_cpu(unsigned int cpu, struct cpumask *dstp);
int cpumask_test_cpu(int cpu, const struct cpumask *cpumask);
unsigned int cpumask_first(const struct cpumask *srcp);
unsigned int cpumask_next(int n, const struct cpumask *srcp);
DPDK视角:DPDK的 rte_cpuset_t 类型是对 cpu_set_t 的封装。在 rte_lcore.h 中,DPDK提供了 rte_thread_set_affinity() 和 rte_eal_cpu_init() 等高层接口,屏蔽了底层位图操作的细节。但在编写自定义绑定脚本时,直接操作 cpu_set_t 更为灵活。
CPU位图(cpumask_t)结构示意 CPU位图:64位(NR_CPUS = 64) 位0 | 位1 | 位2 | 位3 | ... | 位15 | ... | 位31 | ... | 位63 示例:绑定到CPU 0,2,4 → 位图值 = 0b0000...00010101 CPU 0(位0=1)| CPU 1(位1=0)| CPU 2(位2=1)| CPU 3(位3=0)| CPU 4(位4=1) CPU_ZERO() CPU_SET(cpu) sched_setaffinity() 完成

4.4 实战:检查当前进程的CPU亲和性

在编写绑定脚本前,先掌握如何读取当前进程或线程的亲和性配置。以下代码演示了如何获取并打印当前进程允许运行的CPU列表:

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>

void print_cpu_affinity(pid_t pid) {
    cpu_set_t mask;
    int ret = sched_getaffinity(pid, sizeof(cpu_set_t), &mask);
    if (ret != 0) {
        perror("sched_getaffinity");
        return;
    }

    printf("Process %d allowed CPUs: ", pid);
    for (int i = 0; i < CPU_SETSIZE; i++) {
        if (CPU_ISSET(i, &mask)) {
            printf("%d ", i);
        }
    }
    printf("\n");
    printf("Total allowed CPUs: %d\n", CPU_COUNT(&mask));
}

int main() {
    print_cpu_affinity(0);  // 检查当前进程
    return 0;
}
输出示例:
Process 0 allowed CPUs: 0 1 2 3 4 5 6 7
Total allowed CPUs: 8
表示当前进程可以在8个CPU核心上运行,尚未进行任何绑定限制。

4.5 本章小结

  • 软亲和性由调度器自动维护,不保证绑定;硬亲和性通过系统调用强制绑定,是DPDK的必选方案。
  • sched_setaffinity 和 pthread_setaffinity_np 是设置硬亲和性的核心接口,前者用于进程,后者用于线程。
  • CPU位图(cpu_set_t / cpumask_t)是描述CPU集合的数据结构,必须使用 CPU_ZERO 初始化后再通过 CPU_SET 添加目标CPU。
  • DPDK的 rte_cpuset_t 是对 cpu_set_t 的封装,理解底层位图操作有助于编写更灵活的绑定脚本。