操作系统层面的CPU优化:进程与线程调度策略,CPU亲和性设置,中断负载均衡
说到CPU优化,很多人第一反应是调代码、改算法。但说实话,很多时候瓶颈根本不在应用层,而是操作系统怎么把活儿分给CPU去干。我见过太多项目,代码写得挺漂亮,结果跑起来CPU一个核忙死,其他核闲得发慌。嗯,这就是调度策略没搞对。
这一节,咱们就聊聊操作系统层面那三件要紧事:进程与线程怎么调度、CPU亲和性怎么绑、中断怎么分摊。搞明白这些,你的系统性能至少能再往上提一档。
一、进程与线程调度策略:谁先跑,谁等着?
操作系统调度器就像个交通指挥员。车多了,得决定哪辆车先走。Linux默认用的是完全公平调度(CFS),它的核心思想很简单:让每个进程都能分到CPU时间,谁也不饿着。
但公平不等于高效。你想想看,一个后台日志进程和一个实时音视频进程,如果给它们同样的时间片,那视频肯定卡成PPT。所以我们需要干预调度策略。
1. 调度策略分类
Linux支持几种调度策略,我挑最常用的说:
| 调度策略 | 适用场景 | 特点 |
|---|---|---|
| SCHED_OTHER(普通) | 大多数用户进程 | 完全公平,动态优先级 |
| SCHED_FIFO(实时-先入先出) | 实时任务,如音频处理 | 高优先级抢占,直到主动让出 |
| SCHED_RR(实时-时间片轮转) | 多个实时任务轮流执行 | 同优先级轮流,时间片用完切换 |
| SCHED_BATCH(批处理) | 后台计算任务 | 减少唤醒次数,省电省CPU |
我个人习惯,对于延迟敏感的服务,比如游戏服务器或交易系统,会把它改成SCHED_FIFO。但这里有个坑——千万别把所有核心进程都设成实时策略,否则系统调度器会被锁死,连鼠标都动不了。我曾经在测试环境干过这事,结果只能硬重启,尴尬得很。
2. 修改调度策略的实操
用chrt命令就能改。举个例子:
# 查看当前调度策略和优先级
chrt -p 1234
# 将PID为1234的进程设为SCHED_FIFO,优先级50
chrt -f -p 50 1234
# 设为SCHED_RR,优先级30
chrt -r -p 30 1234
注意,实时优先级范围是1-99,数字越大优先级越高。普通进程的优先级是0。改之前一定确认好,这个进程是不是真的需要实时响应。
二、CPU亲和性设置:把任务绑在固定的核上
CPU亲和性,说白了就是让某个进程或线程只跑在指定的CPU核心上。为什么要这么做?因为CPU缓存有局部性。一个进程在核0上跑久了,L1/L2缓存里全是它的数据。如果调度器把它扔到核1上,缓存全失效,性能直接打折扣。
我在项目中遇到过一个问题:一个高并发的网络服务,每秒处理几万个请求。默认情况下,它的线程在各个核之间跳来跳去,CPU缓存命中率只有60%左右。后来我绑定了亲和性,命中率直接升到90%以上,吞吐量提升了将近30%。
1. 设置亲和性的方法
Linux下用taskset命令:
# 启动时绑定到核0和核1
taskset -c 0,1 ./my_server
# 对已运行的进程,绑定到核2-3
taskset -p -c 2,3 5678
也可以在代码里用sched_setaffinity系统调用:
#include <sched.h>
#include <stdio.h>
int main() {
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(0, &mask); // 绑定到核0
CPU_SET(2, &mask); // 也绑定到核2
if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
perror("sched_setaffinity");
return 1;
}
printf("进程已绑定到核0和核2\n");
return 0;
}
2. 什么时候该绑?什么时候不该绑?
我总结了几条经验:
- CPU密集型的计算任务:比如视频编码、科学计算,一定要绑。减少缓存抖动,效果立竿见影。
- I/O密集型的网络服务:建议把每个工作线程绑定到固定核,配合
SO_REUSEPORT,让每个核处理自己的连接。 - 多租户环境:比如容器里跑多个服务,绑核可以防止互相干扰。
- 不要绑死所有核:留一两个核给操作系统和中断处理用,否则系统响应会变慢。
htop或mpstat -P ALL 1观察每个核的负载。如果发现某个核长期跑满,其他核空闲,那就是绑核的好时机。
三、中断负载均衡:别让一个核累死
中断,就是硬件告诉CPU“我有事找你”。比如网卡收到一个数据包,会触发一个中断。如果所有中断都跑到同一个核上,那个核就得忙死,其他核在旁边看热闹。
这就是中断负载均衡要解决的问题。说白了,就是把中断请求均匀地分配到各个CPU核心上。
1. 查看当前中断分布
用/proc/interrupts文件:
cat /proc/interrupts | grep eth0
输出会显示每个中断号在每个核上的触发次数。如果发现某个核的数字特别大,说明中断分配不均匀。
2. 手动调整中断亲和性
每个中断都有一个smp_affinity属性,写在/proc/irq/{中断号}/smp_affinity里。值是一个十六进制掩码,每一位代表一个核。
举个例子,把中断号128分配到核0和核2:
# 核0对应bit0,核2对应bit2,掩码是0x05
echo 05 > /proc/irq/128/smp_affinity
更直观的做法是用irqbalance服务。它会自动把中断分到各个核上。我个人习惯是:
- 普通服务器:开启irqbalance,省心省力。
- 高性能场景:关掉irqbalance,手动绑定。比如网卡的多队列,每个队列绑一个核,配合RPS/RFS,效果更好。
3. 多队列网卡的优化
现在的网卡都支持多队列(RSS)。每个队列可以绑定到不同的核。用ethtool查看和设置:
# 查看网卡eth0的队列数
ethtool -l eth0
# 设置队列数为4
ethtool -L eth0 combined 4
# 查看每个队列的中断号
cat /proc/interrupts | grep eth0
# 手动绑定每个队列到不同核
echo 01 > /proc/irq/130/smp_affinity # 队列0 -> 核0
echo 02 > /proc/irq/131/smp_affinity # 队列1 -> 核1
echo 04 > /proc/irq/132/smp_affinity # 队列2 -> 核2
echo 08 > /proc/irq/133/smp_affinity # 队列3 -> 核3
我曾经在一个高流量网关项目里这么干过。原来单核处理中断,CPU使用率冲到80%,丢包率很高。改成4队列绑4核后,每个核只处理25%的中断,CPU使用率降到30%,丢包率直接归零。
知识体系总览
下面这张图,把这一节的核心逻辑串起来了。你可以看到,从调度策略到绑核,再到中断均衡,其实是一条线:让合适的任务,在合适的核上,以合适的方式运行。
嗯,到这里,操作系统层面的CPU优化就聊得差不多了。调度策略、CPU亲和性、中断均衡,这三板斧用好了,你的系统就能从“能用”变成“好用”。下次遇到CPU负载不均或者性能瓶颈,别急着改代码,先看看操作系统这层有没有调对。