跳到主要内容

Linux 系统中线程亲和性(CPU Affinity)详解

1. 什么是线程亲和性(CPU Affinity)

线程亲和性,也称为CPU亲和性(CPU Affinity),指的是将一个线程(或进程)绑定到指定的一个或多个 CPU 核心上运行。 默认情况下,Linux调度器可以在所有可用CPU上自由迁移一个线程,但如果设置了亲和性,调度器只能在允许的CPU集合中为线程调度运行。

简单理解:

  • 没有限制 → 线程可以在所有核心上漂移。
  • 有亲和性 → 线程只能在特定核心上执行。

2. 线程亲和性的原理

在 Linux 中,每个进程或线程都有一个叫做 CPU Affinity Mask(亲和性掩码) 的数据结构。 这是一个 bitmask,每一位代表一个 CPU:

  • 1 表示允许在对应CPU上运行。
  • 0 表示禁止在对应CPU上运行。

Linux内核在调度时,会参考这个掩码,只在被允许的CPU上安排线程运行。

典型掩码例子(假设有4个核心):

  • 1111 → 可以在核心0、1、2、3自由调度。
  • 0100 → 只能在核心2上运行。

掩码可以通过系统调用 sched_setaffinity 或工具如 taskset 设置。

3. 为什么要设置线程亲和性(优势)

  • 减少缓存失效(cache miss) 如果线程频繁迁移到不同CPU,CPU的本地缓存(L1/L2 Cache)内容会失效,导致重新加载数据,降低性能。固定在一个核心上可以提升缓存命中率。
  • 提高实时性(更好预测时延) 保持线程在特定核心上运行,减少调度开销和上下文切换,提高实时系统的响应速度。
  • 隔离高负载任务 将特定任务锁定在某些核心上,避免干扰其他任务,例如,把后台同步任务绑定到低优先级核心。
  • 更好地利用NUMA(非一致内存访问)结构 在NUMA架构机器上,可以将线程和内存绑定到同一节点,减少跨节点访问延迟。

4. 如何在命令行中实现亲和性控制

4.1 使用 taskset 工具

taskset 是Linux中专门用于设置或查询进程CPU亲和性的命令。

启动新程序时指定亲和性

taskset -c 0,2 ./your_program

启动程序 your_program,限制只能在 CPU 0 和 CPU 2 上运行。

也可以用掩码方式:

taskset 0x5 ./your_program

0x5(二进制 0101)表示 CPU 0 和 CPU 2。

修改已运行的进程亲和性

taskset -cp 1,3 12345

把 PID 为 12345 的进程绑定到 CPU 1 和 CPU 3。

4.2 查询进程的亲和性

taskset -cp 12345

输出示例:

pid 12345's current affinity list: 0,2

表示该进程可以在 CPU 0 和 2 上运行。

5. 如何在 C++ 代码中实现线程亲和性

Linux提供了两套API:

  • sched_setaffinity / sched_getaffinity:针对进程或线程设置亲和性。
  • pthread_setaffinity_np / pthread_getaffinity_np:针对pthread线程设置亲和性(np = non portable)。

5.1 使用 sched_setaffinity

设定当前进程绑定到CPU 0和1:

#include <sched.h>
#include <unistd.h>
#include <iostream>

void bind_to_cpus(std::initializer_list<int> cpus) {
cpu_set_t mask;
CPU_ZERO(&mask);
for (int cpu : cpus) {
CPU_SET(cpu, &mask);
}

pid_t pid = 0; // 0 表示当前进程
if (sched_setaffinity(pid, sizeof(mask), &mask) != 0) {
perror("sched_setaffinity failed");
} else {
std::cout << "Successfully set CPU affinity.\n";
}
}

int main() {
bind_to_cpus({0, 1});
while (true) {
// doing work
}
}

5.2 使用 pthread_setaffinity_np

设定某个线程绑定到CPU 2:

#include <pthread.h>
#include <sched.h>
#include <iostream>

void* thread_func(void*) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(2, &cpuset);

pthread_t thread = pthread_self();
if (pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset) != 0) {
perror("pthread_setaffinity_np failed");
} else {
std::cout << "Thread successfully set to CPU 2.\n";
}

while (true) {
// doing work
}
return nullptr;
}

int main() {
pthread_t tid;
pthread_create(&tid, nullptr, thread_func, nullptr);
pthread_join(tid, nullptr);
}

6. 如何查询线程亲和性

可以在代码中动态查询当前进程或线程的亲和性掩码。

6.1 查询当前进程亲和性

cpu_set_t mask;
CPU_ZERO(&mask);

if (sched_getaffinity(0, sizeof(mask), &mask) == 0) {
for (int i = 0; i < CPU_SETSIZE; ++i) {
if (CPU_ISSET(i, &mask)) {
std::cout << "Allowed CPU: " << i << "\n";
}
}
} else {
perror("sched_getaffinity failed");
}

6.2 查询某个线程亲和性

pthread_t tid = pthread_self();
cpu_set_t cpuset;
CPU_ZERO(&cpuset);

if (pthread_getaffinity_np(tid, sizeof(cpu_set_t), &cpuset) == 0) {
for (int i = 0; i < CPU_SETSIZE; ++i) {
if (CPU_ISSET(i, &cpuset)) {
std::cout << "Thread can run on CPU " << i << "\n";
}
}
} else {
perror("pthread_getaffinity_np failed");
}

7. 最佳实践建议

为了合理、高效地使用线程亲和性,建议遵循以下最佳实践:

  • 避免盲目绑定 不要为了绑定而绑定,需要根据任务类型、负载特性综合考虑。过度绑定可能导致负载不均衡或部分CPU过载。
  • 了解 NUMA 拓扑 多CPU、多内存节点机器(NUMA架构)上,优先考虑将线程绑定到本地内存节点对应的CPU,避免远程访问导致的高延迟。
  • 批量设置线程亲和性 对于大量线程,可以在创建线程时一次性分配亲和性,避免在运行过程中频繁修改。
  • 结合 CPU 隔离(CPU Isolation)使用 可以将某些CPU通过 isolcpus 参数在启动时隔离出来,只运行绑定线程,不被普通系统任务打扰。
  • 动态调整 在服务器负载动态变化时,可以实时调整线程亲和性,比如低峰期放松亲和性,高峰期加强亲和性。
  • 与优先级调度(nice、real-time)配合使用 线程亲和性配合实时调度策略(如 SCHED_FIFO)使用,能进一步减少延迟。
  • 调试与验证 通过 tasksettop -H -p pidhtop 等工具实时监控和验证线程的亲和性和运行情况。