4. CPU 亲和性:把任务钉在指定的核上

好,咱们进入正经优化环节之前,先把一个基础概念说透——CPU 亲和性。

我刚接触多核优化时做过一件傻事:写了个网络收包程序,发现 8 个核里只有 1 个核在疯狂飙到 100%,其他核在围观。后来才意识到,我给那个进程绑定了 CPU 0,还不自知。嗯,这就是亲和性在捣鬼。

说白了,CPU 亲和性就是让进程或中断强制跑在指定的 CPU 上,而不是任由调度器随机分配。这么做的好处很明显:缓存命中率高了,上下文切换少了,性能自然就稳了。

4.1 硬亲和性 vs. 软亲和性

亲和性分两种:硬亲和性和软亲和性。我在很多公开课和线上答疑里发现,好多朋友把这两者搞混,所以咱们先把这个理清楚。

属性 硬亲和性 软亲和性
控制方式 硬件/固件级别绑定 软件/OS 调度器策略
灵活性 固定,难以在线修改 可动态调整
粒度 中断控制器/设备级 进程/线程级
实现位置 GIC / IOMMU / 中断路由表 Linux 内核调度器 + cgroup / sched_setaffinity
典型场景 DMA 中断路由、PCIe MSI-X 绑定 进程/线程亲和性设置、绑核测试

硬亲和性你得把它想象成硬件级的强制绑定。比如中断控制器(GIC)会把某个设备的中断线直接映射到指定 CPU 上——这是硬件层面的路由决策,操作系统都没法干预。我在调试一块 FPGA 加速卡时,它的 DMA 完成中断硬绑到了 CPU0,结果 CPU0 负载一直高居不下,其他的核闲着。那个场景让我第一次深刻理解了硬亲和性的边界。

软亲和性就好比给调度器一个建议或指令。调度器会尽量满足你的要求,但在系统负载极度不均衡时,它仍然可能迁移任务。你通过 sched_setaffinity 设置的亲和性就属于这一类。

关键区别一句话:硬亲和性——你说了算,调度器/中断控制器必须听;软亲和性——你给建议,调度器尽量听,但压力大了可能不听话。
避坑指南:我曾经在项目上只设置了软亲和性,没做硬中断配置,结果高负载下中断被调度到了别的核,导致性能抖动。记得软硬结合,别只做一半。

4.2 sched_setaffinity:软亲和性的核心 API

实战中最常用的是 sched_setaffinity 系统调用。这个函数设置进程/线程的 CPU 亲和性掩码。我来演示一个实际例子。

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

void bind_thread_to_cpu(int cpu_id) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(cpu_id, &cpuset);
    
    pthread_t current_thread = pthread_self();
    if (pthread_setaffinity_np(current_thread, sizeof(cpu_set_t), &cpuset) != 0) {
        perror("pthread_setaffinity_np failed");
        exit(EXIT_FAILURE);
    }
    printf("线程已绑定到 CPU %d\n", cpu_id);
}
  • CPU_ZERO:清零掩码
  • CPU_SET:置位指定 CPU
  • pthread_setaffinity_np:线程级设置,非标准扩展
  • CPU_ISSET:检查是否绑定到某 CPU

这段代码看起来简单,但我在真实项目里遇到过一个大坑:CPU 编号搞错。有些系统上用 lscpu 看到的是逻辑编号,而 sched_setaffinity 使用的是系统级编号。你想想看,一个收包线程本想绑到 CPU 0,结果绑成了 CPU 7,性能不升反降。

个人习惯:我每次写代码绑核之前,会先跑一下 cat /proc/cpuinfo | grep "processor" | wc -l 确认核数,再用 cat /proc/$$/status | grep Cpus_allowed_list 验证当前进程的亲和性。

4.3 知识体系总览

下面这张图,把亲和性的两个分支、对应 API 和典型粒度梳理清楚。你记着它,以后做优化可以先定位自己绑的是硬路由还是软调度。

CPU 亲和性知识体系 CPU 亲和性 硬亲和性(硬件级) 中断控制器路由 PCIe MSI-X 绑定 粒度:中断 / DMA 通道 软亲和性(软件级) sched_setaffinity pthread_setaffinity_np 粒度:进程 / 线程 优化原则:中断用硬亲和性 + 进程用软亲和性,软硬结合才是最佳实践

4.4 实战考量:什么时候用什么亲和性?

现在你清楚了两种亲和性的区别,但什么时候用硬、什么时候用软呢?我总结一个快速判断法:

  • 中断 / DMA:必须用硬亲和性。通过 /proc/irq/ 或 irqbalance 把中断路由到指定 CPU,保证始终在那里处理。
  • 用户态进程:用软亲和性。比如 ksoftirqd 线程,通过 taskset -c 2 command 启动,或者 cpuset 控制组。
  • 混合场景:先配置硬亲和性把中断钉死,再把那个中断对应的收包线程用软亲和性绑到同一个 CPU。
避坑指南:你绑核的时候,不要把所有中断都绑到同一个核上。我见过有工程师设置中断亲和性时写了个全 1 掩码——结果所有中断全砸在一个核上,性能还不如不绑。每次做亲和性优化,先确认目标 CPU 利用率历史,别反而制造了热点。

好了,这一章我们讲清楚了什么是亲和性、硬亲和性和软亲和性的区别,以及 sched_setaffinity 的具体用法。你理解了这些,后面做 DMA 中断亲和性优化时,就能知道自己在哪一层下刀了。