2. 嵌入式系统基础
CPU资源管理,说白了就是决定「下一个该谁跑」的问题。我刚开始做嵌入式时,觉得调度不就是个排队嘛,后来被一个优先级反转的bug折磨了三天,才明白这里面的水有多深。
这一章我们聚焦四个核心话题:任务调度的基本概念、两种经典实时调度算法(RMS和EDF)、非实时场景下的时间片轮转,以及多核CPU带来的新挑战。嗯,每个我都会结合项目里的真实教训来讲。
任务调度基础:你需要知道的几个关键点
调度器要回答三个问题:什么时候切换?切换给谁?切换多久?
我个人习惯把调度模型拆成三要素:
- 就绪队列——所有能跑的任务排在这里
- 调度策略——决定从队列里挑谁
- 上下文切换——保存当前任务状态,恢复下一个任务状态
你想想看,如果上下文切换开销是100μs,而你的任务只跑200μs,那CPU有三分之一的时间都在做无用功。我在一个工业控制项目里就踩过这个坑——任务切得太频繁,实际有效计算时间只剩60%。
核心指标:调度延迟 = 事件发生到任务开始执行的时间。实时系统要求这个值有上界,不能出现「偶尔卡一下」的情况。
实时调度算法:RMS vs EDF
速率单调调度(RMS)
RMS是实践中最常用的实时调度算法。它的规则很简单:周期越短的任务,优先级越高。
为什么会这样?因为周期短意味着截止时间更紧迫,给它高优先级是合理的。RMS是静态优先级调度——任务创建时优先级就定死了,运行时不变。
RMS有一个重要的可调度性条件:
U = Σ (Ci / Ti) ≤ n * (2^(1/n) - 1)
其中:
U = CPU利用率
Ci = 任务i的最坏执行时间
Ti = 任务i的周期
n = 任务数量
当n→∞时,这个上界趋近于ln2 ≈ 69.3%。也就是说,用RMS调度,CPU利用率超过69.3%就可能出问题。
我曾经犯过的错:在一个无人机飞控项目里,我算了总利用率只有65%,觉得没问题就上线了。结果高频抖动时任务执行时间超出预期,利用率瞬间飙到75%,导致一个关键控制任务连续丢deadline,飞机差点炸了。后来我学乖了——RMS一定要留至少20%的余量。
最早截止时间优先(EDF)
EDF的思路更直接:谁的截止时间最近,谁先跑。它是动态优先级调度——每个时刻优先级都可能变化。
EDF的理论利用率上限是100%,听起来比RMS强很多对吧?但实际用起来没那么美好。
| 对比项 | RMS | EDF |
|---|---|---|
| 优先级分配 | 静态,周期越短优先级越高 | 动态,截止时间越近优先级越高 |
| 利用率上限 | n*(2^(1/n)-1),约69.3% | 100%(理论上) |
| 实现复杂度 | 低,O(1)调度 | 高,需要维护就绪队列排序 |
| 过载行为 | 低优先级任务先丢deadline | 多米诺效应,多个任务同时崩溃 |
| 适用场景 | 任务数量固定,周期已知 | 任务动态创建,利用率高 |
EDF有一个致命弱点:过载时不可预测。RMS过载了,至少低优先级任务先挂,高优先级任务还能撑住。EDF过载了,所有任务可能同时错过截止时间——这就是所谓的「多米诺效应」。我在一个视频处理项目里亲眼见过,EDF调度下CPU过载10%,整个系统直接瘫痪。
我的建议:如果任务数量固定且周期已知,优先用RMS。如果任务动态变化且需要高利用率,用EDF但必须加过载保护机制——比如检测到利用率超过90%时主动降级非关键任务。
非实时调度:时间片轮转(Round-Robin)
非实时场景下,我们不在乎任务能不能赶上截止时间,只在乎公平——每个任务都能分到CPU时间。
时间片轮转是最经典的公平调度算法:
- 每个任务分配一个固定时间片(比如10ms)
- 时间片用完就切换到下一个任务
- 所有任务在就绪队列里循环
时间片大小的选择是个艺术。太短了,上下文切换开销占比大;太长了,交互式任务响应慢。
/* 简化的Round-Robin调度器伪代码 */
void round_robin_scheduler() {
while (1) {
task_t *current = get_next_from_ready_queue();
if (current == NULL) {
idle(); // 没有任务就空闲
continue;
}
// 设置定时器为时间片长度
set_timer(TIME_SLICE_MS);
// 切换到当前任务
context_switch(current);
// 时间片到,任务被抢占,放回队列尾部
enqueue_ready_queue(current);
}
}
我记得在一个物联网网关项目里,我把时间片设成5ms,结果系统有12个任务,上下文切换开销占了将近15%的CPU。后来改成20ms,响应时间虽然长了点,但整体吞吐量提升了30%。
经验法则:时间片长度建议设为上下文切换开销的100~1000倍。如果切换开销是50μs,时间片选5ms~50ms比较合理。
多核CPU调度:从单核到多核的思维转变
单核调度很简单——同一时刻只有一个任务在跑。多核就复杂了:多个任务可以同时跑,但资源竞争也更激烈。
多核调度主要有三种模式:
- 全局调度(Global Scheduling)——所有核心共享一个就绪队列,任务可以在任意核心上运行
- 分区调度(Partitioned Scheduling)——每个核心有自己的就绪队列,任务固定分配给某个核心
- 混合调度(Hybrid Scheduling)——部分任务固定分配,部分任务全局调度
| 模式 | 优点 | 缺点 |
|---|---|---|
| 全局调度 | 负载均衡好,CPU利用率高 | 缓存亲和性差,锁竞争严重 |
| 分区调度 | 缓存亲和性好,可预测性强 | 负载可能不均,某个核心空闲另一个却爆满 |
| 混合调度 | 兼顾灵活性和可预测性 | 实现复杂,调试困难 |
我在一个4核的工控主板上做过实验:全局调度下,四个核心的利用率分别是85%、72%、68%、91%,看起来挺均衡。但实际性能反而不如分区调度——因为任务频繁在不同核心间迁移,L1缓存命中率从92%掉到了67%。
多核调度最大的坑:锁竞争。两个任务在不同核心上同时访问共享数据,一个拿锁另一个等着,这时候另一个核心其实在空转。我曾经见过一个系统,4核CPU实际有效并行度只有1.8——大部分时间都在抢锁。
知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以把它当作一个快速索引,遇到具体问题时回来对照。
这张图把调度基础、实时算法、非实时算法和多核调度串在了一起。你可以看到,所有问题最终都回到一个核心矛盾:如何在有限的计算资源下,满足每个任务的时间要求。
嗯,CPU调度这块内容就到这里。记住一点:没有完美的调度算法,只有适合你场景的调度算法。选型时多想想你的任务特点——周期固定还是动态变化?截止时间硬还是软?单核还是多核?想清楚了,调度方案自然就有了。