第三章 线程管理与调度:线程控制块、线程状态与切换、优先级与时间片调度、线程创建与删除实战
大家好,我是你们的老朋友。今天我们来聊聊 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->stat 和 thread->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 个线程,调度器每次都要遍历一遍,那实时性就完蛋了。位图算法完美解决了这个问题。
调度触发条件:
- 当前线程时间片用完
- 有更高优先级线程就绪
- 当前线程主动挂起(如调用 delay、等待信号量)
- 线程主动让出 CPU(yield)
嗯,这里要补充一句:中断返回时也会触发调度。如果中断里唤醒了一个高优先级线程,中断退出后不会回到原来的线程,而是直接切换到高优先级线程。这就是抢占式调度的精髓。
好了,线程管理这块就聊到这儿。下一章我们讲信号量与互斥量,那可是线程同步的利器。到时候我会分享一个我调试过的「优先级反转」案例,保证让你印象深刻。