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()。我曾经在项目里这么干过,结果系统直接死锁。为什么?因为中断上下文里没有任务调度环境,你删了一个任务,调度器就懵了。
4.2 任务优先级与调度策略
NuttX支持两种调度策略:SCHED_FIFO和SCHED_RR。说白了,就是两种排队方式。
SCHED_FIFO:先来先服务。一个任务只要不主动让出CPU,或者不被更高优先级的任务抢占,它就一直跑。适合那些计算量大、不需要频繁切换的任务。
SCHED_RR:时间片轮转。每个任务跑一个固定时间片,时间到了就换下一个同优先级的任务。适合多个同等重要的任务轮流执行。
设置调度策略的代码:
struct sched_param param;
param.sched_priority = 100;
sched_setscheduler(0, SCHED_FIFO, ¶m);
这里0表示当前任务。你想想看,如果你的网络协议栈里有一个数据接收任务和一个数据发送任务,你会怎么分配优先级?我个人建议接收任务优先级高一点,因为数据来了你不及时收,缓冲区可能就溢出了。
4.3 任务状态转换
每个任务在生命周期里会经历几个状态:就绪、运行、阻塞、挂起、终止。我画了一张图,帮你理清它们之间的关系。
这张图你看懂了吗?任务从就绪到运行靠调度器决定。运行中如果等信号量或消息队列,就进入阻塞。阻塞结束后回到就绪队列。你也可以手动挂起任务,让它暂时不参与调度。
嗯,这里要注意:阻塞和挂起是两码事。阻塞是任务自己主动等资源,挂起是别的任务或中断强制让它休息。我在调试一个网络协议栈时,曾经把一个接收任务挂起了,结果数据来了没人收,整个协议栈就卡死了。后来我改用信号量等待,问题就解决了。
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()。这样处理任务就不会空转,省电又高效。
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的根基,你把它吃透了,后面写网络协议栈代码就会顺手很多。记住我踩过的坑,别在中断里删任务,别随便挂起接收任务,用好信号量和消息队列。下次见。