3. 线程管理与调度:线程创建与删除、线程状态、优先级与时间片、调度器工作原理
好,咱们进入第三章。这一章是 RT-Thread 操作系统的核心——线程管理与调度。说白了,就是操作系统怎么管好你写的那些任务,谁先跑、谁后跑、跑多久、怎么切换。我刚开始接触 RTOS 时,觉得这部分很抽象,后来在项目里踩过几次坑,才真正理解它的重要性。
3.1 线程的创建与删除
在 RT-Thread 里,线程就是一条独立的执行流。每个线程都有自己的栈空间、入口函数和优先级。创建线程有两种方式:动态创建和静态创建。
3.1.1 动态创建线程
动态创建使用 rt_thread_create() 函数。系统会从堆里分配线程控制块和栈空间。这种方式灵活,适合线程数量不确定的场景。
rt_thread_t tid = RT_NULL;
tid = rt_thread_create("led_thread",
led_entry, // 入口函数
RT_NULL, // 参数
512, // 栈大小(字节)
25, // 优先级
10); // 时间片(系统滴答数)
if (tid != RT_NULL)
rt_thread_startup(tid);
我个人习惯在原型验证阶段用动态创建,改起来方便。但要注意,动态创建依赖堆管理,如果堆碎片化严重,可能会分配失败。
3.1.2 静态创建线程
静态创建使用 rt_thread_init() 函数。线程控制块和栈空间由用户预先定义,不依赖堆。这种方式适合资源受限或对实时性要求高的场景。
static struct rt_thread led_thread;
static char led_stack[512];
rt_err_t result = rt_thread_init(&led_thread,
"led_thread",
led_entry,
RT_NULL,
&led_stack[0],
sizeof(led_stack),
25,
10);
if (result == RT_EOK)
rt_thread_startup(&led_thread);
3.1.3 删除线程
动态创建的线程用 rt_thread_delete() 删除,静态创建的用 rt_thread_detach() 分离。删除线程时,系统会释放资源,但前提是线程已经退出或处于挂起状态。
3.2 线程的五大状态
RT-Thread 的线程有五种状态,理解它们对调试至关重要。我画了一张状态转换图,方便你理解。
这五种状态里,我最常打交道的是挂起状态。调试时发现线程不跑了,十有八九是它挂起了,但没人唤醒它。
3.3 优先级与时间片
优先级和时间片是调度器的两个核心参数。优先级决定谁先跑,时间片决定能跑多久。
3.3.1 优先级
RT-Thread 支持 256 级优先级(0 最高,255 最低)。数值越小,优先级越高。实际项目中,我一般只用 8-16 个优先级,够用就好。
| 优先级范围 | 典型用途 | 注意事项 |
|---|---|---|
| 0-7 | 中断处理、高实时任务 | 尽量少用,留给关键任务 |
| 8-15 | 通信协议栈、数据采集 | 确保不被低优先级任务阻塞 |
| 16-31 | 普通控制任务 | 大部分任务放这里 |
| 32-255 | 后台任务、空闲任务 | 优先级反转的重灾区 |
3.3.2 时间片
时间片只对相同优先级的线程有效。当多个线程优先级相同时,调度器会按时间片轮转方式分配 CPU。时间片单位是系统滴答(通常 1ms 或 10ms)。
举个例子:两个优先级都是 20 的线程,时间片分别是 5 和 10。那么线程 A 每次运行 5 个滴答,线程 B 每次运行 10 个滴答,交替执行。
3.4 调度器工作原理
RT-Thread 的调度器是基于优先级的抢占式调度器。说白了,就是高优先级的线程随时可以打断低优先级的线程。调度器维护一个就绪优先级位图,通过查表法快速找到最高优先级的就绪线程。
3.4.1 调度触发条件
调度器在以下几种情况下会被触发:
- 线程主动调用
rt_thread_yield()让出 CPU - 线程调用延时函数
rt_thread_delay()或rt_thread_sleep() - 线程等待的信号量、消息队列等资源可用
- 中断服务函数退出前,发现有更高优先级的线程就绪
- 时间片用完
3.4.2 调度过程
调度器的工作流程大致如下:
- 保存当前线程的上下文(寄存器、栈指针等)
- 从就绪队列中找到优先级最高的线程
- 恢复该线程的上下文
- 跳转到该线程继续执行
这个过程在 RT-Thread 里由 rt_schedule() 函数完成。每次调度的时间开销通常在几微秒到十几微秒之间,取决于 CPU 主频和上下文切换的复杂度。
rt_schedule() 函数入口和出口加 GPIO 翻转,用示波器看调度频率。我在一个电机控制项目里就是这么干的,发现调度周期抖动很大,最后定位到是一个中断服务函数执行时间过长导致的。
3.5 实战:线程管理调试
最后,分享一个我在调试线程管理时常用的方法。当系统出现异常时,我会在串口终端输入 list_thread 命令,查看所有线程的状态。
msh /> list_thread
thread pri status sp stack size max used left tick error
----------- --- ------- ---------- ---------- ------ ---------- ---
tidle 31 ready 0x00000058 0x00000100 00000044 0x00000020 000
tshell 20 ready 0x00000068 0x00001000 00000068 0x0000000a 000
led_thread 25 suspend 0x00000048 0x00000200 00000048 0x00000005 000
看到 suspend 状态时,我会检查线程在等什么资源。如果 max used 接近 stack size,说明栈空间可能不够,需要加大。这些信息对定位问题非常有帮助。
好了,线程管理与调度的核心内容就这些。记住:创建线程时要选对方式,理解状态转换才能调试,优先级和时间片要合理搭配,调度器的工作原理是理解整个系统的基石。这些概念搞清楚了,后面学外设驱动和调试就会轻松很多。