4. NuttX任务管理:任务创建与删除、任务优先级与调度策略、任务状态转换、任务间通信

大家好,我是你们的嵌入式系统讲师。今天我们来聊聊NuttX的任务管理。说实话,任务管理是RTOS的基石,你玩不转它,后面写网络协议栈代码就会处处碰壁。我个人习惯把任务管理比作一个公司的部门运作——每个任务就是一个员工,调度器就是CEO,信号量这些就是部门间的沟通工具。好,我们开始。

4.1 任务创建与删除

在NuttX里,创建任务最常用的API是task_create()。它的原型长这样:

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

参数不多,但每个都很关键。name是任务名,调试时有用。priority是优先级,数值越大优先级越高。stack_size是栈大小,这个我吃过亏——给太小了任务跑飞,给太大了浪费内存。entry是任务入口函数,argv是参数列表。

举个例子:

int my_task_pid;
my_task_pid = task_create("my_task", 100, 2048, my_task_entry, NULL);
if (my_task_pid < 0) {
    printf("任务创建失败,错误码: %d\n", errno);
}

删除任务用task_delete()。但我要提醒你——千万不要在中断服务函数里调用task_delete()。我曾经在项目里这么干过,结果系统直接死锁。为什么?因为中断上下文里没有任务调度环境,你删了一个任务,调度器就懵了。

警告: 不要在中断服务函数中调用 task_delete()。如果确实需要在中断中终止任务,请使用信号量或事件标志通知任务自行退出。

4.2 任务优先级与调度策略

NuttX支持两种调度策略:SCHED_FIFOSCHED_RR。说白了,就是两种排队方式。

SCHED_FIFO:先来先服务。一个任务只要不主动让出CPU,或者不被更高优先级的任务抢占,它就一直跑。适合那些计算量大、不需要频繁切换的任务。

SCHED_RR:时间片轮转。每个任务跑一个固定时间片,时间到了就换下一个同优先级的任务。适合多个同等重要的任务轮流执行。

设置调度策略的代码:

struct sched_param param;
param.sched_priority = 100;
sched_setscheduler(0, SCHED_FIFO, &param);

这里0表示当前任务。你想想看,如果你的网络协议栈里有一个数据接收任务和一个数据发送任务,你会怎么分配优先级?我个人建议接收任务优先级高一点,因为数据来了你不及时收,缓冲区可能就溢出了。

经验之谈: 在STM32上跑NuttX,我一般把中断处理任务的优先级设为最高,网络协议栈主任务次之,日志打印任务最低。这样既能保证实时性,又不会因为打印日志耽误正事。

4.3 任务状态转换

每个任务在生命周期里会经历几个状态:就绪、运行、阻塞、挂起、终止。我画了一张图,帮你理清它们之间的关系。

就绪 运行 阻塞 挂起 终止 调度 时间片用完/被抢占 等待资源 资源就绪 task_suspend() task_resume() task_delete() NuttX任务状态转换图

这张图你看懂了吗?任务从就绪运行靠调度器决定。运行中如果等信号量或消息队列,就进入阻塞。阻塞结束后回到就绪队列。你也可以手动挂起任务,让它暂时不参与调度。

嗯,这里要注意:阻塞和挂起是两码事。阻塞是任务自己主动等资源,挂起是别的任务或中断强制让它休息。我在调试一个网络协议栈时,曾经把一个接收任务挂起了,结果数据来了没人收,整个协议栈就卡死了。后来我改用信号量等待,问题就解决了。

4.4 任务间通信

任务间通信是RTOS的灵魂。NuttX提供了三种主要方式:信号量、消息队列、事件组。我一个个说。

4.4.1 信号量

信号量说白了就是一个计数器。任务A释放信号量,任务B获取信号量。如果信号量计数为0,获取的任务就阻塞。

sem_t sem;
sem_init(&sem, 0, 0);  // 初始计数为0

// 任务A:释放信号量
sem_post(&sem);

// 任务B:获取信号量
sem_wait(&sem);  // 如果计数为0,这里会阻塞

我在项目中常用信号量做同步。比如网络协议栈里,DMA传输完成后在中断里sem_post(),处理任务里sem_wait()。这样处理任务就不会空转,省电又高效。

小技巧: 使用 sem_trywait() 可以非阻塞地尝试获取信号量。如果获取失败,任务可以去做别的事,而不是傻等。

4.4.2 消息队列

消息队列适合传递数据块。比如一个任务采集传感器数据,另一个任务处理数据,中间用消息队列传递。

mqd_t mq;
struct mq_attr attr;
attr.mq_maxmsg = 10;     // 最大消息数
attr.mq_msgsize = 64;    // 每条消息最大字节数
attr.mq_flags = 0;

mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0644, &attr);

// 发送消息
char buf[] = "hello";
mq_send(mq, buf, strlen(buf) + 1, 0);

// 接收消息
char rcv_buf[64];
mq_receive(mq, rcv_buf, sizeof(rcv_buf), NULL);

你想想看,如果两个任务共享一个全局变量,你得加锁保护。但用消息队列,数据是拷贝传递的,天然就是线程安全的。我建议网络协议栈里,不同层之间的数据传递都用消息队列,省心。

4.4.3 事件组

事件组适合等待多个条件同时满足的场景。比如一个任务要等网络连接建立、DHCP获取IP、DNS解析完成三个事件都发生了,才开始工作。

struct event_group_s *evt;
evt = event_group_create();

// 任务A:设置事件位
event_group_set_bits(evt, 0x01);  // 网络连接建立

// 任务B:等待多个事件位
event_group_wait_bits(evt, 0x07, false, false, NULL);  // 等待位0、1、2都置位

事件组的好处是你可以用位运算组合多个条件。我曾经在做一个多传感器融合项目时,用了事件组来同步GPS、IMU、磁力计三个数据源,等三个数据都到了才做融合计算。代码写起来很清爽。

总结一下:
  • 信号量:适合做同步和互斥,轻量级
  • 消息队列:适合传递数据,线程安全
  • 事件组:适合等待多个条件,灵活组合
选哪个?看你的场景。我个人习惯:同步用信号量,数据传递用消息队列,多条件等待用事件组。

好了,这一章的内容就到这里。任务管理是NuttX的根基,你把它吃透了,后面写网络协议栈代码就会顺手很多。记住我踩过的坑,别在中断里删任务,别随便挂起接收任务,用好信号量和消息队列。下次见。