从优先级思维切换到截止时间思维
第一次接触 SCHED_DEADLINE,很容易把它当成 SCHED_FIFO 的替代品。毕竟都是实时调度策略,优先级高一点、切换快一点,看起来差别不大。但实际用它调一个稍复杂的实时任务集就会发现,SCHED_DEADLINE 真正改变的,是让调度器从“谁先执行”切换到“任务是否在自己要求的截止时间内完成”。Linux 内核从 3.14 开始引入这套实时调度策略,它把实时调度问题表达为“每个周期给任务分配多少 CPU 时间、最晚在什么时间点之前用完”,而不是单纯靠静态优先级排队。

在单核系统上,这个模型很清晰。最早截止时间优先(EDF)在理论上是最优的抢占式调度算法,只要任务总利用率不超过 100%,就存在一个可行的调度结果。但实际 Linux 用 SCHED_DEADLINE 时并没有直接裸用 EDF,而是叠加了带宽控制机制(CBS)。CBS 负责把每个任务的预算限制在 runtime 以内,避免某个任务执行时间超长拖垮其他实时任务。
不过,当问题升级到多核,事情就不再是“总利用率小于核数”这么简单。这里涉及任务怎么分配到核、什么时候允许迁移、迁移损耗怎么算、以及内核的调度队列怎么做,才能在没有全局锁的情况下接近 EDF 的效果。
runtime、deadline、period 是怎么配合的
SCHED_DEADLINE 的接口不复杂。每个实时任务由三个参数描述:runtime 是任务在一个周期内预计需要的执行时间,period 是任务释放的周期长度,deadline 则是相对于周期起点必须完成的时间点。Linux 里通过 sched_setattr 一次设置这三个参数。
struct sched_attr attr = {0};
attr.size = sizeof(attr);
attr.sched_policy = SCHED_DEADLINE;
attr.sched_runtime = 3000000; /* 3 ms */
attr.sched_deadline = 10000000; /* 10 ms */
attr.sched_period = 10000000; /* 10 ms */
if (sched_setattr(0, &attr, 0) < 0) {
perror("sched_setattr");
}
这段配置的意思是:任务每 10ms 释放一次,最多在 10ms 内完成,每个周期最多消耗 3ms CPU 时间,利用率是 30%。调这个接口需要 CAP_SYS_NICE 权限,或者在 cgroup 里允许对应的 RT 带宽。
一个容易被忽略的约束是 period 必须不小于 deadline,deadline 必须不小于 runtime。如果设成 period=10ms、deadline=5ms、runtime=1ms,语义就是任务每 10ms 才释放一次,但释放后必须在 5ms 内完成。调度器会按这个更早的截止时间来安排位置。
CBS 机制会给每个任务一个执行预算。任务实际运行时预算递减,预算归零就强制挂起,直到下一个周期预算恢复。这个动作叫 throttling。很多第一次用 SCHED_DEADLINE 的人看到任务每隔一段时间莫名停顿,多半是 runtime 设得太小,真实执行时间超过了预算。
多核上的分布式 EDF
一个四核 SoC 上同时跑电流环和位置环是很有代表性的场景。电流环 1kHz,周期 1ms,执行时间只有 0.2ms,但 deadline 非常急;位置环 100Hz,周期 10ms,执行时间却有 5ms。用 SCHED_FIFO 很难排:电流环必须总占着 CPU,否则随时可能超时,但位置环稍微一多跑,又会把 CPU 占住。用 SCHED_DEADLINE 就可以把电流环设成 runtime 0.2ms、deadline 0.5ms、period 1ms,位置环设成 runtime 5ms、deadline 10ms、period 10ms。调度器按截止时间决定先跑谁,比固定优先级灵活得多。
但这个灵活是有代价的。实时调度理论把多核方案分成两类:分区调度和全局调度。分区调度把每个任务固定在一个核上,每个核独立跑 EDF,逻辑简单、可调度性判断也可靠,只是任务不能利用其他核的空闲时间,CPU 碎片会让一部分实时容量白白浪费。全局调度则维护一个所有核共享的可运行任务集合,任何空闲核都可以取最紧急的任务,理论利用率上限更高,但任务迁移会带来缓存失效和锁开销,而且多核 EDF 的可调度性分析远没有单核那么干净。
Linux 的 SCHED_DEADLINE 并没有做成纯全局队列。每个 CPU 各有一个 dl_rq,本地按 deadline 排序;另外通过 push/pull 机制,在合适的时机把任务推送到其他核或从其他核拉取任务。尽量让任务留在当前核,只有在本地紧张、别处空闲时才迁移,这样既保留了一部分全局调度的灵活性,又避免了全局锁和频繁迁移。
| 调度方式 | 任务与核的关系 | 迁移开销 | 可调度性分析难度 | 适用场景 |
|---|---|---|---|---|
| 分区 EDF | 静态绑定 | 无 | 每核独立判断,简单 | 任务集合稳定,缓存亲和性敏感 |
| 全局 EDF | 任意核可执行 | 可能频繁 | 需要额外条件,较复杂 | 任务多,CPU 利用率不均 |
| Linux SCHED_DEADLINE | 本地优先,按需推拉 | 相对可控 | 内核会做全局检查,仍要用户建模 | 多数多核 Linux 实时负载 |
多核可调度性:什么东西在决定“保证”
内核在向系统里加入一个 SCHED_DEADLINE 任务时,会做一个全局的带宽检查,通过才会接受这个任务。简单说,所有 deadline 任务的 runtime/period 之和不能超过 CPU 核数,否则新任务可能被拒绝。这个检查是必要的,但不是充分的。
一个常见误区是把“总利用率小于核数”当成“每个任务都能满足截止时间”。设想两个任务各占 0.6 的利用率,跑在双核上,总利用率只有 1.2,还没超过 2。如果这两个任务频繁访问同一个锁,或者其中一个经常迁移,那实际行为就会偏离理论模型,出现 deadline miss。调度器保证了每个任务最多消耗声明的那部分计算预算,却没有义务保证你的 WCET 估计是准的。
所以在多核上,我通常建议把它当成一个尽力而为的调度机制使用,而不是当作硬实时的充分条件。更实际的做法是先用“分区”思路把每个核上的任务集固定下来,再让每个核的利用率总和低于 70%~80%。这个水位可以吸收任务的运行波动、中断延迟和内核调度的开销。等系统稳定运行之后,再尝试用工具测出真实余量,决定要不要提高利用率。
工程上常见的几个误区
说了这么多,再整理几个实际项目里最容易踩的坑。
- 把 runtime 当成平均执行时间。SCHED_DEADLINE 的预算必须覆盖最坏情况,否则 CBS 会强制节流。要用 perf、ftrace 或者压力测试先量出最坏执行时间,再留 1.2~1.5 倍余量。
- deadline 和 period 搞混。period 是任务释放周期,deadline 是每次释放后的完成时限。如果周期 10ms、要求 5ms 内完成,deadline 应该设 5ms,而不是直接把 period 填进去。
- 以为多核会自动保证。SCHED_DEADLINE 的推拉机制能迁移任务,但它不会做实时任务的精细化负载均衡。CPU 亲和性、cpuset、isolcpus 这类隔离手段仍然要自己规划。
- 忽略中断和锁。调度策略只能控制 CPU 的使用顺序,不能控制中断延迟和临界区等待。实时任务如果要在一个有普通进程的核上运行,或者有时会被自旋锁挡住,截止时间一样会失守。
一个很常见的现场表现是:任务没跑满,CPU 使用率也不高,但就是周期性抖动。这种时候不要急着调 sched_attr,先去看中断、亲和性和 cpuset,很多时候问题不在调度器。
落地建议:从配置到验证
真要把 SCHED_DEADLINE 用到多核产品中,按下面这几步推进会稳很多。
- 先建任务模型。列出每个实时任务的周期、最坏执行时间、截止时间和依赖关系。runtime 和 deadline 都说不清楚,就不要开始编码。
- 隔离 CPU。给实时任务单独划分 cpuset,或者在 kernel cmdline 中通过 isolcpus 和 nohz_full 把干扰移走。普通任务和中断尽量不要再放在同一个核上。
- 设置参数。用 sched_setattr 设置,权限不够就通过 root 或者 cgroup 开放。建议先按分区思路绑定核,等验证通过后再尝试让内核自己推拉。
- 用 trace 验证。trace-cmd 可以记录调度切换和预算耗尽事件,出现 dl_runtime_exhausted 就说明 runtime 不足。
trace-cmd record -e sched_switch -e sched_wakeup -e dl_runtime_exhausted
这段命令会记录与实时任务调度相关的事件。看到 dl_runtime_exhausted 之后,可以先调大 runtime,但更要反思为什么最坏执行时间估计得不准。
再强调一遍余量。调度器的全局带宽检查只保证总容量不超,不保证任意组合都满足。开发阶段把利用率压到 60%~70%,生产阶段也不超过 80%,给调度和迁移留出缓冲。等系统上线后,用性能计数器和调度统计持续观察,确认边界没有漂移。
收尾:保证来自系统设计,不来自调度器
SCHED_DEADLINE 是 Linux 给实时任务提供的最接近“截止时间”语义的调度策略。它第一次让内核调度器从“优先级排序”走向“预算与截止时间管理”,在多核上也有自己的推拉式全局调度实现。但调度策略本身不等于实时性保证,内核替你管理的是 CPU 时间预算,而最坏执行时间、中断延迟、锁等待和内核抢占特性,仍然需要从整个系统层面去设计。
如果想在某个多核平台上评估它,建议从单任务单核开始,把参数行为摸透,再逐渐叠加任务和扩展核数。每加一个任务,都重新算一遍可调度性,并留出足够余量。SCHED_DEADLINE 能做得很漂亮,前提是你像对待硬件中断一样认真对待它。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/974/