第三章:任务管理——任务创建、删除、挂起与恢复,任务优先级设计,任务状态转换

各位好,我是老张。在嵌入式系统里摸爬滚打了十几年,RTOS这块算是我的老本行。今天咱们聊聊任务管理,这是RTOS的核心中的核心。说白了,你写的代码最终都要变成一个个任务,交给系统去调度。任务管理搞不明白,后面写再多代码也是白搭。

3.1 任务创建与删除

任务创建,就是告诉RTOS:“嘿,我这有一段代码,你帮我跑起来。” 不同的RTOS,API名字不一样。FreeRTOS里叫 xTaskCreate,uC/OS里叫 OSTaskCreate。但核心参数都差不多:任务函数指针、任务名字、栈大小、参数、优先级、任务句柄。

我个人习惯,创建任务时栈大小一定要留够余量。我在一个血压计项目里吃过亏,当时为了省RAM,栈大小算得死死的。结果产品在低温环境下偶尔死机,查了三天,最后发现是栈溢出。从那以后,我每个任务至少多给20%的栈空间。

重要提醒:任务栈大小不是算出来的,是测出来的。用 uxTaskGetStackHighWaterMark() 这类API去监控实际使用量,再决定最终值。

任务删除相对简单。调用 vTaskDelete(NULL) 可以删除自己,传任务句柄可以删除其他任务。但要注意,删除任务时,它占用的堆内存会被回收,但栈空间不会自动释放——嗯,这里要小心,别搞出内存泄漏。

// 创建任务示例(FreeRTOS)
TaskHandle_t xTaskHandle = NULL;

void vTaskCode(void *pvParameters) {
    while(1) {
        // 血压测量逻辑
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void AppInit(void) {
    xTaskCreate(
        vTaskCode,          // 任务函数
        "BP_Measure",       // 任务名字
        256,                // 栈大小(单位:字)
        NULL,               // 参数
        3,                  // 优先级
        &xTaskHandle        // 任务句柄
    );
}

3.2 任务挂起与恢复

挂起和恢复,是任务管理的“暂停键”和“继续键”。挂起一个任务,它就不再参与调度,直到被恢复。这在血压计里很常用——比如用户按下“开始测量”按钮,你挂起所有非关键任务,只保留测量和显示任务,保证实时性。

我记得有一次,客户反馈血压计在测量过程中偶尔会“卡住”。我排查后发现,是某个低优先级任务在测量期间被唤醒,抢占了CPU时间。解决方案很简单:测量开始时挂起那个任务,测量结束后恢复它。

小技巧:挂起任务时,最好用 vTaskSuspendAll() 挂起所有任务,而不是一个个挂。但要注意,这个API不能在中断服务函数里调用。

// 挂起与恢复示例
void StartMeasurement(void) {
    // 挂起非关键任务
    vTaskSuspend(xNonCriticalTaskHandle);
    
    // 开始血压测量
    StartBPMeasurement();
}

void EndMeasurement(void) {
    // 恢复非关键任务
    vTaskResume(xNonCriticalTaskHandle);
    
    // 显示测量结果
    DisplayResult();
}

3.3 任务优先级设计

优先级设计,是RTOS应用里最考验功力的地方。设计得好,系统稳如泰山;设计得不好,各种诡异问题层出不穷。

我一般遵循几个原则:

  • 实时性要求高的任务,优先级高。比如血压测量中的AD采样任务,必须高优先级。
  • 计算密集型任务,优先级低。比如数据处理、滤波算法,可以慢慢算。
  • 避免优先级反转。这个坑我踩过。曾经有个项目,低优先级任务占着共享资源不放,高优先级任务一直等,系统性能直接崩了。后来用优先级继承才解决。
任务类型 优先级 说明
AD采样 最高(5) 实时性要求最高,不能丢数据
按键扫描 高(4) 用户交互,响应要快
显示刷新 中(3) 周期性刷新,可容忍少量延迟
数据处理 低(2) 计算量大,优先级低
空闲任务 最低(0) 系统空闲时运行

警告:不要把所有任务都设成同一个优先级。那样RTOS就退化成轮询了,失去了使用RTOS的意义。我曾经见过一个团队,所有任务优先级都是3,结果系统响应时间完全不可控。

3.4 任务状态转换

任务在RTOS里有几种状态:运行态、就绪态、阻塞态、挂起态。你想想看,一个任务从创建到销毁,就在这几个状态之间跳来跳去。

  • 运行态:任务正在使用CPU。单核系统里,同一时刻只有一个任务在运行。
  • 就绪态:任务准备好了,但CPU被别的任务占着。它在等调度器给它机会。
  • 阻塞态:任务在等某个事件,比如延时到期、信号量可用、队列有数据。这时候它不消耗CPU。
  • 挂起态:vTaskSuspend() 挂起,除非被恢复,否则永远不参与调度。

为什么会这样设计?说白了,就是为了高效利用CPU。任务在等I/O的时候,CPU别闲着,赶紧去干别的活。我在一个多任务血压计项目里,用状态转换图给新同事讲解,他们一下子就明白了。

核心要点:任务状态转换是RTOS调度的基础。理解了这个,你就能预测系统在不同场景下的行为。比如,高优先级任务进入阻塞态,调度器会立刻切换到下一个就绪态的高优先级任务。

嗯,任务管理这块内容不少,但都是基本功。我建议你在实际项目里多练练,特别是优先级设计和状态转换,光看书是学不会的。下一章咱们聊聊任务间通信,那又是另一片天地了。