3. NuttX任务管理:任务创建与删除、优先级与调度策略、状态切换

各位同学,咱们今天聊聊NuttX的任务管理。说实话,任务管理是实时操作系统的灵魂。你想想看,一个汽车ECU里同时跑着CAN通信、传感器采集、控制算法、故障诊断……这些活儿怎么安排?谁先干谁后干?干到一半被打断了怎么办?全靠任务管理来搞定。

我个人习惯,拿到一个新项目,第一件事就是理清楚任务划分。任务分得好,后面开发顺风顺水;任务分得乱,调试起来能让你怀疑人生。好,咱们直接进入正题。

3.1 任务创建与删除:task_create 和 task_delete

在NuttX里,创建一个任务非常简单。核心API就两个:task_createtask_delete。我刚开始用NuttX时,觉得这跟Linux的pthread差不多,但实际用起来还是有不少坑。

3.1.1 task_create 函数详解

先看原型:

#include <nuttx/sched.h>

int task_create(const char *name, int priority, int stack_size,
                main_t entry, FAR char * const argv[]);

参数说明:

参数 含义 我踩过的坑
name 任务名称,调试用 别超过CONFIG_TASK_NAME_SIZE,否则截断
priority 任务优先级,0~255 数字越大优先级越高,别搞反了
stack_size 任务栈大小,单位字节 这个最坑,后面细说
entry 任务入口函数 必须是 void main(int argc, char *argv[]) 形式
argv 命令行参数 可以传NULL,但传参数时注意生命周期

返回值:成功返回任务ID(正数),失败返回负数错误码。

来个实际例子:

#include <nuttx/config.h>
#include <nuttx/sched.h>
#include <stdio.h>

int can_monitor_main(int argc, char *argv[])
{
    /* CAN总线监控任务 */
    printf("CAN Monitor started, priority: %d\n", atoi(argv[1]));
    while(1) {
        /* 实际项目中这里会读取CAN消息 */
        usleep(10000);  /* 10ms轮询一次 */
    }
    return 0;
}

void app_start(void)
{
    int pid;
    char *args[] = {"50", NULL};  /* 传递优先级参数 */

    pid = task_create("can_monitor", 120, 2048, can_monitor_main, args);
    if (pid < 0) {
        printf("Failed to create task! errno=%d\n", errno);
    }
}
⚠️ 栈大小陷阱

我曾经在一个项目里,给一个任务分配了512字节的栈。结果任务里调用了printf,直接栈溢出,系统随机崩溃。查了三天才找到原因。记住:printf这类函数会消耗大量栈空间。保守起见,至少给1024字节。如果任务里有局部大数组,栈大小要相应增加。

3.1.2 task_delete 的正确姿势

删除任务用 task_delete

int task_delete(int pid);

嗯,这里要注意。task_delete不是你想删就能删的。它有几个限制:

  • 不能删除自己:任务不能自杀。想退出?用 pthread_exit 或者直接 return。
  • 不能删除空闲任务:ID为0的任务是系统空闲任务,删了系统就崩了。
  • 资源泄漏风险:被删除的任务如果持有互斥锁、信号量,这些资源不会被自动释放。
💡 我的建议

实际项目中,我很少用task_delete。更安全的做法是:让任务自己退出。比如设置一个全局标志位,任务检测到标志位后自行return。这样资源清理更干净。

3.2 任务优先级与调度策略:FIFO vs RR

NuttX支持两种调度策略:SCHED_FIFO 和 SCHED_RR。说白了,就是「插队模式」和「轮班模式」的区别。

3.2.1 优先级设定

优先级范围是0~255,255最高。我习惯这样分配:

优先级范围 适用场景 举例
200~255 硬实时任务 安全气囊触发、刹车控制
100~199 软实时任务 CAN通信、传感器采集
50~99 普通任务 诊断服务、日志记录
0~49 后台任务 自检、统计

你想想看,如果刹车控制和日志记录优先级一样高,刹车信号来了日志还在写,那后果……嗯,你懂的。

3.2.2 FIFO调度(先来先服务)

FIFO模式下,高优先级任务一旦就绪,立即抢占低优先级任务。同优先级任务之间,谁先就绪谁先运行,直到任务主动让出CPU(比如sleep、等待事件)。

举个例子:

/* 任务A:优先级120,FIFO */
void task_a_main(int argc, char *argv[])
{
    while(1) {
        /* 处理紧急事件 */
        usleep(1000);  /* 主动让出CPU */
    }
}

/* 任务B:优先级120,FIFO */
void task_b_main(int argc, char *argv[])
{
    while(1) {
        /* 处理普通事件 */
        usleep(1000);
    }
}

这里有个问题:如果任务A没有sleep,它会一直占用CPU,任务B永远得不到执行。这就是FIFO的「饥饿」问题。

🔑 关键点

FIFO适合「事件驱动型」任务。任务处理完事件就挂起,不会长时间占用CPU。我在做CAN网关项目时,所有通信任务都用FIFO,因为CAN消息来了才处理,处理完就休眠,非常高效。

3.2.3 RR调度(时间片轮转)

RR调度解决了FIFO的饥饿问题。同优先级任务之间,每个任务运行一个时间片(通常10~100ms),时间到了就切换到下一个。

配置RR调度:

#include <nuttx/sched.h>

struct sched_param param;
param.sched_priority = 100;
param.sched_priority = 100;  /* 优先级 */
param.sched_timeslice = 20;  /* 时间片20ms */

/* 设置调度策略 */
sched_setscheduler(pid, SCHED_RR, &param);

什么时候用RR?我个人的经验是:

  • 多个同优先级的「计算密集型」任务,用RR
  • 需要公平分配CPU的场景,用RR
  • 实时性要求不高的后台任务,用RR
⚠️ 时间片别设太小

我曾经把时间片设为5ms,结果任务切换太频繁,CPU大部分时间花在上下文切换上,有效计算时间反而少了。一般10~50ms比较合理。

3.3 任务状态切换:运行、就绪、阻塞、挂起

NuttX的任务有四种基本状态。我画个图你感受一下:

        创建
          ↓
    ┌──────────┐
    │  就绪态   │ ←──────────┐
    └────┬─────┘            │
         │ 调度器选择        │
         ↓                   │
    ┌──────────┐   事件发生  │
    │  运行态   │ ──────────┘
    └────┬─────┘
         │ 等待事件
         ↓
    ┌──────────┐
    │  阻塞态   │
    └──────────┘

四种状态说明:

状态 含义 怎么进入 怎么退出
就绪态 任务已准备好,等待CPU 创建完成、被唤醒 调度器选中后进入运行态
运行态 正在使用CPU 调度器分配CPU 时间片用完、主动让出、被抢占
阻塞态 等待某个事件 调用sleep、等待信号量、等待队列 事件发生、超时
挂起态 被暂停执行 调用task_suspend 调用task_resume

这里有个容易混淆的点:阻塞和挂起的区别。阻塞是任务「主动」等待资源,比如等CAN消息;挂起是「被动」被其他任务暂停,比如调试时暂停某个任务。

💡 调试小技巧

ps 命令可以查看所有任务的状态:

nsh> ps
PID PRI POLICY   STACK   USED  FILLED COMMAND
  0   0 FIFO    2048    1024   50.0% Idle Task
  1 100 RR      2048     512   25.0% can_monitor
  2 120 FIFO    2048     768   37.5% brake_control

看USED列,如果某个任务栈使用率超过80%,就要警惕栈溢出了。

3.4 实战经验总结

好,最后我总结几条实战经验,都是真金白银换来的:

  1. 优先级不要超过20个等级。我见过有人分了50个优先级,结果调度器光比较优先级就花了不少时间。汽车电子里,5~10个优先级足够了。
  2. 高优先级任务要「快进快出」。别在中断上下文或高优先级任务里做复杂计算。我曾经在刹车控制任务里做了个浮点运算,结果导致CAN消息丢失,差点出事。
  3. 用信号量代替轮询。别让任务while(1)里不断检查标志位,那是浪费CPU。用信号量或消息队列,让任务在等待时进入阻塞态,省电又高效。
  4. 栈大小要留余量。我一般给任务分配的理论栈大小的1.5倍。比如计算需要1KB,我就给1.5KB。多出来的空间是救命用的。

嗯,任务管理这块内容不少,但核心就三点:创建删除要小心、优先级分配要合理、状态切换要理解。下一章咱们聊NuttX的任务间通信,信号量和消息队列,那才是真正体现功力的地方。

记住:好的任务设计,能让你的系统跑得又快又稳;乱搞任务,调试能让你怀疑人生。各位加油!