线程管理与调度:从控制块到时间片轮转
各位同学,今天我们来聊聊 RT-Thread 里最核心的部分——线程管理。说白了,一个 RTOS 好不好用,很大程度上就看它怎么管线程。我做了这么多年嵌入式,见过太多项目因为线程管理不当而翻车。嗯,咱们今天就把这块彻底讲透。
线程控制块——每个线程的身份证
在 RT-Thread 里,每个线程都有一个对应的控制块。你可以把它想象成线程的「身份证」,里面存着线程的所有关键信息。
我习惯把控制块理解成一张表格:
| 字段 | 含义 | 我踩过的坑 |
|---|---|---|
stack_addr |
栈起始地址 | 栈对齐没做好,直接 HardFault |
stack_size |
栈大小 | 给太小,递归一深就崩 |
entry |
线程入口函数 | 入口函数不能返回,切记 |
init_tick |
初始化时间片 | 时间片设太大,低优先级线程饿死 |
remaining_tick |
剩余时间片 | 每次时钟中断都会减这个值 |
stat |
当前状态 | 状态切换错了,调度器就乱套 |
线程状态与切换——状态机其实很简单
RT-Thread 的线程有 5 种状态。你想想看,一个线程从出生到消亡,无非就是在这几个状态里转悠:
- 初始态:刚创建,还没启动
- 就绪态:万事俱备,只差 CPU
- 运行态:正在跑代码
- 挂起态:等某个事件(延时、信号量、消息队列)
- 终止态:跑完了或者被强制结束
这里有个关键点——只有就绪态和运行态的线程才能被调度器选中。挂起态的线程,调度器根本不会看它一眼。
线程切换的时机,说白了就三种情况:
- 当前线程主动让出 CPU(调用延时或阻塞 API)
- 时间片用完了
- 有更高优先级的线程就绪了
创建/启动/删除线程——三板斧
创建线程有两种方式:动态和静态。我个人更推荐动态创建,除非你对内存有极端要求。
// 动态创建——我常用的方式
rt_thread_t tid = rt_thread_create(
"led_thread", // 名字,调试时有用
led_entry, // 入口函数
RT_NULL, // 入口参数
1024, // 栈大小
25, // 优先级(数值越小优先级越高)
10 // 时间片(单位:tick)
);
if (tid != RT_NULL) {
rt_thread_startup(tid); // 启动线程
}
启动线程用 rt_thread_startup,这个函数会把线程从初始态变成就绪态。注意,不是直接变成运行态——调度器会在下一个调度点才真正让它跑起来。
删除线程要小心:
// 删除线程
rt_thread_delete(tid); // 动态创建的线程用这个
// 或者
rt_thread_detach(tid); // 静态创建的线程用这个
时间片轮转调度原理——公平是相对的
时间片轮转,说白了就是让同优先级的线程轮流用 CPU。每个线程分到一段固定时间,用完了就换下一个。
RT-Thread 的调度策略是这样的:
- 优先级不同:高优先级线程先跑,直到它主动让出或被更高优先级抢占
- 优先级相同:时间片轮转,每人分一段
我画了一张图,帮你理解这个过程:
你看,三个线程优先级相同,每人分到 10 个 tick 的时间片。线程 A 跑完 10 个 tick,调度器就切换到线程 B,再切换到线程 C,然后回到线程 A。这就是轮转。
调度器的核心逻辑——就绪优先级组
RT-Thread 的调度器为什么快?因为它用了一个叫「就绪优先级组」的位图结构。说白了,就是用 32 位整数来表示 256 个优先级里哪些线程就绪了。
// 核心数据结构
rt_uint32_t rt_thread_ready_priority_group; // 就绪优先级组
// 每个 bit 对应一个优先级
// bit 0 = 优先级 0,bit 1 = 优先级 1,以此类推
// 查找最高优先级就绪线程
register rt_ubase_t highest_ready_priority;
highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group) - 1;
// __rt_ffs 返回最低位为 1 的位置
这个 __rt_ffs 函数(find first set)是调度器的灵魂。它能在几个指令周期内找到最高优先级的就绪线程。我刚开始看源码时,觉得这玩意儿太巧妙了——用空间换时间,一个 32 位整数就搞定了优先级查找。
实战中的线程设计建议
说了这么多理论,咱们来点实际的。我在项目中总结了几条线程设计的原则:
- 紧急的事用高优先级:比如电机控制、通信接收,优先级设 5-10
- 周期性的任务用中等优先级:比如数据采集、状态刷新,优先级设 15-20
- 不紧急的后台任务用低优先级:比如日志输出、界面刷新,优先级设 25-30
- 空闲线程不要占 CPU:RT-Thread 自带 idle 线程,优先级最低,别往里面塞东西
好了,线程管理这块的核心内容就这些。你想想看,控制块是身份证,状态是当前处境,调度器是交通警察,时间片是红绿灯——把这些概念串起来,RT-Thread 的线程调度就没那么神秘了。