2. 任务模型定义:周期性任务、偶发任务、非周期性任务、任务优先级与截止时间

好,咱们正式开始聊任务模型。这一节是调度策略的地基,地基没打好,后面盖什么楼都得塌。我个人习惯,在接手一个混合仪表盘项目时,第一件事不是看代码,而是先把所有任务分类搞清楚。

说白了,任务模型就是回答三个问题:
这个任务多久来一次?
它最晚什么时候必须干完?
它和别的任务抢CPU时谁说了算?

咱们一个一个拆开讲。

2.1 周期性任务

周期性任务,顾名思义,就是每隔固定时间触发一次。比如仪表盘上的车速刷新,每10ms更新一次;发动机转速,每5ms采集一次。这类任务的特点是:时间确定,频率固定

我在项目中遇到过,有人把周期性任务的周期设得太紧,结果CPU负载直接飙到95%。你想想看,一个10ms的任务,如果执行时间就占了8ms,那留给其他任务的时间窗口就只剩2ms了。这不出问题才怪。

定义周期性任务时,需要明确三个参数:

  • 周期 T:两次触发之间的时间间隔
  • 执行时间 C:任务实际占用CPU的时间
  • 截止时间 D:任务必须完成的时间点,通常 D = T

关键约束:对于周期性任务,必须满足 C ≤ T,否则任务永远跑不完。这叫可调度性分析的基本条件。

2.2 偶发任务

偶发任务,名字听着有点绕。其实说白了,就是「偶尔来一次,但来了就必须处理」的任务。比如仪表盘上的故障诊断,正常情况下不触发,但一旦检测到某个传感器异常,就得立刻启动诊断流程。

偶发任务和周期性任务最大的区别是:触发时间不确定,但触发后有最小间隔要求

举个例子,某个报警任务,触发后至少要等100ms才能再次触发。这个100ms就是最小到达间隔。为什么要有这个限制?防止任务被频繁触发导致CPU被撑爆。

我曾经在一个项目中,偶发任务没设最小间隔,结果某个传感器故障后,中断信号像机关枪一样扫过来,CPU直接死机。嗯,从那以后我再也不敢忽略这个参数了。

任务类型 触发方式 最小间隔 典型例子
周期性任务 固定时间 等于周期 车速刷新
偶发任务 事件触发 有最小间隔 故障诊断

2.3 非周期性任务

非周期性任务,和偶发任务有点像,但更「随意」。它没有最小间隔约束,什么时候来、来多少次,完全看外部事件。比如用户按了一下方向盘上的某个按钮,或者CAN总线上突然来了一条紧急报文。

这类任务最让人头疼。为什么?因为它的到达时间完全不可预测。你没法像周期性任务那样做静态分析,只能靠调度策略来动态应对。

我个人习惯,对于非周期性任务,会额外加一个最大执行时间预算。比如「这个任务最多跑5ms,超时就强制挂起」。否则,一个非周期性任务可能把整个系统拖垮。

注意:非周期性任务如果没有执行时间上限,在混合仪表盘这种实时系统中是极其危险的。我曾经见过一个非周期性任务因为数据量过大,执行了200ms,导致所有周期性任务全部错过截止时间,仪表盘直接黑屏。

2.4 任务优先级

优先级,就是给任务排个队。谁重要谁先跑。在混合仪表盘里,优先级通常分为静态优先级和动态优先级两种。

静态优先级:任务创建时就定死了,运行期间不变。比如安全相关的任务(刹车、气囊)永远最高优先级,界面刷新任务次之,日志记录任务最低。

动态优先级:运行时根据某些条件调整。比如一个任务快截止了,临时提高它的优先级。

我建议,在嵌入式实时系统中,优先使用静态优先级。原因很简单:可预测。动态优先级虽然灵活,但分析起来太复杂,容易出死锁或优先级反转的问题。

避坑指南:我曾经在一个项目中用了三层优先级嵌套,结果低优先级任务持有一个锁,高优先级任务等这个锁,中间优先级任务抢占了CPU——典型的优先级反转。后来加了优先级继承协议才解决。所以,优先级不是越多越好,够用就行。

2.5 截止时间

截止时间,就是任务的「死线」。过了这个点,任务即使跑完也没意义了。在混合仪表盘里,截止时间分为硬截止和软截止。

  • 硬截止:错过就是事故。比如安全气囊触发任务,必须在碰撞发生后10ms内完成,否则气囊不弹出。
  • 软截止:错过可以接受,但质量下降。比如仪表盘动画刷新,偶尔卡一帧问题不大,但一直卡用户就会投诉。

定义截止时间时,有一个常见误区:把截止时间和周期混为一谈。其实它们可以不同。比如一个周期性任务周期是20ms,但截止时间可以设为15ms,留出5ms的余量给其他任务。

嗯,这里要注意:截止时间越紧,任务的可调度性越差。所以不要为了「保险」把截止时间设得太短,否则反而会降低系统利用率。

2.6 任务模型总结

好了,咱们把四种要素串起来。一个完整的任务模型定义,至少包含以下信息:

// 任务模型定义示例
typedef struct {
    uint32_t task_id;          // 任务ID
    uint8_t  type;             // 0:周期 1:偶发 2:非周期
    uint32_t period_ms;        // 周期(仅周期任务有效)
    uint32_t min_interval_ms;  // 最小到达间隔(仅偶发任务有效)
    uint32_t exec_time_us;     // 最坏执行时间
    uint32_t deadline_ms;      // 相对截止时间
    uint8_t  priority;         // 优先级(0最高)
    uint8_t  hard_deadline;    // 1:硬截止 0:软截止
} TaskModel_t;

这个结构体,基本覆盖了咱们今天讲的所有内容。你想想看,有了这个模型,调度器就知道:
- 这个任务什么时候该跑
- 它最多跑多久
- 它必须在什么时候完成
- 它和别的任务抢资源时谁优先

下一节,咱们会基于这个模型,开始讲具体的调度算法。到时候你会发现,模型定义得越清晰,调度策略实现起来就越顺手。