调度策略与优先级:nice值、实时优先级、SCHED_OTHER/SCHED_FIFO/SCHED_RR

调度策略和优先级,说白了就是内核决定「下一个该谁跑」的规则。很多开发者在用户层调了 nice 值却发现没效果,或者在实时任务里用错了策略导致系统卡死——这些坑我都踩过。今天咱们就把这三类调度策略和优先级机制彻底讲透。

一、调度策略的三大家族

Linux 内核把调度策略分成三大类,分别对应不同的应用场景。我习惯用一张图来理解它们的关系:

Linux 调度策略分类与优先级体系 调度策略 SCHED_OTHER (CFS) SCHED_FIFO / SCHED_RR SCHED_BATCH / SCHED_IDLE 优先级范围: 0~39 nice值: -20 ~ +19 默认 nice=0 (优先级120) 实时优先级: 0~99 值越大优先级越高 SCHED_FIFO: 无时间片 SCHED_BATCH: 类似OTHER 但更偏向吞吐 SCHED_IDLE: 最低优先级 核心对比 特性 SCHED_OTHER SCHED_FIFO SCHED_RR 时间片 动态分配 无 固定时间片 抢占方式 完全公平 优先级抢占 时间片轮转

二、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, &param) == -1) {
        perror("sched_setscheduler");
        return 1;
    }
    printf("当前策略: SCHED_FIFO, 优先级: %d\n", 
           sched_getparam(0, &param) == 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 按时间片

五、实战:如何选择调度策略

我个人的选择原则很简单:

  1. 普通应用:用 SCHED_OTHER,别乱改 nice 值。默认的 CFS 已经优化得很好了。
  2. 需要低延迟:比如音频处理、工业控制,用 SCHED_FIFO,但优先级别超过 50,留点余地给系统进程。
  3. 多个实时任务:用 SCHED_RR,避免一个任务饿死其他任务。
  4. 批处理任务:用 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,才能在实际项目中做出正确的选择。