调度策略与优先级:nice值、实时优先级、SCHED_OTHER/SCHED_FIFO/SCHED_RR
调度策略和优先级,说白了就是内核决定「下一个该谁跑」的规则。很多开发者在用户层调了 nice 值却发现没效果,或者在实时任务里用错了策略导致系统卡死——这些坑我都踩过。今天咱们就把这三类调度策略和优先级机制彻底讲透。
一、调度策略的三大家族
Linux 内核把调度策略分成三大类,分别对应不同的应用场景。我习惯用一张图来理解它们的关系:
二、SCHED_OTHER 与 nice 值
SCHED_OTHER 是默认策略,内核里叫 CFS(完全公平调度器)。它的核心思想很简单:让每个进程都能公平地获得 CPU 时间。
nice 值到底怎么用?
nice 值的范围是 -20 到 +19。数值越小,优先级越高。但注意——它影响的是「权重」,不是直接优先级。CFS 用 nice 值计算进程的虚拟运行时间(vruntime),vruntime 越小,越容易被调度。
关键公式(内核视角):
// 内核中 nice 值到权重的映射
static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
你看,nice=0 的权重是 1024,nice=19 的权重只有 15。这意味着一个 nice=-20 的进程,获得的 CPU 时间是 nice=19 进程的 88761/15 ≈ 5917 倍。
我的经验: 调 nice 值别指望立竿见影。它影响的是「比例」,不是「绝对优先级」。我曾经在数据库服务器上把日志进程的 nice 调到 19,结果它还是偶尔抢 CPU——因为 CFS 保证每个进程至少能运行一小段时间。
三、实时调度策略:SCHED_FIFO 与 SCHED_RR
实时策略才是真正的「硬核」调度。它们的优先级范围是 0~99,值越大优先级越高。实时进程永远优先于普通进程运行。
SCHED_FIFO:先入先出,不吃完不放手
SCHED_FIFO 没有时间片概念。一个 FIFO 进程一旦获得 CPU,就会一直运行,直到它主动让出(比如 sleep、等待 I/O),或者被更高优先级的实时进程抢占。
// 设置 SCHED_FIFO 的示例
#include <sched.h>
#include <stdio.h>
int main() {
struct sched_param param;
param.sched_priority = 80; // 实时优先级 80
if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) {
perror("sched_setscheduler");
return 1;
}
printf("当前策略: SCHED_FIFO, 优先级: %d\n",
sched_getparam(0, ¶m) == 0 ? param.sched_priority : -1);
return 0;
}
我曾经踩过的坑: 有一次我把一个计算密集型任务设成 SCHED_FIFO,优先级 99,结果它把系统所有 CPU 吃满,连 ssh 都连不上了。因为 FIFO 进程不会主动让出,而 shell 是普通进程,根本抢不过它。后来我只能按重启键。
教训: 用 SCHED_FIFO 一定要留一个「逃生通道」——要么绑定 CPU,要么在代码里加 sched_yield() 主动让出。
SCHED_RR:带时间片的轮转
SCHED_RR 和 FIFO 类似,但多了时间片限制。同一优先级的 RR 进程会轮流执行,时间片用完后自动切换到下一个。这能避免单个进程霸占 CPU。
| 特性 | SCHED_FIFO | SCHED_RR |
|---|---|---|
| 时间片 | 无,一直运行到阻塞或主动让出 | 有,默认 100ms(可配) |
| 同优先级调度 | 先到先服务,后到的排队 | 轮转,时间片用完换下一个 |
| 适用场景 | 需要低延迟、短任务的实时系统 | 需要公平分配 CPU 的实时任务 |
| 风险 | 容易饿死同优先级进程 | 相对公平,但仍有优先级抢占 |
四、优先级与调度策略的交互
这里有个容易混淆的点:实时优先级和 nice 值是两套独立的体系。实时进程(FIFO/RR)的优先级是 0~99,普通进程(OTHER)的优先级是 100~139(由 nice 值映射)。内核调度时,先看是不是实时进程,再看优先级。
// 内核调度核心逻辑(简化版)
if (task->policy == SCHED_FIFO || task->policy == SCHED_RR) {
// 实时进程:按优先级 0~99 调度
pick_next = find_highest_rt_prio();
} else {
// 普通进程:按 CFS 的 vruntime 调度
pick_next = pick_next_entity(cfs_rq);
}
优先级映射关系:
- 实时优先级 0~99 → 内核优先级 0~99
- nice -20~+19 → 内核优先级 100~139
- 实时进程永远优先于普通进程
- 同优先级下,FIFO 按等待时间,RR 按时间片
五、实战:如何选择调度策略
我个人的选择原则很简单:
- 普通应用:用 SCHED_OTHER,别乱改 nice 值。默认的 CFS 已经优化得很好了。
- 需要低延迟:比如音频处理、工业控制,用 SCHED_FIFO,但优先级别超过 50,留点余地给系统进程。
- 多个实时任务:用 SCHED_RR,避免一个任务饿死其他任务。
- 批处理任务:用 SCHED_BATCH,它和 OTHER 类似,但更偏向吞吐量,减少唤醒次数。
查看当前进程的调度策略:
# 查看 PID 为 1234 的调度策略和优先级
chrt -p 1234
# 输出示例:
# pid 1234's current scheduling policy: SCHED_OTHER
# pid 1234's current scheduling priority: 0
# 查看所有线程的调度信息
ps -eo pid,tid,class,rtprio,ni,cmd | head -20
六、避坑指南
最后分享几个我踩过的坑:
- 别把实时优先级设成 99:内核有很多关键线程(比如 ksoftirqd)也用实时优先级 99。你抢了它的 CPU,网络中断就可能丢包。
- nice 值对 I/O 密集型任务效果有限:因为这类任务大部分时间在等 I/O,调度器根本不需要抢 CPU。
- 多核环境下注意 CPU 亲和性:实时任务最好绑定到特定 CPU,避免在核间迁移导致缓存失效。
- 用 chrt 命令测试很方便:不用写代码就能改策略和优先级,适合快速验证。
# 将当前 bash 设为 SCHED_FIFO,优先级 50
chrt -f -p 50 $$
# 启动一个实时进程
chrt -r 70 ./my_realtime_app
调度策略的选择,说白了就是权衡:你要低延迟,就得接受可能卡死系统的风险;你要公平,就得忍受偶尔的调度延迟。理解这些 trade-off,才能在实际项目中做出正确的选择。