3. RT-Thread 架构与源码结构

好,咱们进入第三章。这一章是 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 的线程有五种状态,理解它们对调试至关重要。我画了一张状态转换图,方便你理解。

RT-Thread 线程状态转换图 初始状态 就绪状态 运行状态 挂起状态 关闭状态 rt_thread_startup() 调度器选择 时间片用完/被抢占 等待事件/延时 事件到达/延时结束 线程退出 rt_thread_delete() 未启动直接删除 状态说明: 初始:线程刚创建,未启动 就绪:线程具备运行条件,等待调度器选择 运行:线程正在执行 挂起:线程等待某个事件或延时 关闭:线程结束或被删除

这五种状态里,我最常打交道的是挂起状态。调试时发现线程不跑了,十有八九是它挂起了,但没人唤醒它。

3.3 优先级与时间片

优先级和时间片是调度器的两个核心参数。优先级决定谁先跑,时间片决定能跑多久。

3.3.1 优先级

RT-Thread 支持 256 级优先级(0 最高,255 最低)。数值越小,优先级越高。实际项目中,我一般只用 8-16 个优先级,够用就好。

优先级范围 典型用途 注意事项
0-7 中断处理、高实时任务 尽量少用,留给关键任务
8-15 通信协议栈、数据采集 确保不被低优先级任务阻塞
16-31 普通控制任务 大部分任务放这里
32-255 后台任务、空闲任务 优先级反转的重灾区
避坑指南: 我曾经在一个多传感器采集项目里,把两个任务设成了相同优先级。结果一个任务里的延时函数导致另一个任务迟迟得不到 CPU,数据采集出现了周期性丢包。后来我把采集任务优先级调高了一级,问题就解决了。

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 调度过程

调度器的工作流程大致如下:

  1. 保存当前线程的上下文(寄存器、栈指针等)
  2. 从就绪队列中找到优先级最高的线程
  3. 恢复该线程的上下文
  4. 跳转到该线程继续执行

这个过程在 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,说明栈空间可能不够,需要加大。这些信息对定位问题非常有帮助。

重要提醒: 线程栈大小设置要留余量。我一般按实际使用量的 1.5 倍来设。曾经有个同事把栈设得刚刚好,结果程序跑几天就莫名其妙崩溃,最后发现是栈溢出把相邻的变量给覆盖了。

好了,线程管理与调度的核心内容就这些。记住:创建线程时要选对方式,理解状态转换才能调试,优先级和时间片要合理搭配,调度器的工作原理是理解整个系统的基石。这些概念搞清楚了,后面学外设驱动和调试就会轻松很多。