3、任务与调度的基本概念:任务、上下文、调度器、实时性、并发与并行

在单片机嵌入式系统中,任务与调度是系统软件架构的核心骨架。许多开发者从裸机编程转向RTOS(实时操作系统)时,最先遇到的认知障碍并非API用法,而是对“任务”“上下文”“调度器”这些基础概念的物理意义缺乏直观理解。本章将从CPU执行视角出发,厘清这些概念的本质,并重点辨析实时性、并发与并行在单核MCU上的真实含义,为后续学习任务状态迁移、调度算法和资源管理打下坚实基础。

任务调度核心概念关系图 CPU 核心 单核 MCU 上下文切换 保存/恢复寄存器+SP+PC 调度器 Scheduler 决定“下一个运行谁” 任务 A 优先级: 高 任务 B 优先级: 中 任务 C 优先级: 低 任务状态 就绪 运行 阻塞/挂起 实时性 截止时间 响应时间 确定性 并发:单核上时间片轮转交替执行 并行:多核同时执行(MCU少见)

上图展示了本章核心概念之间的逻辑关系:CPU核心通过上下文切换在多个任务之间跳转,调度器决定切换的时机与对象,而任务状态、实时性约束以及并发/并行的理解则构成了调度策略设计的边界条件。下面我们逐一深入剖析。

3.1 任务:从顺序执行到独立执行单元

核心原理

在裸机编程中,我们通常使用一个 while(1) 超级循环配合中断来完成所有功能。这种模型下,整个程序只有一个执行流,所有代码共享同一个栈空间。而任务(Task/Thread)则是对这一模型的根本性重构:每个任务拥有独立的栈空间、独立的程序计数器(PC)和寄存器集合,从逻辑上看,每个任务都像是“独占CPU”的独立程序。

任务的本质是一个无限循环函数,其典型结构如下:

void task_led(void *param) {
    while(1) {
        // 执行任务功能
        LED_TOGGLE();
        // 主动让出CPU或等待事件
        vTaskDelay(100);
    }
}

任务创建时,RTOS会为其分配任务控制块(TCB)和独立栈。TCB记录了任务名称、优先级、状态、栈指针等元数据。栈大小是任务设计的关键参数——栈溢出是嵌入式系统最常见的隐性故障之一。

判断方法

判断一个功能是否适合拆分为独立任务,可参考以下标准:

  • 时间独立性:该功能是否需要按自己的节奏周期性执行?
  • 事件驱动性:该功能是否等待外部事件(按键、通信帧、传感器中断)?
  • 阻塞容忍度:该功能是否包含可能长时间阻塞的操作(如等待I2C应答)?
  • 优先级差异:该功能是否对响应时间有不同于其他功能的要求?

如果以上任一答案为“是”,则适合拆分为独立任务;反之,若功能简单且与主循环耦合紧密,保留在超级循环中反而更高效。

案例拆解:多传感器数据采集

假设一个环境监测节点需要采集温湿度(I2C接口,每次读取需5ms)、气压(SPI接口,每次读取需2ms)和电池电压(ADC,每次采样需1ms)。裸机方案中,主循环依次读取三个传感器,总耗时8ms,期间无法响应按键或通信请求。拆分为三个独立任务后:

任务周期阻塞时间优先级
温湿度采集500ms5ms (I2C等待)中
气压采集1000ms2ms (SPI等待)中
电压采集200ms1ms (ADC)低

拆分后,当温湿度任务阻塞在I2C等待时,CPU可以切换到气压任务或电压任务执行,整体CPU利用率提升约30%,且按键响应延迟从8ms降至1ms以内。但代价是增加了约1.5KB的RAM开销(三个任务栈)和少量调度时间。

常见误区:任务数量并非越多越好。每个任务至少需要数百字节栈空间,且调度器切换需要时间(通常5-20μs)。在Cortex-M0等低端MCU上,超过10个任务就会明显增加RAM压力和调度开销。建议遵循“够用就好”原则,将功能相近的模块合并为一个任务。

3.2 上下文:CPU状态的完整快照

核心原理

上下文(Context)是任务在某一时刻CPU状态的完整集合,包括:通用寄存器(R0-R12)、程序计数器(PC)、栈指针(SP)、链接寄存器(LR)、程序状态寄存器(xPSR),以及浮点寄存器(如果使用FPU)。当调度器决定从任务A切换到任务B时,必须完整保存A的上下文到A的栈中,然后从B的栈中恢复B的上下文,这个过程称为上下文切换(Context Switch)。

在Cortex-M系列MCU上,硬件自动保存一部分上下文(xPSR、PC、LR、R0-R3、R12),其余寄存器由软件(PendSV异常处理)保存。这意味着一次完整的上下文切换大约需要40-80个CPU周期,具体取决于是否使用FPU。

操作流程

上下文切换的标准流程如下:

  1. 触发PendSV异常(软件设置PendSV挂起位);
  2. 硬件自动压栈部分寄存器到当前任务栈;
  3. 软件保存剩余寄存器(R4-R11)到当前任务栈;
  4. 更新当前任务TCB中的栈指针(SP);
  5. 从就绪列表中选取下一个任务;
  6. 从新任务TCB中恢复SP;
  7. 软件恢复R4-R11;
  8. 硬件自动出栈剩余寄存器,完成切换。

案例拆解:测量上下文切换开销

某工程师在STM32F103(Cortex-M3,72MHz)上运行FreeRTOS,需要评估上下文切换对系统实时性的影响。他使用GPIO翻转法:在任务A中置高某引脚,任务B中置低该引脚,用示波器测量脉冲宽度。

实测结果:脉冲宽度约3.2μs,即一次完整切换(A→B→A)耗时3.2μs,单次切换约1.6μs(约115个CPU周期)。这个数值在可接受范围内,但如果系统有10个任务且每个任务运行1ms,则每秒切换约1000次,总开销约1.6ms,占CPU时间0.16%。

经验提醒:上下文切换开销与任务数量无关,只与切换频率有关。降低切换频率的方法是让任务运行更长时间或使用事件驱动模型(任务阻塞等待事件而非轮询)。

3.3 调度器:系统的大脑

核心原理

调度器(Scheduler)是RTOS中最核心的组件,其职责是决定“在众多就绪任务中,哪一个应该获得CPU使用权”。调度策略直接决定了系统的实时性表现。常见的调度算法包括:

  • 时间片轮转(Round-Robin):每个任务分配固定时间片,到期后强制切换;
  • 优先级抢占(Priority-based Preemptive):高优先级任务就绪时立即抢占低优先级任务;
  • 单调速率调度(RMS):周期越短的任务优先级越高,适用于固定周期任务集;
  • 最早截止时间优先(EDF):截止时间越近的任务优先级越高,动态优先级算法。

大多数商用RTOS(FreeRTOS、RT-Thread、uC/OS-III)采用“优先级抢占+时间片轮转”的混合策略:不同优先级任务之间采用抢占式,同优先级任务之间采用时间片轮转。

判断方法

选择调度策略时,需评估任务集特征:

任务特征推荐策略原因
任务优先级差异明显优先级抢占确保关键任务及时响应
所有任务优先级相同时间片轮转公平分配CPU时间
任务周期固定且已知RMS可证明性强的静态调度
任务截止时间动态变化EDF理论上最优的动态调度

案例拆解:优先级反转问题

某系统有三个任务:高优先级任务H(每10ms执行一次,需5ms)、中优先级任务M(每20ms执行一次,需10ms)、低优先级任务L(每50ms执行一次,需15ms)。L持有共享资源(如I2C总线),H也需要该资源。

初始现象:H在t=0时尝试获取I2C总线,但L正占用该总线,H被阻塞。此时M就绪,抢占L执行。L被M打断,无法释放I2C总线,H只能等待M执行完毕。结果H的响应时间从预期的5ms恶化到15ms,超过其10ms的截止时间。

解决方案:采用优先级继承协议——当L持有资源时,临时将L的优先级提升到H的优先级,使L不被M抢占,尽快释放资源。修改后,H的响应时间恢复至约6ms。

风险提示:优先级继承只能缓解优先级反转,不能完全消除。更彻底的方法是使用互斥量(Mutex)配合优先级天花板协议,或在设计阶段避免高优先级任务与低优先级任务共享资源。

3.4 实时性:硬实时与软实时的边界

核心原理

实时性(Real-time)并非“速度快”,而是“确定性”——系统必须在规定时间内完成规定操作。实时性分为:

  • 硬实时(Hard Real-time):错过截止时间会导致灾难性后果(如安全气囊弹出、电机控制);
  • 软实时(Soft Real-time):偶尔错过截止时间可接受,但性能会下降(如音频播放、GUI刷新);
  • 固实时(Firm Real-time):错过截止时间后结果无意义,但不造成灾难(如视频帧处理)。

衡量实时性的关键指标包括:响应时间(从事件发生到任务开始执行的时间)、截止时间(任务必须完成的最晚时刻)、抖动(响应时间的波动范围)。

判断方法

评估系统实时性时,需计算最坏情况执行时间(WCET)和调度可行性:

  1. 列出所有任务及其周期、WCET、截止时间;
  2. 计算CPU利用率:U = Σ(WCET_i / Period_i);
  3. 对于RMS调度,若U ≤ n(2^(1/n)-1),则任务集可调度;
  4. 对于EDF调度,若U ≤ 1,则任务集可调度。

案例拆解:电机控制系统的实时性验证

某BLDC电机控制器使用STM32G4,PWM频率20kHz(周期50μs),电流环任务需在PWM周期内完成采样和控制计算。任务参数:电流环周期50μs,WCET 20μs;速度环周期1ms,WCET 100μs;通信任务周期10ms,WCET 2ms。

CPU利用率计算:U = 20/50 + 100/1000 + 2000/10000 = 0.4 + 0.1 + 0.2 = 0.7。对于3个任务,RMS可调度条件为U ≤ 3(2^(1/3)-1) ≈ 0.78,0.7 < 0.78,理论可调度。但实际测试发现,当通信任务与电流环任务同时就绪时,电流环响应时间抖动达到8μs,接近其截止时间裕量。

优化措施:将通信任务优先级降至最低,并启用中断嵌套(电流环使用最高优先级中断而非任务),最终电流环抖动降至2μs以内。

总结复盘:实时性设计的关键在于“留有余量”。WCET分析往往偏乐观,实际执行时间可能因缓存命中率、总线仲裁等因素而恶化。建议将CPU利用率控制在70%以下,并为关键任务预留至少30%的截止时间裕量。

3.5 并发与并行:单核MCU的并发真相

核心原理

并发(Concurrency)是指多个任务在宏观上同时推进,但在微观上,单核CPU任一时刻只能执行一个任务。并发通过快速的任务切换(时间片轮转或优先级抢占)实现,切换速度足够快时,用户感觉多个任务在“同时”运行。

并行(Parallelism)是指多个任务真正同时执行,这要求多核CPU或多个CPU。在单片机领域,绝大多数场景是单核,因此只有并发,没有并行。即使使用多核MCU(如ESP32的双核),也需注意:两个核共享外设和内存,并行执行时可能产生资源竞争。

理解并发与并行的区别对任务设计至关重要:并发环境下,任务之间的共享数据必须使用临界区或互斥量保护;并行环境下,还需考虑多核缓存一致性。

判断方法

判断一个系统是并发还是并行,只需看CPU核心数:

  • 单核MCU(STM32、AVR、PIC等):纯并发;
  • 多核MCU(ESP32、i.MX RT1170等):并发+有限并行;
  • MCU+DSP/协处理器:异构并行,但DSP通常执行固定算法。

案例拆解:并发环境下的数据一致性

某数据采集系统使用单核MCU,任务A采集ADC数据并写入缓冲区,任务B通过UART发送缓冲区数据。初始代码中,任务A写入缓冲区后立即更新“数据就绪”标志,任务B检查标志后读取缓冲区。

初始现象:偶发数据错乱,UART发送的数据帧中偶尔出现半个旧数据+半个新数据的混合帧。

分析过程:任务A在写入缓冲区过程中被任务B抢占(任务B优先级更高),任务B读取到未写完的缓冲区。虽然标志位在写入完成后才更新,但任务B可能在任务A写入过程中被调度执行。

解决方案:使用临界区保护缓冲区写入和读取操作:

// 任务A
taskENTER_CRITICAL();
memcpy(buffer, adc_data, sizeof(adc_data));
data_ready = 1;
taskEXIT_CRITICAL();

// 任务B
taskENTER_CRITICAL();
if (data_ready) {
    memcpy(tx_data, buffer, sizeof(buffer));
    data_ready = 0;
}
taskEXIT_CRITICAL();

修改后,数据错乱问题彻底消失。代价是临界区期间中断被屏蔽,最长屏蔽时间约20μs,对系统实时性影响可忽略。

经验提醒:在单核MCU上,并发问题的本质是“任务切换时机不可预测”。任何跨任务访问的共享变量都应视为临界资源,使用临界区、互斥量或信号量保护。不要依赖“任务A执行很快,不会被抢占”这种假设——调度器随时可能切换任务。

3.6 本章总结与常见误区复盘

任务、上下文、调度器、实时性、并发与并行构成了RTOS设计的六大基石。本章核心要点归纳如下:

概念本质设计要点常见误区
任务独立执行流+独立栈栈大小估算、任务划分粒度任务越多越好
上下文CPU状态快照切换开销评估、FPU上下文忽略切换时间
调度器CPU分配决策者优先级分配、调度算法选择所有任务同优先级
实时性确定性而非速度WCET分析、可调度性验证把“快”当“实时”
并发/并行单核交替/多核同时临界区保护、资源共享混淆并发与并行

在实际项目中,建议从最小可行任务集开始,逐步增加任务数量;每次添加任务时,重新评估CPU利用率和内存占用;使用RTOS自带的统计功能(如FreeRTOS的 vTaskGetRunTimeStats())定期检查各任务CPU使用率,及时发现异常。记住:调度器只是工具,良好的任务设计才是系统稳定性的根本保障。