第3章 线程管理与调度(上):线程的概念与状态、线程控制块(TCB)、线程栈与优先级、线程创建与删除(静态/动态)、线程启动与脱离
各位同学,咱们今天开始聊线程管理。说实话,线程这个概念,很多初学者觉得抽象,其实说白了——线程就是RTOS里的一条执行流。你想想看,裸机编程时,CPU一次只能干一件事,但有了RTOS,我们可以把任务拆成多个线程,让它们轮流跑,感觉就像同时干好几件事。
我刚开始接触RT-Thread时,对线程的理解也停留在“就是个函数”的层面。直到有一次做多传感器数据采集项目,三个传感器同时工作,我才真正体会到线程管理的精髓。好,咱们正式开始。
3.1 线程的概念与状态
线程,在RT-Thread里叫rt_thread,是系统调度的基本单位。每个线程都有自己的入口函数、栈空间、优先级,以及一个线程控制块(TCB)。
线程的状态,我习惯用“五态模型”来记:
- 初始态:线程刚创建,还没启动。就像你写好了一个函数,但还没调用它。
- 就绪态:线程具备运行条件,只等CPU分配时间片。说白了就是“万事俱备,只欠东风”。
- 运行态:线程正在占用CPU执行。注意,单核CPU上同一时刻只有一个线程在运行。
- 挂起态:线程因为等待某个资源(比如信号量、消息队列)而暂停。我曾经调试过一个bug,线程莫名其妙不跑了,最后发现是信号量没释放,线程一直挂在那。
- 关闭态:线程执行完毕或被删除,资源等待回收。
状态转换的关键点:线程从就绪态到运行态,靠的是调度器;从运行态到挂起态,靠的是线程主动调用延时或等待函数。记住,线程自己不能把自己从挂起态唤醒,必须由其他线程或中断来唤醒它。
3.2 线程控制块(TCB)
TCB是线程的“身份证”。RT-Thread里,每个线程都有一个struct rt_thread结构体,里面存了线程的所有信息。我挑几个重要的字段说说:
| 字段名 | 作用 |
|---|---|
name |
线程名字,调试时特别有用。我习惯用“task_xxx”命名,一眼就能看出功能。 |
stack_addr |
栈起始地址。栈溢出是嵌入式开发的常见问题,后面会细说。 |
stack_size |
栈大小,单位是字节。给大了浪费内存,给小了容易溢出。 |
priority |
优先级,数值越小优先级越高。RT-Thread支持256级优先级(0~255)。 |
stat |
当前状态,就是上面说的五态之一。 |
entry |
线程入口函数指针。线程跑的就是这个函数。 |
嗯,这里要注意:TCB本身是存在内存里的,静态线程的TCB由编译器分配,动态线程的TCB从堆里分配。我个人建议,如果线程数量固定,用静态方式更可靠,避免堆碎片问题。
3.3 线程栈与优先级
线程栈,说白了就是线程的“临时记事本”。每次函数调用、局部变量、中断现场,都往栈里存。栈大小怎么定?我有个土办法:先给个512字节,跑起来后用rt_thread_control查栈使用率,再调整。
避坑指南:我曾经在一个项目中,给一个处理网络数据的线程只分配了256字节栈,结果程序跑几个小时就死机一次。查了两天才发现是栈溢出,把相邻线程的TCB给覆盖了。从那以后,我每个线程至少给512字节,关键线程给1KB以上。
优先级,是调度器决定“谁先跑”的依据。RT-Thread支持两种调度方式:
- 抢占式调度:高优先级线程就绪时,立即抢占低优先级线程的CPU。这是默认方式。
- 时间片轮转:同优先级线程轮流跑,每个线程跑一个时间片(默认10个系统滴答)。
优先级设计有个原则:紧急且短的任务给高优先级,不紧急或耗时的任务给低优先级。比如按键响应给高优先级,LED闪烁给低优先级。
3.4 线程创建与删除
RT-Thread支持两种创建方式:静态和动态。我分别说说。
3.4.1 静态创建
静态创建需要预先定义好TCB和栈空间。适合线程数量固定的场景。
// 定义线程控制块和栈
static struct rt_thread led_thread;
static rt_uint8_t led_stack[512];
// 线程入口函数
static void led_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_init(&led_thread, // TCB指针
"led", // 线程名
led_entry, // 入口函数
RT_NULL, // 入口参数
&led_stack[0], // 栈起始地址
sizeof(led_stack), // 栈大小
20, // 优先级
10); // 时间片
3.4.2 动态创建
动态创建由系统自动分配TCB和栈,适合线程数量不确定的场景。
// 动态创建线程
rt_thread_t led_thread = rt_thread_create("led", // 线程名
led_entry, // 入口函数
RT_NULL, // 入口参数
512, // 栈大小
20, // 优先级
10); // 时间片
if (led_thread != RT_NULL)
rt_thread_startup(led_thread);
我的建议:动态创建方便,但要注意堆内存管理。如果频繁创建删除线程,容易产生内存碎片。我一般只在初始化阶段动态创建,运行期间不再变动。
3.4.3 删除线程
删除线程用rt_thread_delete(动态)或rt_thread_detach(静态)。注意:线程不能删除自己,必须由其他线程来删除。
// 删除动态线程
rt_thread_delete(led_thread);
// 脱离静态线程
rt_thread_detach(&led_thread);
3.5 线程启动与脱离
线程创建后,并不会立即运行。你需要调用rt_thread_startup来启动它。启动后,线程进入就绪态,等待调度器分配CPU。
// 启动线程
rt_thread_startup(led_thread);
线程脱离,指的是把线程从调度器里移除,但保留TCB和栈。通常用于静态线程的“休眠”场景。比如某个功能模块暂时不用,可以先脱离,需要时再重新初始化。
关键区别:rt_thread_delete会释放动态分配的资源,而rt_thread_detach只是把线程从就绪队列里移除,资源还在。你想想看,如果静态线程调用了delete,那TCB和栈就变成野指针了,后果很严重。
好,这一章的内容就到这。线程管理是RTOS的基石,你把这些概念吃透了,后面学信号量、消息队列就会轻松很多。下一章咱们接着聊线程调度的细节,包括如何避免优先级反转、如何设计合理的优先级体系。到时候见。