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调度:从单核到多核的思维转变

单核调度很简单——同一时刻只有一个任务在跑。多核就复杂了:多个任务可以同时跑,但资源竞争也更激烈。

多核调度主要有三种模式:

  1. 全局调度(Global Scheduling)——所有核心共享一个就绪队列,任务可以在任意核心上运行
  2. 分区调度(Partitioned Scheduling)——每个核心有自己的就绪队列,任务固定分配给某个核心
  3. 混合调度(Hybrid Scheduling)——部分任务固定分配,部分任务全局调度
模式 优点 缺点
全局调度 负载均衡好,CPU利用率高 缓存亲和性差,锁竞争严重
分区调度 缓存亲和性好,可预测性强 负载可能不均,某个核心空闲另一个却爆满
混合调度 兼顾灵活性和可预测性 实现复杂,调试困难

我在一个4核的工控主板上做过实验:全局调度下,四个核心的利用率分别是85%、72%、68%、91%,看起来挺均衡。但实际性能反而不如分区调度——因为任务频繁在不同核心间迁移,L1缓存命中率从92%掉到了67%。

多核调度最大的坑:锁竞争。两个任务在不同核心上同时访问共享数据,一个拿锁另一个等着,这时候另一个核心其实在空转。我曾经见过一个系统,4核CPU实际有效并行度只有1.8——大部分时间都在抢锁。

知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以把它当作一个快速索引,遇到具体问题时回来对照。

CPU资源管理:调度算法知识体系 CPU调度核心问题 调度基础三要素 实时调度算法 非实时调度 就绪队列 调度策略 上下文切换 RMS EDF 利用率上限 vs 100% 时间片轮转(Round-Robin) 时间片大小 = 100~1000倍切换开销 多核CPU调度(三种模式) 全局调度 分区调度 混合调度 核心原则:实时系统关注截止时间,非实时系统关注公平性,多核系统关注缓存亲和性与锁竞争

这张图把调度基础、实时算法、非实时算法和多核调度串在了一起。你可以看到,所有问题最终都回到一个核心矛盾:如何在有限的计算资源下,满足每个任务的时间要求。

嗯,CPU调度这块内容就到这里。记住一点:没有完美的调度算法,只有适合你场景的调度算法。选型时多想想你的任务特点——周期固定还是动态变化?截止时间硬还是软?单核还是多核?想清楚了,调度方案自然就有了。