第四章:任务创建与管理——让RTOS真正跑起来
好,我们进入第四章。前面几章我们聊了RTOS的基本概念、调度原理,还有中断管理。这一章,咱们来点实在的——怎么创建任务、怎么传参数、怎么让任务暂停或消失。
说实话,任务管理是RTOS最基础也最核心的操作。你想想看,整个系统就是由一个个任务组成的。任务创建不好,后面全是坑。我在项目里见过太多因为任务参数传错导致系统崩溃的案例了。
4.1 任务创建API:让代码活起来
先看最常用的API。以FreeRTOS为例,任务创建的核心函数就两个:xTaskCreate 和 xTaskCreateStatic。
我个人习惯用动态创建,也就是 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 实战建议:任务创建的最佳实践
说了这么多,总结几条我踩坑踩出来的经验:
- 栈大小要留余量:别算得刚刚好。我一般多给20%-30%。任务里调用的函数、中断嵌套都会消耗栈空间。
- 优先级别乱设:优先级高的任务会抢占低优先级任务。如果两个任务优先级相同,就轮询调度。我见过有人把所有任务设成同一个优先级,结果实时性全没了。
- 任务名要有意义:调试时靠任务名区分任务。用 "uart_rx" 比 "task1" 好一万倍。
- 参数传递用全局变量或堆内存:别用局部变量,原因前面说过了。
- 删除任务前检查资源:确保任务没有持有锁、信号量等资源,否则会造成资源泄漏。
好了,这一章就到这里。任务创建和管理是RTOS的基础操作,看似简单,但细节很多。下一章我们聊任务间通信——信号量和队列,这才是RTOS真正强大的地方。