任务管理基础:任务的概念、创建与销毁、优先级
大家好,欢迎来到《NuttX POSIX接口兼容性实战》的第四讲。
今天我们来聊聊任务管理。这是RTOS里最基础、也最核心的一块。说白了,你写嵌入式程序,就是在跟任务打交道。任务管理得好,系统就稳;管不好,各种奇怪的问题就来了。
我个人习惯把任务理解成“一个独立运行的执行流”。每个任务都有自己的栈空间、优先级和状态。NuttX的任务管理接口,基本遵循POSIX标准,所以如果你之前接触过Linux或其他POSIX系统,上手会很快。
任务(Task)到底是什么?
任务,在NuttX里就是一个独立的线程。它有自己的程序计数器、寄存器和栈。多个任务之间通过调度器来切换执行。
你想想看,一个单片机只有一个CPU核心,但我们可以同时跑多个任务。这是怎么做到的?其实就是调度器在背后快速切换,让每个任务轮流用CPU。切换得够快,看起来就像“同时”在跑。
嗯,这里要注意:任务和进程在NuttX里其实没有严格区分。NuttX是一个扁平地址空间的RTOS,所有任务共享同一个地址空间。所以任务切换的开销很小,比Linux的进程切换快得多。
核心概念:在NuttX中,任务 = 线程 = 执行流。它们共享地址空间,通过调度器分时复用CPU。
任务的创建:task_create
创建任务,最常用的接口就是 task_create。它的原型长这样:
#include <nuttx/config.h>
#include <sched.h>
int task_create(const char *name, int priority,
int stack_size, main_t entry,
FAR char * const argv[]);
参数说明:
- name:任务的名字,调试时很有用。我习惯用“task_模块名”这种命名方式。
- priority:任务优先级,数值越大优先级越高。NuttX默认支持0-255的优先级范围。
- stack_size:栈大小,单位是字节。这个要小心,给太小会栈溢出,给太大浪费内存。
- entry:任务入口函数,就是任务开始执行的地方。
- argv:传递给入口函数的参数数组。
返回值:成功返回任务ID(一个正整数),失败返回负数错误码。
举个例子:
#include <nuttx/config.h>
#include <sched.h>
#include <stdio.h>
static int my_task_entry(int argc, char *argv[])
{
printf("Hello from my_task! arg1 = %s\n", argv[1]);
return 0;
}
void create_example(void)
{
FAR char *argv[] = {"my_task", "hello_arg", NULL};
int pid;
pid = task_create("my_task", 100, 2048, my_task_entry, argv);
if (pid < 0) {
printf("task_create failed: %d\n", errno);
} else {
printf("task created, pid = %d\n", pid);
}
}
我的经验:栈大小怎么定?我一般先给一个保守值(比如2048字节),然后用task_stackinfo或者调试器观察实际使用量,再调整。曾经有个项目,我给了1024字节,结果任务一跑就崩,查了半天才发现是栈溢出了。
任务的销毁:task_delete
任务跑完了,或者你想中途干掉它,可以用 task_delete。
#include <sched.h>
int task_delete(int pid);
参数就是任务ID。成功返回0,失败返回负数。
但我要提醒你:task_delete 是个危险操作。为什么?
- 如果被删除的任务持有互斥锁、信号量等资源,这些资源不会被自动释放,可能导致死锁。
- 如果任务正在执行关键操作(比如写文件),突然被删除,数据可能损坏。
我曾经踩过的坑:有个项目里,我直接用task_delete干掉一个正在操作UART发送的任务。结果UART的发送缓冲区没清干净,后续所有串口通信都乱了。从那以后,我改用事件标志或消息队列来通知任务“你该退出了”,让任务自己优雅地return。
所以,更安全的做法是:让任务自己退出。任务入口函数return后,NuttX会自动清理任务占用的资源。
static int safe_task_entry(int argc, char *argv[])
{
while (1) {
/* 检查退出标志 */
if (g_exit_flag) {
break; /* 优雅退出 */
}
/* 做正事 */
usleep(10000);
}
return 0; /* 自动清理 */
}
任务优先级
优先级是调度器决定“下一个该谁跑”的关键依据。NuttX默认使用基于优先级的抢占式调度。
规则很简单:
- 数值越大,优先级越高。
- 高优先级的任务就绪时,会立即抢占低优先级任务。
- 相同优先级的任务,采用时间片轮转。
优先级范围可以通过配置修改,默认是0-255。0是最低优先级,255是最高。
| 优先级值 | 典型用途 |
|---|---|
| 0-50 | 后台任务、空闲任务 |
| 51-150 | 普通应用任务 |
| 151-200 | 实时性要求较高的任务 |
| 201-255 | 中断底半处理、关键控制任务 |
设置优先级用 task_setpriority:
#include <sched.h>
int task_setpriority(int pid, int priority);
或者创建时直接在 task_create 里指定。
避坑指南:优先级不要设得太高。我曾经把一个传感器采集任务的优先级设成250,结果它一直占着CPU,其他任务(包括通信任务)都饿死了。系统看起来还在跑,但数据就是发不出去。后来我把优先级降到120,配合适当的休眠,问题就解决了。
还有个常见问题:优先级反转。低优先级任务持有高优先级任务需要的资源,导致高优先级任务被阻塞。NuttX支持优先级继承协议,可以在配置中开启。这个我们后面章节会细讲。
小结
今天的内容就这些。总结一下:
- 任务就是独立的执行流,NuttX里任务和线程是一回事。
- 用
task_create创建任务,注意栈大小要合理。 - 尽量别用
task_delete,让任务自己退出更安全。 - 优先级是调度核心,设太高会饿死其他任务,设太低可能响应不及时。
下一讲,我们会深入任务调度机制,聊聊NuttX的调度策略和如何调优。到时候见。