实时任务特性:硬实时与软实时、任务周期与截止时间、实时任务模型

聊到实时任务,很多人第一反应就是“快”。其实不完全对。实时系统的核心不是快,而是可预测——你得保证任务在某个时间点之前完成。我刚开始接触调度系统时,也踩过这个坑,以为把 CPU 频率拉高就能解决一切。后来在工业控制项目里被 deadline 狠狠教育过一次,才真正理解了实时性的本质。

硬实时 vs 软实时:差之毫厘,谬以千里

先看一个最基础的问题:任务错过截止时间会怎样?

答案不同,决定了你是做硬实时还是软实时。

特性 硬实时 (Hard Real-Time) 软实时 (Soft Real-Time)
错过截止时间的后果 系统故障、灾难性后果 性能下降、体验变差
典型场景 安全气囊、飞行控制、心脏起搏器 视频播放、在线游戏、语音通话
调度策略倾向 确定性优先,预留资源 尽力而为,允许偶尔超时
验证方式 形式化证明 + 最坏情况分析 统计概率 + 平均延迟

我在做自动驾驶项目时,激光雷达的数据处理就是硬实时。如果某个障碍物检测任务没在 10ms 内完成,车可能就撞上了。而旁边的视频流推送到座舱屏幕,偶尔卡顿一帧,用户骂两句,但不会出人命——那就是软实时。

我曾经犯过的错: 把软实时的调度策略直接套在硬实时任务上。结果在压力测试时,一个高优先级任务被低优先级任务阻塞了 200ms,导致整个控制链路崩溃。从那以后,我养成了一个习惯:先问 deadline 的后果,再选调度算法。

任务周期与截止时间:两个核心参数

实时任务通常不是跑一次就完事,而是周期性执行。比如传感器每 20ms 采集一次数据,控制算法每 50ms 计算一次输出。这里有两个关键参数你必须搞清楚:

  • 周期 (Period, T):任务两次释放之间的时间间隔。说白了,就是多久跑一次。
  • 截止时间 (Deadline, D):任务必须在什么时间点之前完成。注意,截止时间不一定等于周期。

举个例子:一个视频解码任务,每 33ms 需要解码一帧(周期 T=33ms),但解码必须在下一帧到来之前完成,所以截止时间 D=33ms。这叫“隐式截止时间”(D=T)。

还有一种情况:某个监控任务每 100ms 触发一次,但它的结果必须在 80ms 内上报,否则告警就失效了。这时 D=80ms,T=100ms,D < T。这叫“约束截止时间”。

我个人习惯在任务模型里把 D 和 T 分开记录,哪怕它们相等。为什么?因为后期做可调度性分析时,D 和 T 的关系直接影响公式选择。你想想看,Rate Monotonic 调度只适用于 D=T 的场景,如果 D < T 还用 RM,那分析结果就是错的。

一个小技巧: 在任务结构体里,永远保留 deadline 和 period 两个字段。即使当前 D=T,也显式赋值。我见过太多人偷懒只存 period,后来需求变更 D 变成 T/2,整个调度器都得重写。

实时任务模型:怎么描述一个实时任务?

搞清楚了硬软实时和周期截止时间,接下来就是怎么用数学模型描述一个实时任务。这是调度系统设计的基础,也是你写代码时真正要落地的部分。

一个经典的实时任务模型包含以下要素:

  • 释放时间 (Release Time, r):任务第一次可以被执行的时刻。对于周期性任务,通常 r=0。
  • 执行时间 (Execution Time, C):任务在最坏情况下需要占用的 CPU 时间。注意,不是平均时间,是最坏情况执行时间 (WCET)。
  • 周期 (Period, T):相邻两次释放的时间间隔。
  • 截止时间 (Deadline, D):任务必须完成的时间点,相对于释放时间。
  • 优先级 (Priority, P):任务在调度时的相对重要程度。

用数学符号表示,一个实时任务 τᵢ 可以写成:

τᵢ = (rᵢ, Cᵢ, Tᵢ, Dᵢ, Pᵢ)

其中,Cᵢ 是最关键的参数。我在项目中见过太多人把平均执行时间当成 Cᵢ,结果系统在极端负载下直接崩了。记住:WCET 不是猜出来的,是测出来的。你需要通过静态分析或压力测试,找到任务在最坏输入、最差缓存命中率、最大中断频率下的执行时间。

核心原则: 实时调度的一切分析,都建立在 WCET 的基础上。如果 WCET 不准,后面的可调度性分析全是空中楼阁。

任务集与超周期

单个任务好理解,但实际系统里是一堆任务同时跑。这些任务组成一个任务集 (Task Set)。任务集有一个重要概念叫超周期 (Hyperperiod)——所有任务周期的最小公倍数。为什么关心这个?因为任务集的调度模式会在每个超周期内重复。

举个例子:任务 A 周期 10ms,任务 B 周期 15ms,任务 C 周期 20ms。超周期 = LCM(10, 15, 20) = 60ms。也就是说,每 60ms,这三个任务的释放模式会完全重复一次。分析调度时,你只需要分析一个超周期内的行为就够了。

我习惯在系统启动时先计算超周期,然后在这个时间窗口内做可调度性验证。如果超周期太长(比如几个任务周期互质,LCM 可能大到离谱),那就需要分段分析或者用响应时间分析法。

一张图看懂实时任务模型

下面这张图展示了三个周期性实时任务在一个超周期内的释放和执行情况。你可以看到每个任务的周期、截止时间,以及它们如何在时间轴上交错。

实时任务模型:周期、截止时间与超周期 任务 A (T=10, D=10) 任务 B (T=15, D=15) 任务 C (T=20, D=20) 超周期 = LCM(10, 15, 20) = 60ms ▲ 释放点 (Release) ● 截止时间 (Deadline) █ 执行时间 (Execution) 每个任务必须在下一个释放点之前完成执行,否则就错过了截止时间

任务间的关系:独立 vs 依赖

最后聊一个容易被忽略的点:任务之间不是孤立的。它们可能有数据依赖、资源竞争、优先级继承等关系。在实时任务模型里,我们通常把任务分为两类:

  • 独立任务:不共享资源,不依赖其他任务的输出。调度起来相对简单,各管各的。
  • 依赖任务:需要等待其他任务的结果,或者共享互斥资源(如锁、缓冲区)。这时候就要考虑优先级反转的问题。

我记得在一个多传感器融合项目里,三个任务共享一个内存池。低优先级任务拿了锁,高优先级任务等着,结果中优先级任务插进来把低优先级任务抢占了——锁一直不释放,高优先级任务活活等死。这就是经典的优先级反转。后来我引入了优先级继承协议,才把这个问题解决掉。

避坑指南: 如果你在任务模型里看到共享资源,千万别直接套用 Rate Monotonic 或 Earliest Deadline First。必须先处理资源访问协议,否则可调度性分析全是错的。我曾经因为这个原因,在项目交付前一周才发现调度器在特定负载下会死锁,差点没赶上 deadline。