第四章:任务创建与管理——让RTOS真正跑起来

好,我们进入第四章。前面几章我们聊了RTOS的基本概念、调度原理,还有中断管理。这一章,咱们来点实在的——怎么创建任务、怎么传参数、怎么让任务暂停或消失。

说实话,任务管理是RTOS最基础也最核心的操作。你想想看,整个系统就是由一个个任务组成的。任务创建不好,后面全是坑。我在项目里见过太多因为任务参数传错导致系统崩溃的案例了。

4.1 任务创建API:让代码活起来

先看最常用的API。以FreeRTOS为例,任务创建的核心函数就两个:xTaskCreatexTaskCreateStatic

我个人习惯用动态创建,也就是 xTaskCreate。为什么?省心。你不需要手动管理内存,RTOS帮你搞定。但要注意,动态创建依赖堆空间,堆不够就创建失败。

// 动态创建任务
BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数指针
    const char * const pcName,      // 任务名称(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字
    void *pvParameters,             // 传给任务的参数
    UBaseType_t uxPriority,         // 优先级
    TaskHandle_t *pxCreatedTask     // 任务句柄
);

嗯,这里要注意:栈深度单位是字,不是字节。32位处理器上一个字是4字节。我见过有人传1024进去,以为栈有1KB,结果实际只有4KB的一半不到。这种错误很难排查。

核心要点:

  • 任务函数必须是一个无限循环,不能返回
  • 任务名最长由 configMAX_TASK_NAME_LEN 决定,默认16字符
  • 优先级数值越大,优先级越高(FreeRTOS)
  • 句柄可以为NULL,如果你不需要操作这个任务

4.2 任务参数传递:别小看这个void指针

任务参数传递,说白了就是通过 pvParameters 这个void指针传东西进去。你可以传整数、结构体指针、字符串指针,什么都行。

但这里有个大坑——传指针时要保证指针指向的内存是有效的。我曾经在项目里犯过这个错:

// 错误示范
void create_task_bad(void) {
    int local_var = 100;
    xTaskCreate(task_func, "task", 128, &local_var, 1, NULL);
    // local_var 在函数返回后就失效了!
}

// 正确做法
static int global_var = 100;  // 全局变量或静态变量
void create_task_good(void) {
    xTaskCreate(task_func, "task", 128, &global_var, 1, NULL);
}

为什么会这样?因为任务创建后不会立即执行,它要等调度器分配CPU。等你函数返回了,局部变量早就被回收了。任务再去读那个地址,读到的就是垃圾数据。

传整数的话,有个小技巧:直接把整数值强制转换成void指针。比如:

// 传整数
xTaskCreate(task_func, "task", 128, (void *)100, 1, NULL);

// 任务里这样取
void task_func(void *pvParameters) {
    int param = (int)pvParameters;
}

这样最安全,因为值直接存在栈里,不涉及指针生命周期问题。

4.3 任务删除与挂起:让任务休息或消失

任务创建了,总得能删掉吧?RTOS提供了两个主要操作:删除和挂起。

操作 API 说明
删除任务 vTaskDelete(TaskHandle_t xTask) 彻底移除任务,释放资源
挂起任务 vTaskSuspend(TaskHandle_t xTask) 暂停任务,不释放资源
恢复任务 vTaskResume(TaskHandle_t xTask) 让挂起的任务继续运行

删除任务要注意:

  • 传入NULL表示删除自己(当前任务)
  • 删除后,任务占用的栈和TCB会被释放回堆
  • 如果任务持有互斥量、信号量等资源,删除前要释放

警告:千万不要在中断服务函数里调用 vTaskDelete 删除其他任务!这会导致不可预料的后果。我曾经在中断里删了一个正在等待信号量的任务,结果系统直接死机。正确的做法是在中断里发一个通知,让任务自己删除自己。

挂起和恢复的使用场景:

挂起比删除更温和。你想想看,有些任务只是暂时不需要运行,比如某个传感器采集任务,在低功耗模式下可以挂起,等需要时再恢复。这样避免了反复创建删除的开销。

// 挂起示例
TaskHandle_t sensor_task_handle = NULL;

void create_sensor_task(void) {
    xTaskCreate(sensor_task, "sensor", 256, NULL, 2, &sensor_task_handle);
}

void enter_low_power_mode(void) {
    vTaskSuspend(sensor_task_handle);  // 挂起传感器任务
}

void exit_low_power_mode(void) {
    vTaskResume(sensor_task_handle);   // 恢复传感器任务
}

个人经验:我建议在任务创建时就保存好句柄,方便后续管理。另外,挂起操作可以嵌套——挂起多次就需要恢复多次。这个特性在某些复杂场景下很有用,但也容易搞混,使用时要注意。

4.4 实战建议:任务创建的最佳实践

说了这么多,总结几条我踩坑踩出来的经验:

  1. 栈大小要留余量:别算得刚刚好。我一般多给20%-30%。任务里调用的函数、中断嵌套都会消耗栈空间。
  2. 优先级别乱设:优先级高的任务会抢占低优先级任务。如果两个任务优先级相同,就轮询调度。我见过有人把所有任务设成同一个优先级,结果实时性全没了。
  3. 任务名要有意义:调试时靠任务名区分任务。用 "uart_rx" 比 "task1" 好一万倍。
  4. 参数传递用全局变量或堆内存:别用局部变量,原因前面说过了。
  5. 删除任务前检查资源:确保任务没有持有锁、信号量等资源,否则会造成资源泄漏。

好了,这一章就到这里。任务创建和管理是RTOS的基础操作,看似简单,但细节很多。下一章我们聊任务间通信——信号量和队列,这才是RTOS真正强大的地方。