RT-Thread学习笔记(二)
RT-Thread学习笔记
- 线程管理
- 线程控制块
- 线程属性
- 线程操作
- 动态创建线程
- 启动线程
- 静态创建线程
- 线程辅助函数
线程管理
RT-Thread 是支持多任务的操作系统,多任务是通过多线程的方式实现。线程是任务的载体,是 RTT 中最基本的调度单位。线程在运行的时候,它自己会认为独占 CPU 运行,线程执行时的运行环境称为上下文,具体来说就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等。
线程管理的主要功能时对线程进行管理和调度,系统中存在两类线程,分别是系统线程和用户线程,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除。
RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到 CPU 的使用权。
当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该线程的上下文信息恢复。
线程控制块
线程控制块(Thread Control Block,简称 TCB)是操作系统中用于管理和维护线程状态的核心数据结构,在 RT-Thread 中也有相应的实现(例如 struct rt_thread 结构体)。
struct rt_thread
{/* inherit from rt_object */struct rt_object parent; // 内核对象头,包含名称、类型、标志等/* stack point and entry */void *sp; // 当前线程栈指针(用于上下文切换)void *entry; // 线程入口函数void *parameter; // 线程入口函数参数void *stack_addr; // 栈起始地址rt_ubase_t stack_size; // 栈大小(单位:字节)/* thread status */rt_uint8_t stat; // 当前线程状态,如 READY、SUSPEND 等rt_ubase_t priority; // 当前优先级rt_ubase_t init_priority; // 初始优先级rt_uint32_t number_mask; // 优先级掩码,用于调度优化rt_uint32_t remaining_tick; // 当前时间片剩余 tick 数rt_uint32_t init_tick; // 每次分配到的初始时间片大小/* error code */rt_err_t error; // 线程最近的错误代码/* thread list */rt_list_t tlist; // 就绪队列链表节点/* signal */
#ifdef RT_USING_SIGNALSrt_sigset_t sig_pending; // 挂起信号集合rt_sigset_t sig_mask; // 屏蔽信号集合void *sig_ret; // 信号返回地址rt_sighandler_t sig_vectors[RT_SIG_MAX]; // 信号处理函数表void *sig_info[RT_SIG_MAX]; // 信号附加信息
#endif/* defunct thread list */rt_list_t thread_timer_list; // 定时器链表节点(挂起/超时使用)/* thread cleanup */void (*cleanup)(struct rt_thread *tid); // 清理函数指针rt_uint32_t user_data; // 用户自定义数据
};
RT_USING_SMP 是 RT-Thread 操作系统中的一个 配置宏,用于启用对多核处理器(SMP,Symmetric Multi-Processing,对称多处理)架构的支持。对于单核的CPU,如CotexM3架构的CPU是无法使用的,只有多核的CPU,如CotexA9架构可以使用
cleanup函数指针指向的函数,会在线程退出的时候,被idle线程回调一次,执行用户设置的清理现场等工作。即线程的调度有时候可能需要占用内存,当线程执行完后需要对内存进行释放
线程属性
线程栈: 线程栈是每个线程独立拥有的一块内存区域,用于存放线程运行时的局部变量、函数调用信息、返回地址、上下文保存等数据。
简单理解:线程栈 = 线程执行过程中临时工作的“记事本” + “备份仓库”
线程状态
线程优先级
RT-Thread 最大支持 256 个线程优先级 (0~255),数值越小的优先级越高,0 为最高优先级。在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置;对于 ARM Cortex-M系列,普遍采用 32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行
时间片
每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效
注意:
作为一个实时系统,一个优先级明确的实时系统,如果一个线程中的程序陷入了死循环操作,那么比它优先级低的线程都将不能够得到执行。所以在实时操作系统中必须注意的一点就是:线程中不能陷入死循环操作,必须要有让出 CPU使用权的动作,如循环中调用延时函数或者主动挂起
线程间状态的切换
创建线程有两个函数,即进入初始状态
rt_thread_creat()
和 rt_thread_init()
调用 rt_thread_startup()
函数,线程进入就绪状态,能够参与调度,但是没有CPU执行权
运行状态的线程具有CPU执行权,如果就绪态的线程优先级与运行状态的线程优先级相同,那么会使用时间片的方式轮询两个线程,如果就绪态的线程优先级高于运行态的线程,会直接打断当前运行的线程,抢占CPU控制权
当就绪态线程调用 rt_thread_suspend() 函数,线程会挂起,不参与调度
处于挂起状态的线程可以调用下图函数重新回到就绪态
处于运行状态的线程调用下图函数可变为挂起状态,几乎与上面的函数相反
运行状态线程调用 rt_thread_suspend()
函数变为关闭状态
挂起状态线程调用 rt_thread_delete()
函数 或 rt_thread_detach()
函数变为关闭状态
线程操作
线程的操作包括:创建/初始化、启动、运行、删除/脱离。
动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。
线程创建 rt_thread_create()
结构体成员:线程名称、线程的入口函数、给入口函数的传参、栈大小、优先级、同优先级的线程会根据tick去轮询
返回值:struct rt_thread,即上文说的创建好的线程控制块
线程删除:rt_thread_delete()
返回值是RT_EOK表示删除成功,如果是其他值则表示删除失败
动态创建线程
使用 rt_thread_creat() 函数创建线程
这个函数有返回值
要先创建一个 rt_thread_t 类型的指针变量用于存放线程的返回值
rt_thread_t th1_ptr = NULL;
线程创建需要定义线程名,线程的入口函数,即该线程需要调用的函数,设置栈大小,优先级,以及tick
定义入口函数的功能是隔1s打印 rt1_entry running
rt_kprintf()
是 RT-Thread 内核级的打印函数,类似于标准 C 语言的 printf(),但它是 RT-Thread 内部为嵌入式环境优化过的版本。
📌 作用
在内核态、线程上下文、中断上下文中打印调试信息、变量值、错误信息等。
⚠️ 注意事项
与Linux应用程序类似,创建失败打印失败信息
LOG_E()
和LOG_D()
是 RT-Thread 中的日志系统(log system)提供的宏定义,分别用于输出 错误级别(Error) 和 调试级别(Debug) 的日志信息。
✅ 宏说明
用串口查看,可以看到线程创建成功,但是没有运行入口函数
用list_thread
命令查看线程状态,可以看到刚刚创建的线程是出于就绪态,即还没有参与调度,自然不会执行入口函数
一般不用自己去调用 rt_thread_delete()
函数去删除线程,释放资源,当线程执行完后系统会自动将该线程释放
启动线程
用rt_thread_startup()
函数启动线程
加上 return -RT_ENOMEM; 是为了明确地告诉系统或上层程序:“线程创建失败了,而且是因为内存不足”,加上这一句是为了在程序运行不正常时方便排查问题
线程成功创建并启动,进入入口函数,每隔1s打印信息
该线程是出于挂起状态,但是不应该是运行状态吗
因此该线程的入口函数内有一个1s的延时函数,因此这个线程大部分时间都是处于挂起状态,延时函数会让CPU去调度其他的线程
静态创建线程
线程初始化
用函数 rt_thread_init()
函数
函数原型
与动态创建函数不同,静态创建需要定义一个线程控制块结构体
调用成功返回RT_EOK,失败返回其他的错误值
线程脱离
rt_thread_detach()
函数
函数原型
调用成功返回RT_EOK,失败返回其他的错误值
传入的参数就是创建的线程控制块结构体,即把创建的线程控制块移除
先不让线程启动,查看状态
成功初始化
启动线程
可以看到上电后,线程2先执行,再执行线程1,两个线程交替执行,因为线程2的优先级高于线程1,所以一开始线程2先执行
线程2执行完10次后就
而且线程2执行完后,查看线程,发现没有线程2,说明线程2执行完后系统自动将该线程移除了
如果把线程2的延时注释掉,即没有延时
系统复位后先执行完线程2,才执行线程1,因为线程2优先级高,线程2没有使用延时释放CPU控制权,所以要一直等到线程2执行完后,才会将CPU控制权让给低优先级的
线程辅助函数
获得当前线程
在线程运行的过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过函数rt_thread_t rt_thread_self(void)
获得当前执行的线程句柄,返回值是线程的句柄
让出处理器资源
让线程主动让出CPU的控制权
rt_err_t rt_thread_yield(void)
线程睡眠
rt_err_t rt_thread_sleep(rt_tick_t tick)
rt_err_t rt_thread_delay(rt_tick_t tick)
传入参数是系统节拍数
rt_err_t rt_thread_mdelay(rt_int32 ms)
传入的参数是ms
线程控制函数
rt_err_t rt_thread_control(rt_thread_t thread, int cmd, void *arg)
第一个参数是线程的句柄,第二个参数是命令,第三个是传入的参数
🧩 支持的命令(常见)
设置和删除idle线程hook函数
🌀 一、Idle 线程(空闲线程)
✅ 什么是 Idle 线程?
Idle 线程是 RT-Thread 系统中**优先级最低(数值最大)**的线程,当系统没有其他线程就绪可运行时,调度器就会切换到 idle 线程。
它是系统自动创建和启动的,开发者不需要手动创建。
📌 主要作用:
CPU 空闲时执行任务(如节能或后台清理)
系统“空转”状态的安全处理
可挂载 idle hook,用于开发者扩展功能
在开启 RT_USING_HEAP 时,释放终止线程所占内存。
🧩 二、Hook 函数(钩子函数)
Hook 是一种机制,允许用户在系统特定行为上插入自定义函数,方便调试、监控或扩展功能。
RT-Thread 中常见 Hook 类型:
✅ 三、idle loop 是什么?
在 RT-Thread 中,idle 线程的入口函数会进入一个死循环,这个循环就是 idle loop。
🔍 idle loop 的作用
设置hook函数
rt_err_t rt_thread_idle_sethook(void (*hook)(void))
删除hook函数
rt_err_t rt_thread_idle_delhook(void (*hook)(void))
当空闲线程被调用时,对应的钩子函数,即hook函数也会被调用
空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态,例如 rt_thread_delay(),rt_sem_take() 等可能会导致线程挂起的函数都不能使用。即空闲线程中不能调用能使线程从就绪态或运行态转为挂起态的函数
设置调度器hook函数
void rt_scheduler_sethook(void (*hook)(struct rt_thread *from, struct rt_thread *to))
在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用
将线程1和线程2的循环次数都设为5次
将线程2的延时也开启,即会主动让出CPU控制权
若在调度空闲线程时按下回车,会从空闲线程跳转到shell线程,然后又立刻跳回空闲线程