将线程绑定到指定 CPU 核

前言

在多核 CPU 环境下,进程和线程通常会被调度到不同的核心上运行。为了减少调度开销、提升实时性或性能一致性,可以通过 sched_setaffinity 将某个进程或线程固定在指定的 CPU 核心上运行

函数原型

#include <sched.h>

int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);

pid

  • 0:表示当前进程/线程

  • 其他:指定目标进程或线程的 PID/TID

cpusetsize

  • 一般为 sizeof(cpu_set_t)

mask

  • CPU 集合,指定允许运行在哪些核心上

使用流程

定义并清空 CPU 集合

cpu_set_t mask;
CPU_ZERO(&mask);

设置需要绑定的 CPU

CPU_SET(0, &mask); // 绑定到 CPU0
CPU_SET(2, &mask); // 也可以同时绑定多个核

调用函数

sched_setaffinity(0, sizeof(mask), &mask); // 0 表示当前线程

线程级绑定

在多线程程序中,pthread_self() 返回的并不是系统线程号,无法直接使用。 需要通过系统调用获取 TID:

#include <sys/syscall.h>
#include <unistd.h>

pid_t tid = syscall(SYS_gettid); // 获取当前线程的 TID
sched_setaffinity(tid, sizeof(mask), &mask);

具体例子

示例代码: test.cpp

  • 主线程不绑定(由系统自由调度)。

  • 创建 2 个工作线程:一个绑到 CPU0,另一个绑到 CPU1。

  • 每个线程启动后打印自己的 TID 和当前所在 CPU(通过 sched_getcpu())。

// test.cpp
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
#include <cstring>
#include <cerrno>

#include <sched.h> // sched_setaffinity, cpu_set_t
#include <unistd.h> // syscall
#include <sys/syscall.h> // SYS_gettid

// 将当前线程绑定到指定 cpu_id
bool bind_current_thread_to_cpu(int cpu_id) {
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(cpu_id, &mask);

pid_t tid = (pid_t)syscall(SYS_gettid);
if (sched_setaffinity(tid, sizeof(mask), &mask) != 0) {
std::cerr << "[ERROR] sched_setaffinity: " << std::strerror(errno) << "\n";
return false;
}
return true;
}

void worker(int cpu_id, const char* name) {
// 绑定
if (!bind_current_thread_to_cpu(cpu_id)) {
std::cerr << name << " failed to bind to CPU" << cpu_id << "\n";
return;
}

// 打印一次绑定信息,并持续打印当前所在 CPU 验证效果
pid_t tid = (pid_t)syscall(SYS_gettid);
std::cout << name << " (TID=" << tid << ") bound to CPU" << cpu_id << "\n";

for (int i = 0; i < 10; ++i) {
// sched_getcpu() 返回当前线程正在运行的逻辑 CPU 编号
int cur = sched_getcpu();
std::cout << name << " (TID=" << tid << ") running on CPU " << cur << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(300));
}
}

int main() {
// 启动两个线程分别绑到 0 和 1 号 CPU
std::thread t0(worker, 0, "worker-0");
std::thread t1(worker, 1, "worker-1");

// 主线程不绑定,打印它当前所在 CPU(可能变化)
for (int i = 0; i < 5; ++i) {
int cur = sched_getcpu();
std::cout << "main thread running on CPU " << cur << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}

t0.join();
t1.join();
return 0;
}

说明

  • 线程级绑核的关键:获取 TID(syscall(SYS_gettid))并传给 sched_setaffinity。

  • sched_getcpu() 是 Linux 的便捷函数,用来查看当前线程正在运行的 CPU。

  • 若你的机器只有 1 个逻辑核,记得把 CPU_SET(1, …) 改成 0 或用 lscpu 先查看核数。

编译

g++ -O2 -pthread test.cpp -o pin
  • 必须加 -pthread,否则多线程与相关符号链接可能不完整。

  • 如果你使用 CMake,也可以在 target_link_libraries 里链接 pthread。

运行与实时观察

方式 A:直接前台运行,看程序自身输出

./pin

输出:

~ » ./pin 
main thread running on CPU 1
worker-1 (TID=896669) bound to CPU1
worker-1 (TID=896669) running on CPU 1
worker-0 (TID=896668) bound to CPU0
worker-0 (TID=896668) running on CPU 0
worker-1 (TID=896669) running on CPU 1
worker-0 (TID=896668) running on CPU 0
main thread running on CPU 1
worker-1 (TID=896669) running on CPU 1
worker-0 (TID=896668) running on CPU 0
worker-1 (TID=896669) running on CPU 1
worker-0 (TID=896668) running on CPU 0
main thread running on CPU 1
worker-1 (TID=896669) running on CPU 1
worker-0 (TID=896668) running on CPU 0
main thread running on CPU 1
worker-1 (TID=896669) running on CPU 1
worker-0 (TID=896668) running on CPU 0
worker-0 (TID=896668) running on CPU 0
worker-1 (TID=896669) running on CPU 1
main thread running on CPU 1
worker-0 (TID=896668) running on CPU 0
worker-1 (TID=896669) running on CPU 1
worker-0 (TID=896668) running on CPU 0
worker-1 (TID=896669) running on CPU 1
worker-0 (TID=896668) running on CPU 0
worker-1 (TID=896669) running on CPU 1

worker-0 永远在 CPU0,worker-1 永远在 CPU1,主线程可能在不同 CPU 间调度。

方式 B:后台运行 + 系统命令验证

把程序放到后台并抓取 PID:

~ » ./pin &               # 后台运行    
pid=$! # 保存进程 PID
echo "pid=$pid"

[1] 896965
pid=896965
main thread running on CPU worker-0 (TID=3896971
) bound to CPU0
worker-0 (TID=896971) running on CPU 0
worker-1 (TID=896972) bound to CPU1
worker-1 (TID=896972) running on CPU 1
---------------------------------------------------------------------------------------------------
~ » worker-0 (TID=worker-1896971) running on CPU 0 (TID=896972) running on CPU 1
main thread running on CPU 3
worker-0 (TID=896971) running on CPU 0
worker-1 (TID=896972) running on CPU 1
worker-0 (TID=896971) running on CPU 0
worker-1 (TID=896972) running on CPU 1
main thread running on CPU 5
worker-0 (TID=896971) running on CPU 0
worker-1 (TID=896972) running on CPU 1
main thread running on CPU 5
worker-0 (TID=896971) running on CPU 0
worker-1 (TID=896972) running on CPU 1
worker-0 (TID=896971) running on CPU 0
worker-1 (TID=896972) running on CPU 1
main thread running on CPU 5
worker-0 (TID=896971) running on CPU 0
worker-1 (TID=896972) running on CPU 1
worker-0 (TID=896971) running on CPU 0
worker-1 (TID=896972) running on CPU 1
worker-0 (TID=896971) running on CPU 0
worker-1 (TID=896972) running on CPU 1

[1] + 896965 done ./pin
-------------------------------

查看该进程的线程级 CPU 运行核(psr 列):

ps -o pid,tid,psr,comm -Lp "$pid"
  • pid:进程号

  • tid:线程 TID

  • psr:该线程当前运行在哪个 CPU(随时刻变化;被绑核的线程只会出现在允许范围内)

示意输出:

  PID   TID PSR COMMAND
12344 12344 2 pin
12344 12345 0 pin # worker-0
12344 12346 1 pin # worker-1

用 taskset 查看每个线程的 允许 CPU 列表(亲和性设置):

# 查看整个进程(所有线程)允许的 CPU 集合
taskset -pc "$pid"

# 分别查看具体线程(用 TID)
taskset -pc 12345
taskset -pc 12346

被成功绑核的线程会显示类似:

pid 12345's current affinity list: 0
pid 12346's current affinity list: 1

用 /proc 进一步核验(最权威的来源之一):

# 线程 12345 的亲和性(以列表形式显示)
cat /proc/$pid/task/12345/status | grep -E 'Cpus_allowed_list|Cpus_allowed'
  • Cpus_allowed_list:人类可读的核列表,例如 0 或 0-1,4

  • Cpus_allowed:位掩码形式(16 进制)

最后,回收后台进程:

kill "$pid"
wait "$pid" 2>/dev/null

用命令行直接绑进程/线程

除了在代码里调用 sched_setaffinity,你也可以用 taskset 在进程级别直接限制亲和性:

# 启动时指定(只允许运行在 CPU0)
taskset -c 0 ./pin

# 已经在跑了,再修改进程亲和性(允许运行在 CPU0-3)
taskset -pc 0-3 <pid>

注意:taskset 设置的是进程级亲和性,对现有和新建的线程都生效; 而代码里对某个线程 sched_setaffinity 是线程级,粒度更细。

常见问题与补充

  1. 机器核数不够:

先用 lscpu 确认逻辑 CPU 数(CPU(s)),避免对不存在的 cpu_id 调用 CPU_SET。

  1. 多 NUMA/大规模 CPU 场景:

对超多核系统(>1024 核)可使用动态大小的位图族 API:CPU_ALLOC/CPU_FREE/CPU_ALLOC_SIZE/CPU_ZERO_S/CPU_SET_S,避免 cpu_set_t 固定大小不够。

  1. 容器/调度器限制:

Docker/容器可能限制可见 CPU(例如 --cpuset-cpus),此时只能在被允许的核内绑定。

  1. 权限:

一般无需 root 权限。但若你的环境对 CPU 亲和性设置做了额外限制,可能需要更高权限或调整安全策略。

  1. 验证稳定性:

psr 是“当前时刻”的运行核,瞬时值会跳变;是否“被限制到某些核”应以 taskset -pc 和 /proc/…/status 的 Cpus_allowed_list 为准。

知识共享许可协议