操作系统调度原理:从CFS到多级反馈队列
调度器,说白了就是操作系统里那个决定「下一个谁上CPU」的模块。你写再多线程,如果调度器不给力,该响应的卡住,该算完的拖半天——我见过太多业务层优化半天,结果根子在调度策略上的案例了。
这一章,我们就把Linux CFS、时间片轮转、优先级调度、多级反馈队列这几个核心概念掰开揉碎。嗯,我会结合自己踩过的坑来讲。
2.1 Linux CFS调度器:公平不是平均
CFS的全称是Completely Fair Scheduler,完全公平调度器。从2.6.23内核开始它就是默认调度器了。
核心思想:不是给每个任务固定时间片,而是让每个任务获得相等的CPU时间比例。
CFS维护一个红黑树,键值是vruntime(虚拟运行时间)。每次选最左边那个节点——也就是vruntime最小的任务上CPU。
关键公式:vruntime += 实际运行时间 × (nice值对应的权重 / 默认权重)
优先级高的任务(nice值小),vruntime增长慢,所以能多占CPU;优先级低的增长快,自然少占。
我曾经在一个数据库中间件项目里,发现某个后台清理线程总是抢不过业务线程,导致内存回收延迟。查了半天,其实就是nice值没调——清理线程的vruntime增长太快,几乎轮不到它。把nice值从0调到-5,问题解决。
CFS的关键参数
| 参数 | 含义 | 默认值 | 我的建议 |
|---|---|---|---|
sched_latency_ns |
目标调度延迟 | 6ms(桌面)/ 20ms(服务器) | 交互型任务可以调小 |
sched_min_granularity_ns |
最小执行粒度 | 0.75ms | 太小会导致上下文切换过频 |
sched_wakeup_granularity_ns |
唤醒抢占粒度 | 1ms | 调小可减少唤醒延迟 |
调优技巧:如果你的应用是延迟敏感的(比如游戏服务器、高频交易),可以尝试把sched_latency_ns降到3ms以下。但注意——这会增加上下文切换次数,CPU开销会上升。
2.2 时间片轮转:最朴素的公平
时间片轮转(Round Robin)是最基础的调度算法。每个任务轮流上CPU,时间片一到就切换下一个。
你想想看,如果时间片太大,交互任务会卡顿;太小呢,切换开销又上去了。我记得早期Linux 2.4内核用的就是纯时间片轮转,那时候我做一个嵌入式GUI系统,鼠标移动都一卡一卡的——就是因为时间片设成了100ms。
时间片选择的经验法则:
- 交互型任务:10-50ms,保证响应
- 计算型任务:50-200ms,减少切换开销
- 混合场景:建议用CFS,别自己手撸时间片
注意:现代操作系统很少单独用纯时间片轮转。CFS本质上是一种加权的时间片分配,但更灵活。如果你在写RTOS或者裸机调度,时间片轮转仍然是最可靠的选择。
2.3 优先级调度:谁急谁先上
优先级调度分两种:
- 静态优先级:创建时定死,运行中不变
- 动态优先级:根据行为调整(比如I/O密集型任务自动提权)
Linux的实时优先级范围是0-99,普通优先级通过nice值映射到100-139。数值越小,优先级越高。
优先级反转——这是个经典坑。低优先级任务持有锁,高优先级任务等锁,中优先级任务抢CPU,结果高优先级任务反而被低优先级任务阻塞。
我曾经在一个音视频处理流水线里遇到过:解码线程(高优先级)等锁,锁被日志写入线程(低优先级)持有,结果一个中优先级的网络线程把CPU占满了,解码线程活活饿死。解决方案是给日志线程也提优先级,或者用优先级继承协议。
避坑指南:
- 不要轻易给业务线程设实时优先级(99),搞不好系统都卡死
- 锁的持有者优先级要高于或等于等待者
- 监控/proc/sys/kernel/sched_rt_runtime_us,防止实时任务饿死普通任务
2.4 多级反馈队列:兼顾响应与吞吐
多级反馈队列(MLFQ)是很多现代操作系统的调度基石。它维护多个队列,每个队列优先级不同,时间片也不同。
核心规则:
- 新任务进入最高优先级队列
- 时间片内主动让出CPU(I/O操作),保持优先级
- 时间片用完还没完成,降级到下一级队列
- 优先级高的队列先执行,同级别轮转
说白了,就是「交互型任务优先,计算型任务靠后」。I/O密集任务很快就能再次获得CPU,计算密集任务慢慢降到低优先级队列,用大时间片跑。
我记得调优一个Web服务器时,发现某些请求偶尔延迟飙高。用perf一看,是内核的MLFQ把长连接的处理线程降级了,导致响应变慢。解决方案是把Nginx的worker进程绑定到实时优先级,绕过MLFQ的降级逻辑。
2.5 知识体系总览
下面这张图把四种调度策略的关系和适用场景串起来了:
2.6 实战:如何选择调度策略
我个人的经验是:
- 通用业务:用CFS默认配置,别乱动
- 延迟敏感:调小
sched_latency_ns,考虑绑定CPU - 计算密集:调大
sched_min_granularity_ns,减少切换 - 混合负载:用cgroup隔离,不同组用不同调度策略
快速检查当前调度器:
cat /sys/kernel/debug/sched_features
cat /proc/sys/kernel/sched_*