第4章:任务分配策略(一):静态分配 vs 动态分配,负载均衡原则,核间依赖分析

各位同学,咱们今天聊聊多核任务分配。说实话,这是整个多核架构里最让人头疼的部分。我做了十几年嵌入式,见过太多项目在任务分配上栽跟头。有的车机系统跑着跑着就卡死,有的ADAS控制器莫名其妙丢数据——追根溯源,都是分配策略没选对。

4.1 静态分配:简单可靠,但不够灵活

静态分配,说白了就是「出厂前定死」。每个任务绑定到哪个核,编译时就决定了。运行时不能迁移,不能调整。

优点很明显:

  • 确定性高。你算好每个核的负载,跑起来基本不会变
  • 没有迁移开销。任务不用搬来搬去,省了上下文切换的功夫
  • 调试方便。出问题直接定位到具体核,不用猜

缺点也扎心:

  • 负载不均。有的核忙死,有的核闲死
  • 扩展性差。加个新功能,可能得重新分配所有任务
  • 对硬件故障不敏感。某个核坏了,绑在上面的任务全挂

我个人的习惯:安全等级高的任务(ASIL-D级别),我倾向用静态分配。比如刹车控制、转向控制这些,确定性比灵活性重要得多。我曾经在一个项目中,把制动系统的任务绑在核0上,其他所有核都不能碰。虽然核0负载高了点,但安全认证一次过。

4.2 动态分配:灵活高效,但代价不小

动态分配就灵活多了。任务可以在运行时根据负载情况,自动迁移到空闲的核上。听起来很美好,对吧?

但我要泼盆冷水:动态分配不是银弹。我见过一个团队,为了追求负载均衡,把任务迁移做得太频繁,结果迁移开销占了CPU时间的30%。得不偿失。

什么时候该用动态分配?

  • 任务执行时间波动大。比如图像处理任务,不同帧的复杂度差很多
  • 系统负载变化剧烈。比如信息娱乐系统,用户操作时负载飙升,空闲时几乎为零
  • 需要热插拔或故障恢复。某个核挂了,任务能自动迁移到其他核

注意:动态分配必须考虑缓存亲和性。任务迁移后,新核的缓存是冷的,性能会暂时下降。我曾经踩过这个坑——迁移后任务执行时间反而变长了,就是因为缓存命中率暴跌。

4.3 负载均衡原则:别让一个核累死

负载均衡,说白了就是「雨露均沾」。但怎么才算「均」?我总结了几条原则:

  1. 粒度要适中。任务分得太细,调度开销大;分得太粗,负载不均。我个人建议,单个任务的执行时间控制在1-10ms之间比较合适。
  2. 考虑优先级。高优先级任务不能因为负载均衡被低优先级任务挤占。我习惯把高优先级任务静态分配,低优先级任务动态分配。
  3. 监控实际负载。别光看理论计算。实际跑起来,中断、DMA、缓存缺失都会影响负载。我一般会在每个核上放一个负载监控任务,实时采集CPU占用率。
负载均衡策略 适用场景 我踩过的坑
轮询分配 任务执行时间相近 任务时间差异大时,负载不均
最小负载优先 负载波动大 迁移开销可能抵消收益
历史预测分配 有规律的任务模式 预测不准时反而更糟

一个小技巧:我习惯在系统启动后的前100ms做一次「冷启动分配」,把所有任务按静态策略分配好。运行过程中,如果某个核的负载超过80%,再触发动态迁移。这样既保证了启动阶段的确定性,又保留了运行时的灵活性。

4.4 核间依赖分析:别让任务互相等死

任务分配时,最容易被忽略的就是核间依赖。你想想看,任务A在核0上,任务B在核1上,但A需要B的数据。如果A先跑完了,等B的数据,那A的核就空转了。这叫「核间等待」。更糟的是,如果A和B互相等,那就是死锁。

怎么分析依赖?我一般分三步走:

  1. 画依赖图。把每个任务需要的输入、产生的输出列出来。用箭头表示数据流向。这个图能帮你一眼看出哪些任务必须串行,哪些可以并行。
  2. 找关键路径。依赖图中最长的路径,决定了系统的响应时间。我习惯把关键路径上的任务尽量放在同一个核上,减少核间通信开销。
  3. 识别共享资源。两个任务如果访问同一个内存区域、同一个外设、同一个信号量,它们就有隐式依赖。我曾经在一个项目中,两个任务分别在不同核上,但都频繁访问同一个CAN控制器,结果总线冲突导致通信延迟暴增。
// 一个简单的依赖分析示例
// 任务A: 采集传感器数据
// 任务B: 处理数据(依赖A的输出)
// 任务C: 控制执行器(依赖B的输出)

// 错误分配:A在核0,B在核1,C在核2
// 结果:核0跑完A后空闲,等核1的B;核1跑完B后空闲,等核2的C
// 三个核串行工作,效率极低

// 正确分配:A、B、C都在核0上
// 或者:A在核0,B和C在核1(如果B和C可以部分并行)

避坑指南:我曾经在一个ADAS项目中,把感知任务(A)分配在核0,规划任务(B)分配在核1,控制任务(C)分配在核2。看起来挺均衡的,对吧?但实际跑起来,A和B之间需要传递大量点云数据,核间通信带宽成了瓶颈。后来我把A和B放在同一个核上,虽然负载高了点,但整体延迟反而降了40%。

4.5 我的选择建议

说了这么多,到底怎么选?我个人的经验是:

  • 安全关键任务:静态分配。确定性第一,负载均衡第二。
  • 计算密集型任务:动态分配。比如图像处理、雷达信号处理,负载波动大,动态分配能充分利用算力。
  • I/O密集型任务:看情况。如果I/O设备有固定的中断亲和性,最好静态分配;如果没有,动态分配更灵活。
  • 混合场景:静态+动态混合。把关键任务固定,非关键任务动态迁移。这是我在量产项目中最常用的方案。

嗯,任务分配这块内容比较多。下一章咱们接着聊「任务分配策略(二):优先级继承、优先级天花板、资源访问协议」。到时候我会讲讲怎么避免优先级反转——那又是一个让人头大的问题。