2、基础知识:实时操作系统(RTOS)与调度器基础,任务、周期、优先级、截止时间等核心术语
TinyML 设备上的多模型调度,本质上是在资源极度受限的微控制器(MCU)上,对多个推理任务进行时间与计算资源的分配。要理解这一过程,必须首先掌握实时操作系统(RTOS)与调度器的核心概念。与桌面操作系统追求“吞吐量”不同,RTOS 追求的是确定性——即每个任务能否在规定的截止时间(Deadline)之前完成。本章将系统梳理任务、周期、优先级、截止时间等核心术语,并解释它们如何在调度器中相互作用,为后续章节的模型调度策略打下坚实的理论基础。
上图展示了 RTOS 调度的核心要素:调度器作为中枢,管理着多个具有不同周期、执行时间和优先级的任务。在 TinyML 场景中,每个神经网络模型(如唤醒词检测、人体活动识别、异常检测)都可以视为一个独立的任务。调度器根据预设的优先级和时序规则,决定在任一时刻哪个模型占用 CPU 进行推理。下面我们将逐一拆解这些核心术语。
2.1 任务(Task)与任务状态机
核心原理:任务是 RTOS 中可独立调度的最小执行单元。在 TinyML 中,一个任务可以是一个完整的推理流程(读取传感器数据 → 预处理 → 模型推理 → 后处理 → 输出结果),也可以是其中的某个阶段。任务在生命周期中会经历多个状态:就绪(Ready)、运行(Running)、阻塞(Blocked)和挂起(Suspended)。调度器只从就绪队列中选择任务进入运行状态。
判断方法:在裸机编程中,我们通常使用一个超级循环(Super Loop)顺序执行所有功能。但在多模型场景下,这种结构会导致严重的时序耦合——如果模型 A 推理耗时 50ms,模型 B 的响应延迟就会高达 50ms。引入 RTOS 任务后,每个模型被封装为独立任务,通过调度器实现时间上的“分时复用”。
// 定义人体活动识别任务
void vHAR_Task(void *pvParameters) {
float sensor_data[64];
float result;
for(;;) {
// 1. 读取 IMU 传感器数据
read_imu_data(sensor_data);
// 2. 执行模型推理(假设为 8ms)
result = run_har_model(sensor_data);
// 3. 发送结果到通信队列
xQueueSend(xResultQueue, &result, 0);
// 4. 等待下一次周期释放(周期为 20ms)
vTaskDelay(pdMS_TO_TICKS(20));
}
}
案例拆解:假设一个智能门锁设备需要同时运行三个模型:人脸检测(周期 100ms)、活体检测(周期 200ms)、异常敲击检测(事件触发)。在裸机中,若人脸检测耗时 80ms,活体检测将被迫等待。通过 RTOS 将三者设为独立任务,活体检测任务可以在人脸检测任务阻塞等待传感器数据时抢占执行,从而保证所有模型的实时性。
2.2 周期(Period)与截止时间(Deadline)
核心原理:周期(T)是指任务两次被激活之间的时间间隔。截止时间(D)是指任务从被激活到必须完成的最晚时间。在硬实时系统中,D 通常等于 T(即任务必须在下一个周期到来前完成);在软实时系统中,D 可以大于 T,但超过 D 会导致服务质量下降。TinyML 中的模型推理任务通常属于周期型任务,例如每 50ms 执行一次关键词识别。
判断方法:确定任务的周期和截止时间是调度设计的第一步。对于传感器采样任务,周期由物理信号的最高频率决定(遵循奈奎斯特定理);对于模型推理任务,周期由应用需求决定(如视频流 30fps 对应 33ms 周期)。截止时间则取决于下游任务对数据时效性的容忍度。
| 任务类型 | 典型周期 (T) | 截止时间 (D) | 最坏执行时间 (WCET) | CPU 利用率 (C/T) |
|---|---|---|---|---|
| 唤醒词检测 | 50 ms | 50 ms | 15 ms | 30% |
| 人体活动识别 | 200 ms | 180 ms | 40 ms | 20% |
| 异常声音检测 | 事件触发 | 10 ms | 8 ms | 峰值 80% |
| BLE 通信 | 100 ms | 100 ms | 5 ms | 5% |
案例拆解:以智能音箱的语音交互为例。音频采集任务周期为 10ms(对应 16kHz 采样率,每周期采集 160 个样本),唤醒词检测任务周期为 50ms。若唤醒词检测的 WCET 为 20ms,则其 CPU 利用率为 40%。此时调度器必须确保在 50ms 周期内,音频采集任务(10ms 周期)能够被及时抢占执行,否则音频缓冲区会溢出。
2.3 优先级(Priority)与调度策略
核心原理:优先级是调度器决定任务执行顺序的关键属性。在固定优先级抢占式调度(Fixed-Priority Preemptive Scheduling)中,高优先级任务可以随时打断低优先级任务的执行。TinyML 中,事件型任务(如紧急停止检测)通常赋予最高优先级,而周期型模型推理任务根据其截止时间的紧迫程度分配优先级。
判断方法:常用的优先级分配算法是单调速率调度(RMS)——周期越短的任务优先级越高。例如,音频采集任务(T=10ms)的优先级应高于唤醒词检测任务(T=50ms)。另一种是最早截止时间优先(EDF),它动态调整优先级,截止时间越近的任务优先级越高,但 EDF 在 MCU 上实现开销较大。
// FreeRTOS 优先级配置示例
#define AUDIO_TASK_PRIORITY (configMAX_PRIORITIES - 1) // 最高优先级
#define WAKEUP_TASK_PRIORITY (configMAX_PRIORITIES - 2)
#define HAR_TASK_PRIORITY (configMAX_PRIORITIES - 3)
#define BLE_TASK_PRIORITY (configMAX_PRIORITIES - 4) // 最低优先级
案例拆解:考虑一个可穿戴设备,运行心率监测(周期 100ms)和跌倒检测(事件触发)。跌倒检测必须立即响应,因此赋予最高优先级。当跌倒检测任务被触发时,即使心率监测任务正在执行模型推理,也会被抢占。这种设计确保了安全关键功能的实时性,但代价是心率监测任务的完成时间可能被推迟。
2.4 调度器的工作机制与调度延迟
核心原理:调度器在系统时钟节拍(Tick)的驱动下,周期性检查就绪队列。每次任务切换涉及上下文切换——保存当前任务的寄存器、堆栈指针,恢复新任务的上下文。上下文切换时间(通常为 2-10μs)是调度开销的主要来源。在 TinyML 中,如果模型推理任务本身只有几毫秒,频繁的上下文切换会显著降低有效计算效率。
判断方法:评估调度器性能的关键指标是调度延迟——从任务被激活到实际开始执行的时间。它由中断延迟、调度器执行时间和上下文切换时间组成。对于硬实时任务,必须保证最坏情况下的调度延迟小于截止时间。
- 中断延迟:从硬件中断触发到 ISR 开始执行的时间,通常由 CPU 的关中断时间决定。
- 调度决策时间:调度器扫描就绪队列并选择下一个任务的时间,与任务数量成正比。
- 上下文切换时间:保存和恢复寄存器的时间,与 MCU 架构相关(Cortex-M 系列通常为 12 个周期)。
案例拆解:在 STM32F4(Cortex-M4,168MHz)上运行 FreeRTOS,系统 Tick 设为 1ms。假设有 4 个模型任务,调度器扫描就绪队列耗时约 2μs,上下文切换约 4μs。若一个高优先级任务被释放,其调度延迟约为中断延迟(0.5μs)+ 调度决策(2μs)+ 上下文切换(4μs)= 6.5μs。这个值远小于模型推理的毫秒级时间,因此调度开销可以接受。
2.5 可调度性分析(Schedulability Analysis)
核心原理:在设计多模型调度方案时,必须验证所有任务能否在截止时间内完成。对于固定优先级调度,利用率测试是最简单的充分条件:对于 n 个周期任务,如果总利用率 U = Σ(Ci/Ti) ≤ n(2^(1/n) - 1),则任务集可调度。当 n 较大时,该界限趋近于 ln2 ≈ 0.693。
判断方法:对于 TinyML 场景,通常任务数不超过 5 个,因此可以使用精确的响应时间分析(RTA)。任务的响应时间 Ri 满足:Ri = Ci + Σ(Cj × ⌈Ri/Tj⌉),其中 j 是优先级高于 i 的任务。通过迭代求解 Ri,若 Ri ≤ Di,则任务可调度。
| 任务 | 周期 T (ms) | 执行时间 C (ms) | 优先级 | 利用率 C/T |
|---|---|---|---|---|
| 音频采集 | 10 | 2 | 高 | 0.20 |
| 唤醒词检测 | 50 | 15 | 中 | 0.30 |
| BLE 通信 | 100 | 5 | 低 | 0.05 |
总利用率 U = 0.20 + 0.30 + 0.05 = 0.55。对于 3 个任务,RMS 的充分条件界限为 3(2^(1/3) - 1) ≈ 0.779。由于 0.55 < 0.779,该任务集在 RMS 下可调度。但需注意,这只是充分条件,即使 U 超过界限,任务集仍可能可调度,需要进一步做 RTA 分析。
案例拆解:假设在上述任务集中,唤醒词检测的 WCET 从 15ms 增加到 25ms,则总利用率变为 0.20 + 0.50 + 0.05 = 0.75,仍小于 0.779。但通过 RTA 计算,唤醒词检测的响应时间 R = 25 + 2×(⌈R/10⌉) = 25 + 2×3 = 31ms(迭代两次后收敛),小于其截止时间 50ms,因此仍然可调度。但如果 WCET 增加到 30ms,R = 30 + 2×4 = 38ms,依然可调度。这说明利用率测试偏保守,实际可调度性更好。
2.6 常见误区与设计复盘
误区一:所有任务都设为最高优先级。这会导致低优先级任务饿死,且调度器频繁切换,增加开销。正确做法是:只有对安全或用户体验至关重要的任务(如跌倒检测)才设为最高优先级。
误区二:忽略中断对任务时序的影响。中断服务程序(ISR)会抢占所有任务。如果 ISR 执行时间过长(如超过 100μs),会严重影响模型推理任务的截止时间。建议 ISR 只做标记事件,实际处理放在任务中。
误区三:使用阻塞式延时模拟周期。在裸机中常用的 HAL_Delay() 在 RTOS 中会阻塞整个任务,导致其他任务无法运行。必须使用 vTaskDelay() 或 vTaskDelayUntil() 实现精确周期。
误区四:忽视优先级反转。当两个模型任务共享一个 I2C 总线访问传感器时,若低优先级任务持有总线锁,高优先级任务会被阻塞。解决方法是使用互斥锁并启用优先级继承。
- ✅ 每个模型是否封装为独立任务?任务是否包含无限循环?
- ✅ 是否根据 RMS 或 EDF 原则分配了优先级?
- ✅ 是否测量了每个模型的最坏执行时间(WCET)?
- ✅ 是否计算了总 CPU 利用率?是否小于 70%?
- ✅ 是否验证了所有任务的响应时间小于截止时间?
- ✅ 是否处理了共享资源的互斥访问?
- ✅ 是否测量了实际调度延迟?