第3章:NuttX任务管理:任务创建与删除、任务优先级、任务调度、任务状态

好,咱们今天聊聊NuttX的任务管理。说实话,任务管理是RTOS的魂。你想想看,一个嵌入式系统里跑着好几个活儿,有的要实时响应,有的可以慢慢来,怎么管?全靠任务管理这一套机制。

我个人习惯,拿到一个新板子,第一件事就是先跑几个任务试试水。不是为了炫技,而是为了摸清楚这个系统的调度脾气。NuttX的任务管理,说白了就是一套「谁先跑、谁后跑、谁让路」的规则。

3.1 任务创建与删除

先说说怎么创建一个任务。NuttX里最常用的API就是task_create()。我刚开始用的时候,总觉得参数太多记不住,后来发现其实就那几样:任务名、优先级、栈大小、入口函数、参数。

#include <nuttx/config.h>
#include <sched.h>
#include <sys/types.h>

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

这里有个坑,我踩过好几次。栈大小别给太小,也别太大。太小了任务跑着跑着就栈溢出,系统直接崩了。太大了浪费内存,尤其是一些RAM紧张的MCU。我一般给1024或2048字节起步,调试阶段可以给大一点,稳定后再优化。

我的小技巧: 调试阶段把栈大小设成4096,跑稳定了再慢慢往下压。我曾经因为栈给少了,一个递归函数直接让系统死机,查了两天才找到原因。

删除任务用task_delete()。这个函数我用的不多,因为大多数任务都是无限循环的。但有些场景,比如临时启动一个传感器采集任务,采完就删掉,这时候就派上用场了。

int task_delete(pid_t pid);

嗯,这里要注意:删除任务时,如果这个任务正拿着某个信号量或者锁,直接删掉会导致资源泄漏。我建议你在删除前,确保任务已经释放了所有资源。

3.2 任务优先级

优先级这东西,说白了就是给任务排个队。NuttX支持256级优先级(0到255),0是最低,255是最高。不过实际用的时候,我很少用到这么多级,一般分个3到5级就够了。

优先级范围 典型用途 我的建议
200-255 中断处理、高实时任务 慎用,别把所有任务都设成高优先级
100-199 普通实时任务 大部分控制任务放这里
1-99 后台任务、低优先级处理 日志、统计、非关键任务
0 空闲任务 系统保留,别动它

你想想看,如果两个任务优先级一样,NuttX怎么调度?它会用时间片轮转,每个任务跑一个时间片,然后切换。但如果优先级不同,高优先级的任务会一直跑,直到它主动让出CPU或者被更高优先级的任务抢占。

优先级反转: 这是一个经典问题。低优先级任务拿着一个资源,高优先级任务等着用,结果中优先级任务一直跑,把低优先级任务堵死了。我曾经在一个电机控制项目里遇到过,电机转速忽快忽慢,查了半天才发现是优先级反转。解决方案?用优先级继承或者优先级天花板协议。

3.3 任务调度

NuttX的调度器,默认是抢占式调度。什么意思?就是高优先级的任务随时可以打断低优先级的任务。这听起来很合理,但实际用起来要注意。

我记得有一次,我写了一个高优先级的按键扫描任务,优先级设得特别高。结果这个任务一跑,其他所有任务都卡住了。为什么?因为它每次扫描完,马上又进入就绪状态,调度器一看,哎,高优先级任务能跑了,又切回来。这就形成了「任务饿死」。

解决办法?要么在任务里加个sleep(),要么用信号量让任务等一等。说白了,高优先级任务也要懂得「让一让」。

// 错误示范:高优先级任务疯狂循环
void high_prio_task(int argc, char *argv[])
{
  while (1) {
    scan_key();  // 扫描按键
    // 没有延时,直接继续循环
  }
}

// 正确示范:加个延时,给其他任务机会
void high_prio_task(int argc, char *argv[])
{
  while (1) {
    scan_key();
    usleep(10000);  // 10ms延时,让出CPU
  }
}

NuttX还支持同优先级任务的时间片轮转。每个任务默认跑100ms(这个值可以配置),时间到了就换下一个。我个人习惯把时间片设成50ms,感觉切换更平滑一些。

3.4 任务状态

任务在NuttX里有几种状态?我数数看:就绪、运行、阻塞、挂起、终止。其实跟其他RTOS差不多,但每个状态怎么切换,这里头有门道。

  • 就绪态: 任务准备好了,就等调度器叫它。所有能跑的任务都在就绪队列里排队。
  • 运行态: 任务正在使用CPU。同一时刻只有一个任务在运行。
  • 阻塞态: 任务在等某个事件,比如信号量、消息队列、延时。这时候它不消耗CPU。
  • 挂起态: 被其他任务或者自己主动挂起。跟阻塞有点像,但挂起是人为控制的。
  • 终止态: 任务跑完了或者被删除了。这时候任务占用的资源会被回收。

你想想看,一个任务从创建到删除,状态是怎么流转的?创建后进入就绪态,调度器选中它后变成运行态,如果它调用sleep()或者等信号量,就进入阻塞态。阻塞结束又回到就绪态。如果被挂起,就进入挂起态,直到被恢复。

核心要点: 任务状态切换是RTOS调度的基础。你写代码时,心里要清楚每个任务当前在什么状态。我曾经调试一个系统,发现CPU占用率忽高忽低,用ps命令一看,有个任务一直在就绪和运行之间疯狂切换,原来是优先级设太高了。

NuttX提供了一个很实用的命令——ps,可以在控制台查看所有任务的状态。我调试时几乎离不开它。你可以看到每个任务的PID、优先级、状态、栈使用情况。

nsh> ps
  PID PRI POLICY   TYPE    NPX STATE    NAME
    0   0 FIFO     Kthread   - Ready    Idle Task
    1 100 RR       Task      - Waiting  nsh_main
    2 150 RR       Task      - Running  sensor_task
    3 120 RR       Task      - Waiting  led_task

看到没?sensor_task正在运行,led_task在等待。如果led_task一直不跑,我就能猜到它可能在等某个信号量或者消息队列。

好了,任务管理这块就聊到这儿。下一章咱们深入消息队列,看看任务之间怎么高效地传数据。记住一句话:任务管理是RTOS的骨架,消息队列和信号量是血肉。骨架搭不好,后面再好的代码也跑不起来。