任务(线程)管理基础
各位同学,今天我们来聊聊 RT-Thread 里最核心的概念——任务管理。说白了,任务就是一段可以独立运行的程序片段。在 RTOS 的世界里,CPU 只有一个,但任务可以有很多个。怎么让它们井井有条地轮流跑?这就是任务调度要解决的问题。
我记得刚接触 RTOS 那会儿,总觉得任务和函数差不多。后来踩了不少坑才明白,任务是有“生命”的,它有出生、运行、休眠、死亡这些状态。你想想看,如果连任务的状态都搞不清楚,写出来的代码肯定到处是 bug。
线程的概念与状态
在 RT-Thread 里,线程(Thread)就是任务。每个线程都是一个独立的执行流,有自己的栈空间、优先级和状态。线程的状态切换,是调度器工作的基础。
RT-Thread 定义了 5 种线程状态:
| 状态 | 宏定义 | 说明 |
|---|---|---|
| 初始态 | RT_THREAD_INIT | 线程刚创建,还没开始运行 |
| 就绪态 | RT_THREAD_READY | 线程已准备好,等待调度器分配 CPU |
| 运行态 | RT_THREAD_RUNNING | 线程正在占用 CPU 执行 |
| 挂起态 | RT_THREAD_SUSPEND | 线程因等待资源或延时被暂停 |
| 关闭态 | RT_THREAD_CLOSE | 线程执行完毕或被删除,等待回收 |
状态之间的转换关系,我画了张图,你一看就明白:
我的经验:调试时最常遇到的就是线程莫名其妙“卡死”了。这时候先查状态——如果线程一直处于挂起态,多半是等待的信号量或消息队列没释放。我曾经花了一整天查一个 bug,最后发现是某个中断里忘了释放互斥量。
线程控制块(TCB)
每个线程在 RT-Thread 内部都有一个对应的控制块,类型是 struct rt_thread。你可以把它理解成线程的“身份证”,调度器靠它来管理所有线程。
TCB 里存了哪些东西?我列一下关键字段:
- 线程名称:调试时用来区分线程,建议起有意义的名字
- 栈指针:指向线程栈的当前位置
- 优先级:决定线程被调度的先后顺序
- 时间片:同优先级线程轮转执行的时间配额
- 状态标志:记录线程当前处于什么状态
- 挂起链表:用于将线程挂到等待队列上
- 入口函数:线程启动后执行的函数地址
TCB 的定义在 rtdef.h 里,感兴趣的同学可以自己翻翻源码。我建议你至少把结构体的大致布局记住,因为调试时经常要看 TCB 里的值。
线程栈与优先级
每个线程都有自己的栈空间。栈用来保存局部变量、函数调用时的返回地址、以及上下文切换时的寄存器值。栈的大小怎么定?
避坑指南:我曾经在一个项目中把栈设得太小,结果程序跑着跑着就 HardFault 了。查了半天才发现是栈溢出,把相邻的 TCB 数据给踩了。后来我养成了一个习惯:先给个保守值(比如 1024 字节),跑起来后用 rt_thread_control 查实际使用量,再慢慢调小。
优先级是 RT-Thread 调度算法的核心。RT-Thread 支持 256 个优先级(0~255),数值越小优先级越高。0 是最高优先级,一般留给中断或关键任务。
调度规则很简单:
- 高优先级线程就绪时,立即抢占低优先级线程
- 同优先级线程采用时间片轮转调度
- 没有就绪线程时,系统运行空闲线程
动态/静态创建线程
RT-Thread 提供了两种创建线程的方式:动态创建和静态创建。我刚开始用的时候也纠结过选哪个,后来总结了一套经验。
| 对比项 | 动态创建 | 静态创建 |
|---|---|---|
| API 函数 | rt_thread_create() |
rt_thread_init() |
| 内存来源 | 从堆中动态分配 | 由用户提供静态缓冲区 |
| 灵活性 | 高,可随时创建/删除 | 低,编译时确定 |
| 确定性 | 可能分配失败 | 总是成功 |
| 适用场景 | 任务数量动态变化 | 任务固定、资源受限 |
动态创建的代码示例:
#include <rtthread.h>
/* 线程入口函数 */
static void led_thread_entry(void *parameter)
{
while (1)
{
rt_pin_write(LED_PIN, PIN_HIGH);
rt_thread_mdelay(500);
rt_pin_write(LED_PIN, PIN_LOW);
rt_thread_mdelay(500);
}
}
/* 动态创建线程 */
rt_thread_t led_thread = rt_thread_create(
"led", /* 线程名称 */
led_thread_entry, /* 入口函数 */
RT_NULL, /* 入口参数 */
1024, /* 栈大小,单位字节 */
5, /* 优先级 */
10 /* 时间片,单位系统时钟节拍 */
);
if (led_thread != RT_NULL)
{
rt_thread_startup(led_thread); /* 启动线程 */
}
else
{
rt_kprintf("创建 led 线程失败!\n");
}
静态创建的代码示例:
#include <rtthread.h>
/* 定义线程栈和 TCB 的静态缓冲区 */
static rt_uint8_t led_stack[1024];
static struct rt_thread led_thread;
/* 线程入口函数 */
static void led_thread_entry(void *parameter)
{
/* 同上 */
}
/* 静态初始化线程 */
rt_err_t result = rt_thread_init(
&led_thread, /* TCB 指针 */
"led", /* 线程名称 */
led_thread_entry, /* 入口函数 */
RT_NULL, /* 入口参数 */
led_stack, /* 栈起始地址 */
sizeof(led_stack), /* 栈大小 */
5, /* 优先级 */
10 /* 时间片 */
);
if (result == RT_EOK)
{
rt_thread_startup(&led_thread);
}
else
{
rt_kprintf("初始化 led 线程失败!\n");
}
我的建议:产品原型阶段用动态创建,方便调试和调整。量产代码如果内存紧张,换成静态创建更稳妥。我见过一个项目,动态创建线程时堆碎片化严重,跑了几天后分配失败导致系统崩溃——换成静态后问题就解决了。
线程删除与脱离
线程不再需要时,有两种处理方式:删除和脱离。两者有本质区别。
线程删除:调用 rt_thread_delete(),系统会释放线程占用的资源(栈、TCB),前提是线程是用动态方式创建的。删除后线程进入关闭态,等待系统回收。
线程脱离:调用 rt_thread_detach(),只把线程从调度队列中移除,但不会释放资源。适用于静态创建的线程,因为资源是用户自己管理的,系统不该替你做主。
我举个例子你就明白了:
/* 删除动态线程 */
rt_err_t del_result = rt_thread_delete(led_thread);
if (del_result == RT_EOK)
{
rt_kprintf("线程已删除\n");
}
/* 脱离静态线程 */
rt_err_t det_result = rt_thread_detach(&led_thread);
if (det_result == RT_EOK)
{
rt_kprintf("线程已脱离\n");
}
注意:千万不要在中断服务函数里调用 rt_thread_delete()!我曾经犯过这个错,结果系统直接死机。原因是删除操作可能触发内存释放,而中断里不允许做内存操作。正确的做法是在中断里发一个信号量,让线程自己退出。
另外,线程删除前要确保它已经退出运行态。如果线程正在运行,删除操作会先把它挂起再删除。但如果你删除了一个持有互斥量的线程,其他等待该互斥量的线程就会永远等下去——嗯,这种死锁问题排查起来相当头疼。
好了,任务管理的基础就讲到这里。记住一句话:理解线程的状态转换,你就掌握了 RT-Thread 调度的钥匙。剩下的就是多写代码、多调试,慢慢积累手感。