4、CPU调优实践:通过sysfs接口手动绑定进程到特定核心,设置CPU亲和性,避免核间切换带来的缓存污染
4.1 为什么要手动绑定CPU核心?
先问大家一个问题:你的多核CPU,真的在「高效」工作吗?
我在RK3588的调试项目中发现一个现象——明明CPU负载不高,但延迟就是不稳定。后来一查,原来是Linux内核调度器在频繁地把进程从一个核踢到另一个核。每次迁移,L1、L2缓存全部失效,性能直接打七折。
说白了,这就是「缓存污染」的代价。你想想看,一个进程刚在Core 0上把数据加载到缓存里,调度器突然把它扔到Core 3上。Core 3的缓存是空的,得重新加载。这一来一回,几十微秒就没了。对于实时音视频处理、AI推理这类任务,这种抖动是致命的。
所以,手动绑定CPU亲和性,就是告诉内核:「这个活儿,就固定在这个核上干,别乱动。」
4.2 sysfs接口:最直接的CPU绑定工具
Linux提供了两套CPU亲和性设置方式:sched_setaffinity系统调用和taskset命令行工具。但今天我要重点讲的是sysfs接口——因为它最底层、最透明,适合嵌入式场景下的精细控制。
sysfs的CPU相关路径在:
/sys/devices/system/cpu/
每个CPU核心对应一个子目录:
/sys/devices/system/cpu/cpu0/
/sys/devices/system/cpu/cpu1/
/sys/devices/system/cpu/cpu2/
...
我个人习惯先看一眼present和online文件,确认哪些核是活着的:
cat /sys/devices/system/cpu/present
# 输出示例:0-7 (表示有8个核)
cat /sys/devices/system/cpu/online
# 输出示例:0-7 (全部在线)
4.3 实战:用taskset绑定进程到特定核心
taskset是封装了sysfs操作的命令行工具。用法非常简单:
# 将进程PID 1234绑定到CPU 0和2上
taskset -p -c 0,2 1234
# 启动新进程并绑定到CPU 4-7(大核)
taskset -c 4-7 ./my_ai_app
这里-c参数指定CPU列表,可以是单个数字、逗号分隔列表或范围。
我记得有一次,客户反馈RK3588上的NPU推理任务偶尔卡顿。我上去一看,进程在4个小核和4个大核之间来回跳。用taskset把它固定在大核上,延迟抖动从±5ms降到了±0.3ms。效果立竿见影。
4.4 更精细的控制:直接操作sysfs
有时候taskset不够灵活,比如你想在代码里动态调整亲和性。这时候就得直接读写sysfs文件了。
每个进程的CPU亲和性掩码存储在:
/proc/[PID]/task/[TID]/cpus_allowed
但更常用的方式是使用sched_setaffinity系统调用。不过,在嵌入式脚本中,我更喜欢直接写sysfs:
# 将当前shell进程绑定到CPU 0
echo 1 > /proc/self/cpus_allowed
# 将PID 1234绑定到CPU 2和3(掩码:0b1100 = 12)
echo 12 > /proc/1234/cpus_allowed
掩码计算小技巧:
- CPU 0:掩码 = 1(二进制0001)
- CPU 1:掩码 = 2(二进制0010)
- CPU 2:掩码 = 4(二进制0100)
- CPU 3:掩码 = 8(二进制1000)
- CPU 0-3:掩码 = 15(二进制1111)
4.5 避坑指南:我曾经踩过的三个坑
坑一:中断亲和性没设置
我曾经只绑定了用户态进程,但没管中断。结果网卡中断全跑在Core 0上,把CPU 0打满了。用户态进程在Core 1上干等网络数据。后来我把中断也绑到Core 2-3上,才解决问题。
设置中断亲和性的方法:
# 查看中断号
cat /proc/interrupts | grep eth0
# 将中断42绑定到CPU 2-3
echo 12 > /proc/irq/42/smp_affinity
坑二:大小核混绑
RK3588上,如果你把延迟敏感的任务绑到小核上,性能会差一大截。反过来,把后台任务绑到大核上,又浪费功耗。我的建议是:
| 任务类型 | 推荐绑定核心 | 原因 |
|---|---|---|
| AI推理、音视频编码 | 大核(4-7) | 需要高算力 |
| 网络协议栈、日志 | 小核(0-3) | 功耗优先 |
| 实时控制任务 | 独占一个大核 | 避免干扰 |
坑三:绑死后忘记释放
有一次我把一个监控进程绑到了Core 7上,后来系统升级,Core 7被热拔了。进程直接挂掉。所以,动态绑定时最好监听cpu hotplug事件,或者用cpuset子系统做更灵活的管理。
4.6 验证绑定是否生效
绑完之后,怎么确认生效了?我一般用三个方法:
- 看
/proc/[PID]/status:
grep Cpus_allowed /proc/1234/status
# 输出:Cpus_allowed: 0c (表示CPU 2-3)
- 用
htop实时观察:按F2进入设置,开启「Tree view」和「Hide kernel threads」,然后按F5看进程在哪个核上跑。 - 写一个测试程序:循环打印当前CPU编号,看它是否跳变。
#include <stdio.h>
#include <sched.h>
int main() {
while(1) {
int cpu = sched_getcpu();
printf("Running on CPU %d\n", cpu);
sleep(1);
}
return 0;
}
小技巧:在瑞芯微平台上,我习惯在开机脚本里就把关键进程的亲和性写好。比如/etc/init.d/里加一行taskset -c 4-7 /usr/bin/my_service。这样系统一启动,亲和性就固定了,省得后面再调。
4.7 什么时候不该用CPU绑定?
嗯,这里要注意——CPU绑定不是万能的。我见过有人把所有进程都绑死,结果系统负载不均衡,反而更慢。
以下场景不建议手动绑定:
- CPU数量少(2核以下):绑来绑去没意义,调度器自己就能处理好。
- 任务数量远大于核心数:比如8个核跑50个线程,绑死了反而增加调度开销。
- 任务执行时间极短(微秒级):绑定带来的收益抵不上配置成本。
说白了,CPU亲和性是一把手术刀,不是大砍刀。用对了地方,性能提升立竿见影;用错了,反而添乱。
我在项目中总结的经验是:先测量,再绑定。用perf stat看看当前进程的migrations次数,如果每秒迁移超过几十次,那就值得绑。如果本来就很少迁移,那就别折腾了。
好了,这一节的内容就到这里。下一节我们会讲如何用cpuset做更高级的CPU分区管理,把大小核彻底隔离开来。