实时调度器原理与调优: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, ¶m);
// 获取实际时间片长度
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值对比,就能看出你的调度配置在压力下是否扛得住。
知识体系总览
下面这张图把本章的核心逻辑串起来了。从策略选择到参数配置,再到测量验证,是一条完整的链路。
我的工作流:先根据任务特性选策略,再配优先级和时间片,然后用cyclictest+hackbench测延迟。如果Max值超标,回头调整优先级或考虑SCHED_DEADLINE。循环迭代,直到指标达标。