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迁移都会造成性能抖动和丢包。
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 更为灵活。
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;
}
输出示例:
表示当前进程可以在8个CPU核心上运行,尚未进行任何绑定限制。
Process 0 allowed CPUs: 0 1 2 3 4 5 6 7Total 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的封装,理解底层位图操作有助于编写更灵活的绑定脚本。