3. CPU亲和性配置:sched_setaffinity接口、cpuset控制组、中断亲和性(IRQ Affinity)
各位,咱们今天聊点实在的。CPU亲和性,说白了就是告诉操作系统:「这个任务,你就给我老老实实在这几个核上跑,别到处乱窜。」
我刚开始做多核系统时,总觉得调度器挺智能的,让它自己分配不就完了?结果有一次在通信基站项目里,一个数据面任务被调度器在不同核之间来回踢,L1缓存命中率直接掉到30%以下。嗯,从那以后,我再也不敢轻视亲和性配置了。
3.1 sched_setaffinity:最直接的绑定方式
这是Linux内核提供的最基础的亲和性设置接口。你想想看,一个线程创建出来,默认可以在所有CPU上运行。但有时候,我们就想把它钉死在某个核上。
接口长这样:
#include <sched.h>
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(2, &cpuset); // 绑定到CPU 2
// 设置当前线程的亲和性
int ret = sched_setaffinity(0, sizeof(cpu_set_t), &cpuset);
if (ret != 0) {
perror("sched_setaffinity failed");
}
我个人习惯用 CPU_SET 和 CPU_CLR 来操作位掩码。注意一点:sched_setaffinity 的第一个参数是线程ID,传0表示当前线程。
3.2 cpuset控制组:容器级亲和性管理
如果你觉得 sched_setaffinity 太底层,每次都要写代码,那 cpuset 控制组就是你的好帮手。它属于 cgroup 子系统,可以在进程组级别管理 CPU 亲和性。
我一般在调试阶段这么用:
# 创建一个cpuset
mkdir /sys/fs/cgroup/cpuset/myapp
# 分配CPU 0-3
echo "0-3" > /sys/fs/cgroup/cpuset/myapp/cpuset.cpus
# 分配内存节点(NUMA环境下必须)
echo "0" > /sys/fs/cgroup/cpuset/myapp/cpuset.mems
# 把当前shell进程加入
echo $$ > /sys/fs/cgroup/cpuset/myapp/cgroup.procs
这里有个关键点:cpuset.mems 必须设置。我见过不少新手只配了 CPU 没配内存节点,结果进程直接挂掉。为什么?因为内核不知道从哪个内存节点分配内存,直接报错。
| 配置项 | 作用 | 必填 |
|---|---|---|
| cpuset.cpus | 允许使用的CPU列表 | 是 |
| cpuset.mems | 允许使用的内存节点 | 是 |
| cpuset.cpu_exclusive | 是否独占CPU | 否 |
| cpuset.mem_exclusive | 是否独占内存节点 | 否 |
numactl 使用效果更好。比如 numactl --physcpubind=0-3 --membind=0 ./myapp,这样进程的 CPU 和内存都在同一个 NUMA 节点上,延迟能降不少。
3.3 中断亲和性(IRQ Affinity)
说完进程的亲和性,咱们聊聊中断的。你想想看,一个网卡中断来了,如果被分配到某个核上处理,那这个核就得频繁响应中断。如果这个核上还跑着关键任务,那性能就受影响了。
中断亲和性的配置在 /proc/irq/ 目录下:
# 查看某个中断的亲和性
cat /proc/irq/78/smp_affinity
# 输出:00000000,00000001 (表示绑定到CPU 0)
# 修改亲和性(绑定到CPU 2)
echo "00000004" > /proc/irq/78/smp_affinity
这里用的是十六进制掩码。CPU 0 对应 0x01,CPU 1 对应 0x02,CPU 2 对应 0x04,以此类推。我习惯用 smp_affinity_list 这个文件,它支持直接写 CPU 编号:
echo "2" > /proc/irq/78/smp_affinity_list
这样直观多了,对吧?
3.4 知识体系总览
说了这么多,咱们用一张图来梳理一下整个亲和性配置的脉络:
3.5 实战中的避坑指南
最后,我把自己这些年踩过的坑总结一下:
- 亲和性不是万能的——我曾经把一个计算密集任务绑在某个核上,结果那个核还要处理时钟中断,性能反而下降了。要结合
isolcpus内核参数把核隔离出来。 - 子进程会继承亲和性——用
fork()创建的子进程默认继承父进程的 CPU 亲和性。如果你用system()或popen()启动外部程序,记得重新设置。 - 中断亲和性要持久化——
/proc/irq/下的配置重启就没了。我一般写一个 systemd 服务,在启动时自动设置。 - cpuset 的层级关系——子 cpuset 的 CPU 集合必须是父集合的子集。这个限制让我在动态调整时吃过亏。
taskset -p <pid> 查看进程当前的亲和性掩码。配合 htop 按 F2 开启「Tree view」和「CPU per process」,能直观看到每个进程跑在哪个核上。
好了,亲和性配置这块就聊到这儿。记住一句话:让该跑的跑对地方,让不该跑的别来打扰。这就是多核调度的精髓。