RT-Rhread学习日记之线程
在之前的STM32开发过程中,我一直是使用裸机开发,裸机开发对MCU性能占用低,但是不够灵活,没法真正意义上实现并行,就算实现了简单的调度器,也是非阻塞式的,不够好用。
线程管理的功能特点
RT-Thread 线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程和用户线程,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除,如下图所示,每个线程都有重要的属性,如线程控制块、线程栈、入口函数等。

RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到 CPU 的使用权。
当一个运行着的线程使一个比它优先级高的线程满足运行条件,当前线程的 CPU 使用权就被剥夺了,或者说被让出了,高优先级的线程立刻得到了 CPU 的使用权。
如果是中断服务程序使一个高优先级的线程满足运行条件,中断完成时,被中断的线程挂起,优先级高的线程开始运行。
当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该线程的上下文信息恢复。
线程的工作机制
线程控制块
在 RT-Thread 中,线程控制块由结构体 struct rt_thread 表示,线程控制块是操作系统用于管理线程的一个数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等,详细定义如下:
1 | /* 线程控制块 */ |
其中 init_priority 是线程创建时指定的线程优先级,在线程运行过程当中是不会被改变的(除非用户执行线程控制函数进行手动调整线程优先级)。cleanup 会在线程退出时,被空闲线程回调一次以执行用户设置的清理现场等工作。最后的一个成员 user_data 可由用户挂接一些数据信息到线程控制块中,以提供一种类似线程私有数据的实现方式。
线程重要属性
线程栈
RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。
线程栈还用来存放函数中的局部变量:函数中的局部变量从线程栈空间中申请;函数中局部变量初始时从寄存器中分配(ARM 架构),当这个函数再调用另一个函数时,这些局部变量将放入栈中。
对于线程第一次运行,可以以手工的方式构造这个上下文来设置一些初始的环境:入口函数(PC 寄存器)、入口参数(R0 寄存器)、返回位置(LR 寄存器)、当前机器运行状态(CPSR 寄存器)。
线程栈的增长方向是芯片构架密切相关的,RT-Thread 3.1.0 以前的版本,均只支持栈由高地址向低地址增长的方式,对于 ARM Cortex-M 架构,线程栈可构造如下图所示。

线程栈大小可以这样设定,对于资源相对较大的 MCU,可以适当设计较大的线程栈;也可以在初始时设置较大的栈,例如指定大小为 1K 或 2K 字节,然后在 FinSH 中用 list_thread 命令查看线程运行的过程中线程所使用的栈的大小,通过此命令,能够看到从线程启动运行时,到当前时刻点,线程使用的最大栈深度,而后加上适当的余量形成最终的线程栈大小,最后对栈空间大小加以修改。
关于FinSH,ai给了以下解释
FinSH 是 RT-Thread 实时操作系统中一个非常有用的命令行调试组件。你可以把它想象成嵌入式设备上的一个交互式命令行终端。
FinSH 是什么?
在 RT-Thread 中,FinSH(或者更常被称为 FinSH 控制台或 FinSH Shell)提供了一个与运行在微控制器上的 RT-Thread 系统进行交互的接口。通过这个接口,开发者可以在运行时执行各种命令,查询系统状态,调试程序,甚至调用应用程序中的函数。
就像你在电脑上使用命令提示符(Windows 的 CMD 或 PowerShell)或终端(Linux/macOS 的 Bash)一样,FinSH 让你能够通过串口(通常是 UART)或其他通信接口,向运行在 MCU 上的 RT-Thread 系统发送命令并接收输出。
FinSH 的主要功能和作用
- 系统信息查询:
list_thread: 正如你提到的,这是 FinSH 最常用的命令之一。它可以列出系统中所有线程的信息,包括线程名、状态、优先级、当前已使用的栈大小以及历史最大栈深度等。这对于调试线程栈溢出问题和优化栈空间非常关键。list_mem: 查看内存使用情况。list_sem: 查看信号量信息。list_mq: 查看消息队列信息。version: 查看 RT-Thread 版本信息。
- 动态调试:
- 线程管理: 可以暂停、恢复、启动或停止线程。
- 变量/内存查看: 某些 FinSH 配置允许你查看或修改内存地址上的值。
- 函数调用:
- FinSH 支持注册 C/C++ 函数,然后可以直接在命令行中调用这些函数。这对于测试特定的功能模块或者改变程序运行时的一些参数非常方便,无需重新编译和烧录固件。
- 文件系统操作:
- 如果 RT-Thread 配置了文件系统(如 DFS),FinSH 可以用来执行文件操作命令,比如
ls(列出文件)、cd(切换目录)、mkdir(创建目录)、rm(删除文件) 等。
- 如果 RT-Thread 配置了文件系统(如 DFS),FinSH 可以用来执行文件操作命令,比如
- 设备操作:
- 可以查看或控制通过 RT-Thread 设备框架注册的设备。
为什么 FinSH 对线程栈大小设定很重要?
你引用的这段话,恰恰说明了 FinSH 在线程栈大小优化中的关键作用:
- 初始设定: 在开发初期,你可能对每个线程需要多少栈空间没有准确概念,所以会设置一个相对宽松(大一些)的值,例如 1KB 或 2KB。
- 运行时监测: 当程序在实际环境中运行一段时间后,通过 FinSH 的
list_thread命令,你可以精确地看到每个线程在运行过程中实际使用过的最大栈深度。这个值非常重要,因为它反映了线程在最“忙碌”或调用链最深时的栈使用情况。 - 优化与调整: 根据
list_thread报告的最大栈深度,你可以为线程栈添加一个适当的余量(通常是 10%~20% 或一个固定大小),然后将线程栈大小调整为这个更精确的值。这样既可以避免栈溢出(如果初始值太小),又能节省宝贵的 RAM 资源(如果初始值太大)。
总结
所以,FinSH 就是 RT-Thread 的一个交互式控制台工具,它让你能够在不重新烧录固件的情况下,深入了解和控制运行中的嵌入式系统。对于调试线程栈大小这样的内存优化任务,list_thread 命令通过提供线程的历史最大栈深度,成为了不可或缺的利器。
线程状态
线程运行的过程中,同一时间内只允许一个线程在处理器中运行,从运行的过程上划分,线程有多种不同的运行状态,如初始状态、挂起状态、就绪状态等。在 RT-Thread 中,线程包含五种状态,操作系统会自动根据它运行的情况来动态调整它的状态。 RT-Thread 中线程的五种状态,如下表所示:
| 状态 | 描述 |
|---|---|
| 初始状态 | 当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_INIT |
| 就绪状态 | 在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操作系统会马上寻找最高优先级的就绪态线程运行。此状态在 RT-Thread 中的宏定义为 RT_THREAD_READY |
| 运行状态 | 线程当前正在运行。在单核系统中,只有 rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在 RT-Thread 中的宏定义为 RT_THREAD_RUNNING |
| 挂起状态 | 也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_SUSPEND |
| 关闭状态 | 当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_CLOSE |
线程优先级
RT-Thread 线程的优先级是表示线程被调度的优先程度。每个线程都具有优先级,线程越重要,赋予的优先级就应越高,线程被调度的可能才会越大。
RT-Thread 最大支持 256 个线程优先级 (0~255),数值越小的优先级越高,0 为最高优先级。在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置;对于 ARM Cortex-M 系列,普遍采用 32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。
时间片
每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick),详见《时钟管理》章节。假设有 2 个优先级相同的就绪态线程 A 与 B,A 线程的时间片设置为 10,B 线程的时间片设置为 5,那么当系统中不存在比 A 优先级高的就绪态线程时,系统会在 A、B 线程间来回切换执行,并且每次对 A 线程执行 10 个节拍的时长,对 B 线程执行 5 个节拍的时长。
这个时间节拍应该是来自于SysTick这个外设
线程的入口函数
线程控制块中的 entry 是线程的入口函数,它是线程实现预期功能的函数。线程的入口函数由用户设计实现,一般有以下两种代码形式:
-无限循环模式:
在实时系统中,线程通常是被动式的:这个是由实时系统的特性所决定的,实时系统通常总是等待外界事件的发生,而后进行相应的服务:
大概就是你这个函数的执行完全是由操作系统来决定的,所以内部的while(1)循环并不会真的一直执行下去
1 | void thread_entry(void* paramenter) |
线程看似没有什么限制程序执行的因素,似乎所有的操作都可以执行。但是作为一个实时系统,一个优先级明确的实时系统,如果一个线程中的程序陷入了死循环操作,那么比它优先级低的线程都将不能够得到执行。所以在实时操作系统中必须注意的一点就是:线程中不能陷入死循环操作,**必须要有让出 CPU 使用权的动作,如循环中调用延时函数或者主动挂起。**用户设计这种无限循环的线程的目的,就是为了让这个线程一直被系统循环调度运行,永不删除。
-顺序执行或有限次循环模式:
如简单的顺序语句、do while() 或 for()循环等,此类线程不会循环或不会永久循环,可谓是 “一次性” 线程,一定会被执行完毕。在执行完毕后,线程将被系统自动删除。
1 | static void thread_entry(void* parameter) |
线程错误码
一个线程就是一个执行场景,错误码是与执行环境密切相关的,所以每个线程配备了一个变量用于保存错误码,线程的错误码有以下几种:
1 |
C语言的错误处理还是相对原始,也没有Rust那种传递错误之类的特性,目前来看Rt-Thread也是通过返回错误码来解决错误处理的
线程状态切换

Rt-Thread提供了一系列函数供线程使用
线程通过调用函数 rt_thread_create/init() 进入到初始状态(RT_THREAD_INIT);初始状态的线程通过调用函数 rt_thread_startup() 进入到就绪状态(RT_THREAD_READY);就绪状态的线程被调度器调度后进入运行状态(RT_THREAD_RUNNING);当处于运行状态的线程调用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数或者获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND);处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,如果调用 rt_thread_delete/detach() 函数,将更改为关闭状态(RT_THREAD_CLOSE);而运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit() 函数,将状态更改为关闭状态。
系统线程
前文中已提到,系统线程是指由系统创建的线程,用户线程是由用户程序调用线程管理接口创建的线程,在 RT-Thread 内核中的系统线程有空闲线程和主线程。
空闲线程
空闲线程(idle)是系统创建的最低优先级的线程,线程状态永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。另外,空闲线程在 RT-Thread 也有着它的特殊用途:
若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。
空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合处理功耗管理、看门狗喂狗等工作。空闲线程必须有得到执行的机会,即其他线程不允许一直while(1)死卡,必须调用具有阻塞性质的函数;否则例如线程删除、回收等操作将无法得到正确执行。
主线程
在系统启动时,系统会创建 main 线程,它的入口函数为 main_thread_entry(),用户的应用入口函数 main() 就是从这里真正开始的,系统调度器启动后,main 线程就开始运行,过程如下图,用户可以在 main() 函数里添加自己的应用程序初始化代码。

线程的管理方式
本章前面 2 节对线程的功能与工作机制进行了概念上的讲解,相信大家对线程已经不再陌生。本节将深入到 RT-Thread 线程的各个接口,并给出部分源码,帮助读者在代码层次上理解线程。
下图描述了线程的相关操作,包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程。可以使用 rt_thread_create() 创建一个动态线程,使用 rt_thread_init() 初始化一个静态线程,动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。
类似于在普通windows上拿C语言写东西时,直接声明使用的变量在栈上,通过malloc申请出来的内存空间在堆上

创建和删除线程
一个线程要成为可执行的对象,就必须由操作系统的内核来为它创建一个线程。可以通过如下的接口创建一个动态线程:
1 | rt_thread_t rt_thread_create(const char* name, |
调用这个函数时,系统会从动态堆内存中分配一个线程句柄以及按照参数中指定的栈大小从动态堆内存中分配相应的空间。分配出来的栈空间是按照 rtconfig.h 中配置的 RT_ALIGN_SIZE 方式对齐。线程创建 rt_thread_create() 的参数和返回值见下表:
| 参数 | 描述 |
|---|---|
| name | 线程的名称;线程名称的最大长度由 rtconfig.h 中的宏 RT_NAME_MAX 指定,多余部分会被自动截掉 |
| entry | 线程入口函数 |
| parameter | 线程入口函数参数 |
| stack_size | 线程栈大小,单位是字节 |
| priority | 线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0~255,数值越小优先级越高,0 代表最高优先级 |
| tick | 线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行 |
| 返回 | —— |
| thread | 线程创建成功,返回线程句柄 |
| RT_NULL | 线程创建失败 |
对于栈空间,Ai给了我如下解释
虽然动态线程的内存(包括栈空间)是由系统从动态内存堆中分配的,但系统在分配时依然需要知道具体分配多大的栈空间。这个“多大”就是由 stack_size 参数来决定的。
可以这样理解:
- 动态线程: 你告诉系统“我需要创建一个线程,它的栈要这么大(
stack_size)”。系统就会去动态内存堆上找一块足够大的内存来作为这个线程的栈。 - 静态线程: 你自己预先准备好一块内存,然后告诉系统“这是我的线程,它的栈就在这里,大小是这么多(
stack_size)”。
所以,无论是动态线程还是静态线程,stack_size 都是一个必不可少的参数,因为它定义了线程运行时的栈空间上限,以防止栈溢出并帮助系统进行资源管理。两者的区别仅在于这块栈内存是由谁来分配和管理:动态线程由系统动态分配,静态线程由用户静态分配。
对于一些使用 rt_thread_create() 创建出来的线程,当不需要使用,或者运行出错时,我们可以使用下面的函数接口来从系统中把线程完全删除掉:
1 | rt_err_t rt_thread_delete(rt_thread_t thread); |
调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放,收回的空间将重新用于其他的内存分配。实际上,用 rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为 RT_THREAD_CLOSE 状态,然后放入到 rt_thread_defunct 队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作。线程删除 rt_thread_delete() 接口的参数和返回值见下表:
| 参数 | 描述 |
|---|---|
| thread | 要删除的线程句柄 |
| 返回 | —— |
| RT_EOK | 删除线程成功 |
| -RT_ERROR | 删除线程失败 |
Note
注:rt_thread_create() 和 rt_thread_delete() 函数仅在使能了系统动态堆时才有效(即 RT_USING_HEAP 宏定义已经定义了)。
初始化和脱离线程
线程的初始化可以使用下面的函数接口完成,来初始化静态线程对象:
1 | rt_err_t rt_thread_init(struct rt_thread* thread, |
静态线程的线程句柄(或者说线程控制块指针)、线程栈由用户提供。静态线程是指线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。需要注意的是,用户提供的栈首地址需做系统对齐(例如 ARM 上需要做 4 字节对齐)。线程初始化接口 rt_thread_init() 的参数和返回值见下表:
| 参数 | 描述 |
|---|---|
| thread | 线程句柄。线程句柄由用户提供出来,并指向对应的线程控制块内存地址 |
| name | 线程的名称;线程名称的最大长度由 rtconfig.h 中定义的 RT_NAME_MAX 宏指定,多余部分会被自动截掉 |
| entry | 线程入口函数 |
| parameter | 线程入口函数参数 |
| stack_start | 线程栈起始地址 |
| stack_size | 线程栈大小,单位是字节。在大多数系统中需要做栈空间地址对齐(例如 ARM 体系结构中需要向 4 字节地址对齐) |
| priority | 线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0 ~ 255,数值越小优先级越高,0 代表最高优先级 |
| tick | 线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行 |
| 返回 | —— |
| RT_EOK | 线程创建成功 |
| -RT_ERROR | 线程创建失败 |
对于用 rt_thread_init() 初始化的线程,使用 rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。线程脱离函数如下:
1 | rt_err_t rt_thread_detach (rt_thread_t thread); |
线程脱离接口 rt_thread_detach() 的参数和返回值见下表:
| 参数 | 描述 |
|---|---|
| thread | 线程句柄,它应该是由 rt_thread_init 进行初始化的线程句柄。 |
| 返回 | —— |
| RT_EOK | 线程脱离成功 |
| -RT_ERROR | 线程脱离失败 |
这个函数接口是和 rt_thread_delete() 函数相对应的, rt_thread_delete() 函数操作的对象是 rt_thread_create() 创建的句柄,而 rt_thread_detach() 函数操作的对象是使用 rt_thread_init() 函数初始化的线程控制块。同样,线程本身不应调用这个接口脱离线程本身。
就是不能自己删自己的意思
启动线程
创建(初始化)的线程状态处于初始状态,并未进入就绪线程的调度队列,我们可以在线程初始化 / 创建成功后调用下面的函数接口让该线程进入就绪态:
1 | rt_err_t rt_thread_startup(rt_thread_t thread); |
当调用这个函数时,将把线程的状态更改为就绪状态,并放到相应优先级队列中等待调度。如果新启动的线程优先级比当前线程优先级高,将立刻切换到这个线程。线程启动接口 rt_thread_startup() 的参数和返回值见下表:
| 参数 | 描述 |
|---|---|
| thread | 线程句柄 |
| 返回 | —— |
| RT_EOK | 线程启动成功 |
| -RT_ERROR | 线程启动失败 |
获得当前线程
在程序的运行过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄:
1 | rt_thread_t rt_thread_self(void); |
该接口的返回值见下表:
| 返回 | 描述 |
|---|---|
| thread | 当前运行的线程句柄 |
| RT_NULL | 失败,调度器还未启动 |
使线程让出处理器资源
当前线程的时间片用完或者该线程主动要求让出处理器资源时,它将不再占有处理器,调度器会选择相同优先级的下一个线程执行。线程调用这个接口后,这个线程仍然在就绪队列中。线程让出处理器使用下面的函数接口:
1 | rt_err_t rt_thread_yield(void); |
调用该函数后,当前线程会把自己挂到这个优先级队列链表的尾部,然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。
rt_thread_yield() 函数和 rt_schedule() 函数比较相像,但有相同优先级的其他就绪态线程存在且没有更高优先级的线程存在时,系统的行为却完全不一样。执行 rt_thread_yield() 函数后,当前线程肯定会被换出,相同优先级的下一个就绪线程将被执行。而执行 rt_schedule() 函数后,当前线程并不一定被换出,即使被换出,也不会被放到就绪线程链表的尾部。
注:rt_schedule()是更加底层的函数,rt_thread_yield()还做了上锁,修改当前线程状态的工作,最后调用了rt_schedule() 。rt_thread_yield()是面向用户的接口,rt_schedule()是内核实现调度的内部接口。一般情况下应调用rt_thread_yield()。
使线程睡眠
在实际应用中,我们有时需要让运行的当前线程延迟一段时间,在指定的时间到达后重新运行,这就叫做 “线程睡眠”。线程睡眠可使用以下三个函数接口:
1 | rt_err_t rt_thread_sleep(rt_tick_t tick); |
这三个函数接口的作用相同,调用它们可以使当前线程挂起一段指定的时间,当这个时间过后,线程会被唤醒并再次进入就绪状态。这个函数接受一个参数,该参数指定了线程的休眠时间。线程睡眠接口 rt_thread_sleep/delay/mdelay() 的参数和返回值见下表:
| 参数 | 描述 |
|---|---|
| tick/ms | 线程睡眠的时间: sleep/delay 的传入参数 tick 以 1 个 OS Tick 为单位 ; mdelay 的传入参数 ms 以 1ms 为单位; |
| 返回 | —— |
| RT_EOK | 操作成功 |
挂起和恢复线程
当线程调用 rt_thread_delay() 时,线程将主动挂起;当调用 rt_sem_take(),rt_mb_recv() 等函数时,资源不可使用也将导致线程挂起。处于挂起状态的线程,如果其等待的资源超时(超过其设定的等待时间),那么该线程将不再等待这些资源,并返回到就绪状态;或者,当其他线程释放掉该线程所等待的资源时,该线程也会返回到就绪状态。
rt_thread_mdelay()内部已经包含了线程挂起和调度器的激活操作,所以您无需手动调用rt_schedule()。
线程挂起使用下面的函数接口:
1 | rt_err_t rt_thread_suspend (rt_thread_t thread); |
线程挂起接口 rt_thread_suspend() 的参数和返回值见下表:
| 参数 | 描述 |
|---|---|
| thread | 线程句柄 |
| 返回 | —— |
| RT_EOK | 线程挂起成功 |
| -RT_ERROR | 线程挂起失败,因为该线程的状态并不是就绪状态 |
注:一个线程尝试挂起另一个线程是一个非常危险的行为,因此RT-Thread对此函数有严格的使用限制:该函数只能使用来挂起当前线程(即自己挂起自己),不可以在线程A中尝试挂起线程B。而且在挂起线程自己后,需要立刻调用
rt_schedule()函数进行手动的线程上下文切换。这是因为A线程在尝试挂起B线程时,A线程并不清楚B线程正在运行什么程序,一旦B线程正在使用例如互斥量、信号量等影响、阻塞其他线程(如C线程)的内核对象,如果此时其他线程也在等待这个内核对象,那么A线程尝试挂起B线程的操作将会引发其他线程(如C线程)的饥饿,严重危及系统的实时性。
恢复线程就是让挂起的线程重新进入就绪状态,并将线程放入系统的就绪队列中;如果被恢复线程在所有就绪态线程中,位于最高优先级链表的第一位,那么系统将进行线程上下文的切换。线程恢复使用下面的函数接口:
1 | rt_err_t rt_thread_resume (rt_thread_t thread); |
线程恢复接口 rt_thread_resume() 的参数和返回值见下表:
| 参数 | 描述 |
|---|---|
| thread | 线程句柄 |
| 返回 | —— |
| RT_EOK | 线程恢复成功 |
| -RT_ERROR | 线程恢复失败,因为该个线程的状态并不是 RT_THREAD_SUSPEND 状态 |
控制线程
当需要对线程进行一些其他控制时,例如动态更改线程的优先级,可以调用如下函数接口:
1 | rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg); |
线程控制接口 rt_thread_control() 的参数和返回值见下表:
| 函数参数 | 描述 |
|---|---|
| thread | 线程句柄 |
| cmd | 指示控制命令 |
| arg | 控制参数 |
| 返回 | —— |
| RT_EOK | 控制执行正确 |
| -RT_ERROR | 失败 |
指示控制命令 cmd 当前支持的命令包括:
- RT_THREAD_CTRL_CHANGE_PRIORITY:动态更改线程的优先级;
- RT_THREAD_CTRL_STARTUP:开始运行一个线程,等同于 rt_thread_startup() 函数调用;
- RT_THREAD_CTRL_CLOSE:关闭一个线程,等同于 rt_thread_delete() 或 rt_thread_detach() 函数调用。
关于钩子函数,Ai给了我们如下解释
在 RT-Thread 中,钩子函数(Hook Function) 是一种特殊的机制,它允许你在操作系统内核的特定事件发生时,插入并执行自己的代码。
可以把它想象成在系统的“关键时刻”预留的“插座”,当这个时刻到来时,系统就会自动调用你插入的“插头”(也就是你的钩子函数)。
钩子函数的作用
钩子函数的主要作用是:
- 监控和调试:你可以通过钩子函数来实时监控线程的运行状态、内存使用情况、调度器的行为等。例如,你可以在线程创建、删除、切换时执行代码,以便进行性能分析或记录日志。
- 扩展和定制:如果你想在 RT-Thread 的标准行为之外增加一些自定义功能,钩子函数是一个很好的方法。比如,在线程挂起时自动保存一些数据,或者在空闲时执行一些低功耗操作。
- 统计和分析:利用钩子函数,你可以轻松地统计线程的 CPU 占用率、上下文切换次数等关键数据,为系统的优化提供依据。
常见的钩子函数类型
RT-Thread 提供了多种钩子函数,覆盖了系统运行的多个重要环节:
线程钩子:用于监控线程的生命周期,例如:
rt_thread_inited_hook:在线程初始化时调用。rt_thread_suspend_hook:在线程挂起时调用。rt_thread_resume_hook:在线程恢复时调用。
调度器钩子:用于监控线程切换,例如:
rt_scheduler_hook:在调度器进行线程上下文切换时调用。
空闲线程钩子:用于在系统无事可做时执行任务,例如:
rt_thread_idle_hook:在空闲线程运行时调用。这通常是实现低功耗或执行后台任务的好地方。
这是一个钩子点,rt-thread并不提供一个默认的空闲钩子函数,你需要自己注册一个才能使用
如何使用钩子函数?
使用钩子函数非常简单,通常分为两步:
- 编写钩子函数:根据你的需求,编写一个符合 RT-Thread 要求的 C 函数。
- 注册钩子函数:调用 RT-Thread 提供的注册接口,将你的函数注册到特定的钩子点上。例如,使用
rt_scheduler_sethook()来注册调度器钩子。
总之,钩子函数是一种非常强大和灵活的机制,它让你可以深入到 RT-Thread 内核,实现高级的监控、调试和功能扩展,而无需修改内核源代码。
设置和删除空闲钩子
空闲钩子函数是空闲线程的钩子函数,如果设置了空闲钩子函数,就可以在系统执行空闲线程时,自动执行空闲钩子函数来做一些其他事情,比如系统指示灯。设置 / 删除空闲钩子的接口如下:
1 | rt_err_t rt_thread_idle_sethook(void (*hook)(void)); |
设置空闲钩子函数 rt_thread_idle_sethook() 的输入参数和返回值如下表所示:
| 函数参数 | 描述 |
|---|---|
| hook | 设置的钩子函数 |
| 返回 | —— |
| RT_EOK | 设置成功 |
| -RT_EFULL | 设置失败 |
删除空闲钩子函数 rt_thread_idle_delhook() 的输入参数和返回值如下表所示:
| 函数参数 | 描述 |
|---|---|
| hook | 删除的钩子函数 |
| 返回 | —— |
| RT_EOK | 删除成功 |
| -RT_ENOSYS | 删除失败 |
注:空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态,例如 rt_thread_delay(),rt_sem_take() 等可能会导致线程挂起的函数都不能使用。并且,由于 malloc、free 等内存相关的函数内部使用了信号量作为临界区保护,因此在钩子函数内部也不允许调用此类函数!
设置调度器钩子
在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:
1 | void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to)); |
设置调度器钩子函数的输入参数如下表所示:
| 函数参数 | 描述 |
|---|---|
| hook | 表示用户定义的钩子函数指针 |
钩子函数 hook() 的声明如下:
1 | void hook(struct rt_thread* from, struct rt_thread* to); |
调度器钩子函数 hook() 的输入参数如下表所示:
| 函数参数 | 描述 |
|---|---|
| from | 表示系统所要切换出的线程控制块指针 |
| to | 表示系统所要切换到的线程控制块指针 |
Note
注:请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个钩子函数中,基本上不允许调用系统 API,更不应该导致当前运行的上下文挂起)。
线程应用示例
下面给出在 Keil 模拟器环境下的应用示例。
创建线程示例
这个例子会创建一个动态线程和一个静态线程,当这个静态线程完成其任务并被系统自动回收后,具有较低优先级的动态线程才能开始运行并打印信息。
注意:RT-Thread 5.0 及更高的版本将
ALIGN关键字改成了rt_align,使用时注意。
1 |
|
仿真运行结果如下:
1 | \ | / |
线程 2 计数到一定值会执行完毕,线程 2 被系统自动删除,计数停止。线程 1 才会打印计数。
注:关于删除线程:大多数线程是循环执行的,无需删除;而能运行完毕的线程,RT-Thread 在线程运行完毕后,自动删除线程,在 rt_thread_exit() 里完成删除动作。用户只需要了解该接口的作用,不推荐使用该接口(可以由其他线程调用此接口或在定时器超时函数中调用此接口删除一个线程,但是这种使用非常少)。
线程时间片轮转调度示例
这个例子创建两个线程,在执行时会一直打印计数,如下代码:
1 |
|
仿真运行结果如下:
1 | \ | / |
由运行的计数结果可以看出,线程 2 的运行时间是线程 1 的一半。
线程调度器钩子示例
在线程进行调度切换时,会执行调度,我们可以设置一个调度器钩子,这样可以在线程切换时,做一些额外的事情,这个例子是在调度器钩子函数中打印线程间的切换信息,如下代码:
注意:RT-Thread5.0 及更高版本将
struct rt_thread结构体的 name 成员移到了 parent 里,使用时代码需要由thread->name更改为thread->parent.name,否则编译会报错!
1 |
|
static void hook_of_scheduler(struct rt_thread* from, struct rt_thread* to);
调度器钩子函数的返回值跟参数列表是写死的,但是函数名应该是可以改的。
仿真运行结果如下:
1 | \ | / |
由仿真的结果可以看出,对线程进行切换时,设置的调度器钩子函数是在正常工作的,一直在打印线程切换的信息,包含切换到空闲线程。可以使用scheduler_del取消调度器的钩子函数。