实时调度器原理与调优:SCHED_FIFO/SCHED_RR/SCHED_DEADLINE

调度器是实时系统的核心。说白了,它决定了哪个任务在什么时候能拿到CPU。我见过不少项目,应用层代码写得漂亮,但调度策略没选对,一上高负载就崩。今天咱们就把Linux实时调度器的底牌翻出来看看。

三种实时调度策略的本质区别

Linux内核提供了三种实时调度策略:SCHED_FIFO、SCHED_RR和SCHED_DEADLINE。它们的目标都是保证任务在确定时间内完成,但实现思路完全不同。

策略 调度依据 时间片 适用场景
SCHED_FIFO 静态优先级 无(运行到阻塞或主动让出) 短周期、高优先级控制任务
SCHED_RR 静态优先级 + 时间片轮转 默认100ms(可调) 同优先级任务需要公平分时
SCHED_DEADLINE 截止时间(Earliest Deadline First) 由Runtime/Deadline/Period计算 强实时、有明确周期和截止时间

核心原则:实时任务的优先级永远高于普通SCHED_OTHER任务。一旦实时任务就绪,普通任务会被立即抢占。这也是为什么实时任务写不好,整个系统都可能挂掉。

SCHED_FIFO:最直接的抢占式调度

SCHED_FIFO遵循一个简单的规则:高优先级任务就绪时,立即抢占低优先级任务。同优先级任务之间,谁先就绪谁先运行,直到它主动阻塞或主动让出CPU。

我在项目中遇到过一个问题:两个SCHED_FIFO任务优先级相同,其中一个任务写了死循环。结果另一个任务永远拿不到CPU,整个控制链路断了。嗯,这就是FIFO的陷阱——同优先级任务之间没有时间片强制切换。

我的建议:使用SCHED_FIFO时,尽量让同优先级任务数量少。如果必须有多个,确保每个任务都有阻塞点(比如等待信号量、休眠、读取设备)。

SCHED_RR:带时间片的轮转调度

SCHED_RR在FIFO的基础上加了时间片机制。同优先级任务轮流运行,每个任务用完时间片后,被放到队尾,下一个同优先级任务开始运行。

时间片默认是100ms,但可以通过sched_rr_get_interval()查看,或者通过/proc/sys/kernel/sched_rr_timeslice_ms调整。我个人习惯把时间片设短一些,比如10ms,这样响应更平滑。

// 设置SCHED_RR策略,优先级50,时间片使用系统默认值
struct sched_param param;
param.sched_priority = 50;
sched_setscheduler(0, SCHED_RR, &param);

// 获取实际时间片长度
struct timespec ts;
sched_rr_get_interval(0, &ts);
printf("RR time slice: %ld ns\n", ts.tv_nsec);

注意:SCHED_RR的时间片只对同优先级任务生效。如果高优先级任务一直就绪,低优先级任务永远拿不到时间片。优先级设计才是关键。

SCHED_DEADLINE:真正的硬实时调度

SCHED_DEADLINE是Linux 3.14引入的,基于Earliest Deadline First(EDF)算法。它需要你提供三个参数:

  • Runtime:任务每次运行需要多少CPU时间
  • Deadline:任务必须在多长时间内完成
  • Period:任务的释放周期

调度器保证:只要总利用率不超过100%,每个任务都能在Deadline之前完成。这比FIFO/RR的静态优先级灵活得多。

// 使用sched_setattr设置SCHED_DEADLINE
struct sched_attr attr;
memset(&attr, 0, sizeof(attr));
attr.size = sizeof(attr);
attr.sched_policy = SCHED_DEADLINE;

// Runtime = 10ms, Deadline = 40ms, Period = 100ms
attr.sched_runtime = 10 * 1000 * 1000;    // 10ms in ns
attr.sched_deadline = 40 * 1000 * 1000;   // 40ms in ns
attr.sched_period = 100 * 1000 * 1000;    // 100ms in ns

sched_setattr(0, &attr, 0);

避坑指南:我曾经把Runtime设得比实际执行时间还小,结果任务频繁超时,内核直接把它杀了。记住,Runtime必须是任务最坏情况执行时间(WCET),别往小了估。

调度优先级与时间片配置

实时优先级范围是1-99,数字越大优先级越高。SCHED_DEADLINE的优先级实际上是-1,但它比任何FIFO/RR任务都优先。

配置优先级时,我建议遵循几个原则:

  • 关键控制任务:80-99,比如电机控制、传感器采集
  • 通信任务:50-79,比如CAN总线收发、网络协议栈
  • 监控/日志任务:1-49,这些任务可以容忍偶尔延迟

时间片配置在/proc/sys/kernel/sched_rr_timeslice_ms,单位毫秒。对于大多数嵌入式场景,10-50ms比较合理。太短会导致频繁上下文切换,太长会影响响应。

RT组调度(RT Group Scheduling)

RT组调度是内核的一个特性,它允许你对实时任务组进行带宽限制。默认情况下,所有实时任务可以吃掉100%的CPU。但如果你不想让实时任务把系统锁死,可以启用RT组调度。

启用方式:

# 挂载cgroup文件系统
mount -t cgroup -o cpu none /sys/fs/cgroup/cpu

# 创建RT组
mkdir /sys/fs/cgroup/cpu/rt_group

# 设置该组实时任务最多使用70%的CPU时间(每1秒中最多0.7秒)
echo 700000 > /sys/fs/cgroup/cpu/rt_group/cpu.rt_runtime_us
echo 1000000 > /sys/fs/cgroup/cpu/rt_group/cpu.rt_period_us

# 将任务加入组
echo $$ > /sys/fs/cgroup/cpu/rt_group/cgroup.procs

注意:RT组调度是一把双刃剑。它保护了系统不被实时任务饿死,但也可能让你的实时任务因为带宽限制而错过截止时间。我一般只在非关键调试阶段启用,生产环境慎用。

调度延迟测量工具

光说不练假把式。调度策略配好了,怎么知道实际效果?两个工具必须掌握:cyclictest和hackbench。

cyclictest:测量调度延迟

cyclictest是rt-tests套件里的核心工具。它创建一个高优先级实时线程,定时唤醒并测量实际唤醒时间与预期时间的差值,这个差值就是调度延迟。

# 基本用法:测量SCHED_FIFO优先级80的延迟
cyclictest -t 1 -p 80 -n -i 1000 -l 100000

# 参数说明:
# -t 1    :1个测量线程
# -p 80   :优先级80
# -n      :使用nanosleep
# -i 1000 :间隔1000微秒(1ms)
# -l 100000:测量10万次

输出示例:

T: 0 (7) P: 80 I: 1000 C: 100000 Min: 2 Act: 5 Avg: 4 Max: 27

重点关注Max值,它代表最坏情况延迟。在我的一个项目中,没优化前Max是200μs,调整了中断亲和性和优先级后,降到了15μs。

hackbench:制造负载压力

hackbench用来模拟系统负载,配合cyclictest一起用,才能测出真实场景下的调度延迟。

# 启动hackbench制造压力(20组进程对)
hackbench -p -l 100000 -g 20 &

# 同时运行cyclictest测量延迟
cyclictest -t 1 -p 80 -n -i 1000 -l 100000

我习惯的做法是:先空载测一次,再带负载测一次。两次的Max值对比,就能看出你的调度配置在压力下是否扛得住。

知识体系总览

下面这张图把本章的核心逻辑串起来了。从策略选择到参数配置,再到测量验证,是一条完整的链路。

实时调度器知识体系 实时调度器原理与调优知识体系 SCHED_FIFO SCHED_RR SCHED_DEADLINE 静态优先级 无时间片 静态优先级 时间片轮转 Runtime/Deadline Period 优先级设计 + 时间片配置 + RT组调度带宽限制 优先级范围1-99 | sched_rr_timeslice_ms | cpu.rt_runtime_us cyclictest 测量调度延迟 hackbench 制造系统负载 Min / Act / Avg / Max 延迟指标

我的工作流:先根据任务特性选策略,再配优先级和时间片,然后用cyclictest+hackbench测延迟。如果Max值超标,回头调整优先级或考虑SCHED_DEADLINE。循环迭代,直到指标达标。