第三章 线程管理与调度:线程控制块、线程状态与切换、优先级与时间片调度、线程创建与删除实战

大家好,我是你们的老朋友。今天我们来聊聊 RT-Thread 里最核心的部分——线程管理。说实话,我刚开始接触 RTOS 时,觉得线程就是个「会跑的函数」。后来踩了不少坑才明白,线程控制块、状态切换、优先级这些,才是决定系统稳不稳的关键。

3.1 线程控制块——线程的「身份证」

每个线程在 RT-Thread 里都有一个对应的数据结构,叫线程控制块(TCB)。你可以把它想象成线程的「身份证」,里面记录了线程的所有信息。

核心结构体:struct rt_thread

struct rt_thread {
    rt_list_t       list;           // 就绪链表节点
    rt_list_t       tlist;          // 线程链表节点
    void           *sp;             // 栈指针
    void           *entry;          // 入口函数
    void           *parameter;      // 入口参数
    void           *stack_addr;     // 栈起始地址
    rt_uint32_t     stack_size;     // 栈大小
    rt_uint8_t      current_priority;   // 当前优先级
    rt_uint8_t      init_priority;      // 初始优先级
    rt_uint32_t     number_mask;    // 优先级位图掩码
    rt_uint8_t      stat;           // 线程状态
    rt_uint8_t      err;            // 错误码
    rt_uint32_t     remaining_tick; // 剩余时间片
    rt_uint32_t     init_tick;      // 初始时间片
    rt_uint32_t     error;          // 错误码
    void           *cleanup;        // 清理函数
    rt_uint32_t     user_data;      // 用户数据
};

我个人习惯把 TCB 比作「档案袋」。每次线程切换,系统就是从这个档案袋里取出信息,恢复现场。嗯,这里要注意:栈指针 sp 是最关键的字段,它指向线程当前的运行栈位置。切换线程时,CPU 就是靠它找到「上次干到哪了」。

小技巧:调试时打印 thread->statthread->current_priority,能快速定位线程卡死的原因。我在项目中遇到过线程莫名其妙不跑了,一查发现状态是 RT_THREAD_SUSPEND,原来是等信号量超时了。

3.2 线程状态与切换——线程的「生老病死」

一个线程从创建到销毁,会经历多个状态。RT-Thread 定义了 5 种基本状态:

状态 宏定义 说明
初始态 RT_THREAD_INIT 刚创建,还没启动
就绪态 RT_THREAD_READY 万事俱备,只等 CPU
运行态 RT_THREAD_RUNNING 正在使用 CPU
挂起态 RT_THREAD_SUSPEND 等待某件事(延时、信号量等)
关闭态 RT_THREAD_CLOSE 线程已结束,等待回收

状态切换说白了就是「谁该干活了」。系统里有个调度器,它每隔一个 tick(系统心跳)就检查一次:当前线程是不是该让位了?有没有更高优先级的线程在等?

为什么会这样?因为 RT-Thread 是抢占式内核。高优先级的线程一旦就绪,会立刻抢走 CPU。我记得有一次调试一个串口接收任务,低优先级的线程一直在跑,高优先级的接收线程死活进不去。后来发现是低优先级线程里有个死循环,没给调度器机会。加了个 rt_thread_yield() 就好了。

避坑指南:我曾经在中断服务函数里直接调用了 rt_thread_delay(),结果系统直接死机。记住:中断里不能调用任何可能引起线程切换的函数。中断只该做最轻量的事,比如发个信号量。

3.3 优先级与时间片调度——谁先跑,跑多久

RT-Thread 支持 256 级优先级(0 最高,255 最低)。调度规则很简单:谁优先级高谁先跑。如果优先级相同,就按时间片轮转。

时间片是什么?说白了就是每个线程一次能跑多久。默认是 10 个 tick(具体看配置)。比如两个优先级都是 10 的线程 A 和 B:

  • A 跑 10 个 tick → 时间片用完 → 切换到 B
  • B 跑 10 个 tick → 时间片用完 → 切换回 A
  • 如此循环

你想想看,如果某个线程的时间片设得特别大,其他同优先级的线程就得等很久。我在项目中遇到过 UI 界面卡顿,查了半天发现是后台数据处理线程的时间片设成了 100 个 tick,前台 UI 线程根本抢不到 CPU。

关键函数:

  • rt_thread_create() — 动态创建线程
  • rt_thread_init() — 静态初始化线程
  • rt_thread_startup() — 启动线程
  • rt_thread_delete() — 删除动态线程
  • rt_thread_detach() — 脱离静态线程

3.4 线程创建与删除实战——手把手写代码

光说不练假把式。我们直接写个例子,创建一个 LED 闪烁线程和一个按键扫描线程。

#include <rtthread.h>

/* 线程控制块指针 */
static rt_thread_t led_thread = RT_NULL;
static rt_thread_t key_thread = RT_NULL;

/* LED 线程入口 */
static void led_entry(void *parameter)
{
    while (1)
    {
        rt_kprintf("LED ON\n");
        rt_thread_mdelay(500);  // 延时 500ms
        rt_kprintf("LED OFF\n");
        rt_thread_mdelay(500);
    }
}

/* 按键扫描线程入口 */
static void key_entry(void *parameter)
{
    while (1)
    {
        rt_kprintf("Key scan...\n");
        rt_thread_mdelay(100);  // 每 100ms 扫描一次
    }
}

/* 创建线程 */
int thread_demo_init(void)
{
    /* 动态创建 LED 线程 */
    led_thread = rt_thread_create("led",
                                  led_entry,
                                  RT_NULL,
                                  1024,      // 栈大小 1KB
                                  5,         // 优先级 5
                                  10);       // 时间片 10 tick
    if (led_thread != RT_NULL)
    {
        rt_thread_startup(led_thread);
    }

    /* 动态创建按键线程 */
    key_thread = rt_thread_create("key",
                                  key_entry,
                                  RT_NULL,
                                  512,       // 栈大小 512B
                                  10,        // 优先级 10
                                  10);
    if (key_thread != RT_NULL)
    {
        rt_thread_startup(key_thread);
    }

    return 0;
}
/* 自动初始化 */
INIT_APP_EXPORT(thread_demo_init);

这段代码里,LED 线程优先级是 5,按键线程是 10。所以 LED 线程会优先运行。如果想让按键响应更快,可以把它的优先级调高。

删除线程时要注意:不能删除自己。我曾经写了个线程,在入口函数里调 rt_thread_delete(rt_thread_self()),结果系统直接崩溃。正确的做法是让线程自己退出,或者由其他线程来删除它。

经验之谈:栈大小怎么定?我一般先给个保守值(比如 1024),然后用 rt_thread_control() 查看实际使用量。如果发现栈溢出,再调大。别一开始就给 4096,嵌入式设备内存金贵着呢。

3.5 调度器的工作原理——谁来决定下一个跑谁

调度器是 RT-Thread 的大脑。它维护了一个优先级位图,每个优先级对应一个 bit。当某个优先级上有就绪线程时,对应的 bit 置 1。调度时,用位图找到最高优先级的非空链表,取出第一个线程来运行。

这个过程是 O(1) 的,不管系统里有多少线程,调度时间都是固定的。这也是 RT-Thread 实时性好的原因之一。

你想想看,如果系统里有 50 个线程,调度器每次都要遍历一遍,那实时性就完蛋了。位图算法完美解决了这个问题。

调度触发条件:

  1. 当前线程时间片用完
  2. 有更高优先级线程就绪
  3. 当前线程主动挂起(如调用 delay、等待信号量)
  4. 线程主动让出 CPU(yield)

嗯,这里要补充一句:中断返回时也会触发调度。如果中断里唤醒了一个高优先级线程,中断退出后不会回到原来的线程,而是直接切换到高优先级线程。这就是抢占式调度的精髓。

好了,线程管理这块就聊到这儿。下一章我们讲信号量与互斥量,那可是线程同步的利器。到时候我会分享一个我调试过的「优先级反转」案例,保证让你印象深刻。