操作系统层面的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。改之前一定确认好,这个进程是不是真的需要实时响应。

⚠️ 警告: 把关键系统进程(如sshd、systemd)改成实时策略,可能导致系统无法远程登录。我建议先在测试机上验证,再上生产。

二、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优化:三大核心策略 进程/线程调度策略 SCHED_OTHER / FIFO / RR 优先级设置与时间片分配 实时任务 vs 普通任务 工具: chrt, sched_setscheduler CPU亲和性设置 进程/线程绑定到固定核心 减少缓存抖动 提升L1/L2命中率 工具: taskset, sched_setaffinity 中断负载均衡 硬件中断均匀分布 多队列网卡RSS 手动/自动均衡策略 工具: irqbalance, smp_affinity 系统性能最大化 低延迟 · 高吞吐 · 资源均衡 三者协同:调度策略决定谁跑 → 亲和性决定在哪跑 → 中断均衡保证不偏载

嗯,到这里,操作系统层面的CPU优化就聊得差不多了。调度策略、CPU亲和性、中断均衡,这三板斧用好了,你的系统就能从“能用”变成“好用”。下次遇到CPU负载不均或者性能瓶颈,别急着改代码,先看看操作系统这层有没有调对。


可可精品资料库