4、分区调度算法:P-EDF、P-RM与任务到核的静态分配
好,咱们接着聊分区调度。前面几章我们把多核调度的整体框架搭起来了,也聊了全局调度那些事儿。这一章,我重点讲讲分区调度里的三个核心问题:P-EDF、P-RM,还有任务到核的静态分配。
说实话,我在实际项目中,分区调度用得比全局调度多得多。为什么?因为它的确定性好,分析起来心里有底。你想想看,每个核跑自己的任务集,互不干扰,出了问题也好定位。嗯,这里有个前提——你得把任务分对地方。
4.1 分区调度的基本思想
分区调度,说白了就是「分而治之」。我们把整个系统的任务,按照某种规则,提前分配到各个CPU核心上。每个核心独立运行自己的调度器,核心之间没有任务迁移。
这样做的好处很明显:
- 隔离性强:一个核上的任务出问题,不会波及到其他核
- 可分析性好:每个核可以当作单核系统来分析可调度性
- 实现简单:不需要复杂的负载均衡机制
但代价呢?就是灵活性差。一旦分配好了,运行期间不能动态调整。我曾经在一个项目中吃过这个亏——任务分配时没考虑周全,结果某个核负载过高,另一个核却闲着。那叫一个憋屈。
4.2 分区最早截止时间优先(P-EDF)
P-EDF,就是把EDF调度算法用在每个核上。每个核上的任务,按照绝对截止时间排序,截止时间越早,优先级越高。
我个人习惯在软实时系统中用P-EDF。为什么?因为EDF的CPU利用率可以跑到100%(理论上),比RM高出一截。但要注意,EDF有个「多米诺骨牌效应」——一旦一个任务超期,后面的任务可能跟着全崩。
核心要点:P-EDF的可调度性条件
对于每个核上的n个任务,如果满足:
∑(Cᵢ / Tᵢ) ≤ 1
那么该核上的所有任务都是可调度的。其中Cᵢ是任务i的最坏执行时间,Tᵢ是任务i的周期。
这里有个坑——上面的条件只是充分条件,不是必要条件。也就是说,即使利用率超过1,也不一定就不可调度。但工程上,我建议你老老实实按这个条件来,别去赌那个「不一定」。
4.3 分区速率单调(P-RM)
P-RM就更有意思了。它是固定优先级调度,任务的优先级由周期决定——周期越短,优先级越高。
我在做硬实时系统时,更倾向于用P-RM。原因有三:
- 实现简单:优先级固定,运行时开销小
- 可预测性强:高优先级任务不会被低优先级任务阻塞太久
- 分析工具成熟:响应时间分析(RTA)方法很完善
但P-RM有个著名的「利用率天花板」问题。对于n个任务,可调度性的充分条件是:
∑(Cᵢ / Tᵢ) ≤ n × (2^(1/n) - 1)
当n→∞时,这个值趋近于ln2 ≈ 0.693。也就是说,任务越多,你能用的CPU资源越少。我曾经在一个8核系统上,每个核跑了6个任务,结果利用率只能用到70%左右,剩下的30%就这么浪费了。心疼啊。
我的建议:如果任务周期差异很大(比如有的1ms,有的100ms),用P-RM比较合适。如果任务周期都差不多,P-EDF可能更好。
4.4 任务到核的静态分配
这才是分区调度的精髓所在。任务分得好不好,直接决定了系统能不能跑起来。
静态分配,就是在系统启动前,把每个任务固定到某个核上。分配策略有很多种,我挑几个常用的说说:
| 分配策略 | 核心思想 | 适用场景 |
|---|---|---|
| 首次适应(First-Fit) | 按顺序找第一个能放下的核 | 任务数量少,快速原型 |
| 最佳适应(Best-Fit) | 找剩余容量最接近任务需求的核 | 追求高利用率 |
| 最差适应(Worst-Fit) | 找剩余容量最大的核 | 平衡负载 |
| 分组分配(Partitioning) | 按任务类型分组,每组一个核 | 功能模块化设计 |
我个人比较喜欢最佳适应策略。虽然计算量稍大,但能最大化利用CPU资源。不过要注意,最佳适应容易造成「碎片化」——每个核上都剩一点零碎容量,但放不下新任务。
避坑指南:我曾经在一个项目中,用首次适应分配了20个任务到4个核上。结果跑起来才发现,有两个核的利用率超过95%,另外两个核才50%。后来改用最佳适应,重新分配了一遍,利用率均衡多了。所以,分配策略一定要根据实际负载来选,别偷懒。
4.5 一个简单的分配示例
假设我们有4个任务,要分配到2个核上。任务参数如下:
| 任务 | 周期T (ms) | 执行时间C (ms) | 利用率U = C/T |
|---|---|---|---|
| Task1 | 10 | 3 | 0.30 |
| Task2 | 20 | 5 | 0.25 |
| Task3 | 15 | 4 | 0.27 |
| Task4 | 50 | 8 | 0.16 |
用最佳适应策略,分配过程是这样的:
- 先排个序,按利用率从大到小:Task1(0.30), Task3(0.27), Task2(0.25), Task4(0.16)
- Task1放到Core0,Core0剩余容量:1 - 0.30 = 0.70
- Task3放到Core0?0.27 ≤ 0.70,可以。Core0剩余:0.70 - 0.27 = 0.43
- Task2放到Core0?0.25 ≤ 0.43,可以。Core0剩余:0.43 - 0.25 = 0.18
- Task4放到Core0?0.16 ≤ 0.18,可以。Core0剩余:0.18 - 0.16 = 0.02
最终,所有任务都放到了Core0上,Core1空闲。这显然不是最优解。如果手动调整一下:
// 更好的分配方案
Core0: Task1(0.30) + Task2(0.25) = 0.55
Core1: Task3(0.27) + Task4(0.16) = 0.43
这样两个核的负载就均衡多了。所以,自动分配算法只是起点,人工调优才是关键。
4.6 总结与建议
好了,这一章的内容就这些。我最后给你几个实用建议:
- 选P-EDF还是P-RM? 看你的实时性要求。硬实时选P-RM,软实时选P-EDF。
- 任务怎么分? 先用自动分配算法打个底,再根据实际运行数据手动调优。
- 别忘了留余量:别把CPU利用率推到100%,留个10%-20%的余量,给未来扩展和突发负载。
下一章,我们会聊聊混合调度策略——把分区调度和全局调度结合起来。嗯,那才是真正考验架构师功底的地方。