任务(线程)管理基础

各位同学,今天我们来聊聊 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 线程执行完毕或被删除,等待回收

状态之间的转换关系,我画了张图,你一看就明白:

RT-Thread 线程状态转换图 初始态 就绪态 运行态 挂起态 关闭态 线程启动 调度器选择 时间片用完 等待资源 资源就绪 线程退出 删除线程

我的经验:调试时最常遇到的就是线程莫名其妙“卡死”了。这时候先查状态——如果线程一直处于挂起态,多半是等待的信号量或消息队列没释放。我曾经花了一整天查一个 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 调度的钥匙。剩下的就是多写代码、多调试,慢慢积累手感。