任务管理基础:任务的概念、创建与销毁、优先级

大家好,欢迎来到《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的调度策略和如何调优。到时候见。