嵌入式技术论坛
直播中

马祥

7年用户 774经验值
私信 关注
[经验]

基于STM32L475VET5的RT-Thread线程管理学习

前言

本文主要讲RT-Thread的线程管理,基于STM32L475VET5。学习RT-Thread的线程管理,实现线程的创建、多线程创建,线程的挂起与恢复等,过程中会涉及核对象容器概念。

一、RT-Thread线程管理的功能特点

RT-Thread 线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程和用户线程,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除,每个线程都有重要的属性,如线程控制块、线程栈、入口函数等。


1、RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到 CPU 的使用权。

2、当一个运行着的线程使一个比它优先级高的线程满足运行条件,当前线程的 CPU 使用权就被剥夺了,或者说被让出了,高优先级的线程立刻得到了 CPU 的使用权。

3、如果是中断服务程序使一个高优先级的线程满足运行条件,中断完成时,被中断的线程挂起,优先级高的线程开始运行。

4、当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该线程的上下文信息恢复。

二、RT-Thread线程的状态

1、线程状态

线程运行的过程中,同一时间内只允许一个线程在处理器中运行,从运行的过程上划分,线程有多种不同的运行状态,如初始状态、挂起状态、就绪状态等。

(1)初始状态:当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_INIT。

(2)就绪状态:在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操作系统会马上寻找最高优先级的就绪态线程运行。此状态在 RT-Thread 中的宏定义为RT_THREAD_READY。

(3)运行状态:线程当前正在运行。在单核系统中,只有 rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在 RT-Thread 中的宏定义为RT_THREAD_RUNNING。

(4)挂起状态:也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_SUSPEND。

(5)关闭状态:当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在RT-Thread 中的宏定义为 RT_THREAD_CLOSE。

2、线程状态切换


三、RT-Thread的线程和常用线程函数

1、空闲线程

空闲线程是系统创建的最低优先级的线程,线程状态永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。空闲线程在 RT-Thread 也有着它的特殊用途:若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。空闲线程是唯一不允许出现阻塞情况的线程,因为 RT-Thread需要保证系统用于都有一个可运行的线程,对于空闲线程钩子上挂接的空闲钩子函数,它应该满足以下的条件:(1)不会挂起空闲线程;(2)不应该陷入死循环,需要留出部分时间用于系统处理系统资源回收。

2、主线程

在系统启动时,系统会创建 main 线程,它的入口函数为 main_thread_entry(),用户的应用入口函数 main() 就是从这里真正开始的,系统调度器启动后,main 线程就开始运行,过程如下图,用户可以在main() 函数里添加自己的应用程序初始化代码。


3、动态线程创建函数

动态线程是在程序运行的时候才进行内存的分配,调用这个函数时,系统会从动态堆内存中分配一个线程句柄以及按照参数中指定的栈大小从动态堆内存中分配相应的空间。分配出来的栈空间是按照 rtconfig.h 中配置的 RT_ALIGN_SIZE 方式对齐。


(1)入口参数:

name:线程的名称;线程名称的最大长度由 rtconfig.h 中的宏 RT_NAME_MAX 指定,多余部分会被自动截掉。

entry:线程入口函数。
parameter:线程入口函数参数。
stack_size:线程栈大小,单位是字节。
priority:线程的优先级,范围:0~RT_THREAD_PRIORITY_MAX 。
tick:线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行。

(2)返回值:

thread:线程创建成功,返回线程句柄。
RT_NULL:线程创建失败。

4、删除动态线程函数

当不在需要使用某个动态线程的时候,可通过删除动态线程函数把线程完全删除掉。调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放,收回的空间将重新用于其他的内存分配。实际上,用 rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为 RT_THREAD_CLOSE 状态,然后放入到 rt_thread_defunct 队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作。这个函数是对应动态线程创建函数的。


(1)入口参数:

thread:要删除的线程句柄。

(2)返回值:

RT_EOK:删除线程成功。
RT_ERROR:删除线程失败。

5、静态线程创建函数

静态线程创建函数也就是线程初始化,之所以称为线程初始化时因为静态线程的线程句柄(或者说线程控制块指针)、线程栈由用户提供。静态线程是指线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。需要注意的是,用户提供的栈首地址需做系统对齐(例如 ARM 上需要做 4 字节对齐)。


(1)入口参数:

thread:线程句柄,线程句柄由用户提供出来,并指向对应的线程控制块内存地址。
name:线程的名称;线程名称的最大长度由 rtconfig.h 中定义的 RT_NAME_MAX 宏指定,多余部分会被自动截掉。
entry:线程入口函数。
parameter:线程入口函数参数。
stack_start:线程栈起始地址。
stack_size:线程栈大小,单位是字节,大多数系统中需要做栈空间地址对齐, ARM体系结构中需要向 4 字节地址对齐。
priority:线程的优先级,范围:0~RT_THREAD_PRIORITY_MAX 。
tick:线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度,这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行。

(2)返回值:

RT_EOK:线程创建成功。
RT_ERROR:线程创建失败。

6、删除静态线程函数

当不再需要某个静态线程的时候,可以使用使用 rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。


(1)入口参数:

thread:线程句柄,它应该是由 rt_thread_init 进行初始化的线程句柄。

(2)返回值:

RT_EOK:线程脱离成功。
RT_ERROR:线程脱离失败

7、启动线程函数

前面讲到动态线程的创建和静态线程的创建,那么要想让线程运行起来,还需要启动线程,就像FreeRTOS或UCOS的启动任务调度一样,使用启动线程函数来启动线程调度,当调用启动线程函数时,将把线程的状态更改为就绪状态,并放到相应优先级队列中等待调度。如果新启动的线程优先级比当前线程优先级高,将立刻切换到这个线程。


(1)入口参数:

thread:要启动的线程句柄。

(2)返回值:

RT_EOK:线程启动成功。
RT_ERROR:线程起动失败。

8、挂起线程函数

当线程调用 rt_thread_delay() 时,线程将主动挂起;当调用 rt_sem_take(),rt_mb_recv() 等函数时,资源不可使用也将导致线程挂起。处于挂起状态的线程,如果其等待的资源超时(超过其设定的等待时间),那么该线程将不再等待这些资源,并返回到就绪状态;或者,当其他线程释放掉该线程所等待的资源时,该线程也会返回到就绪状态。


(1)入口参数:

thread:要挂起的线程句柄。

(2)返回值:

RT_EOK:线程挂起成功。
RT_ERROR:线程挂起失败,因为该线程的状态并不是就绪状态。

注意事项:通常不应该使用这个函数来挂起线程本身, 如果确实需要采用rt_thread_suspend()函数挂起当前线程, 需要在调用rt_thread_suspend()函数后立 刻调用rt_schedule() 函数进行手动的线程上下文切换。用户只需要了解该接口的作用,RT-Thread不推荐使用该接口。

9、恢复线程函数

恢复线程就是让挂起的线程重新进入就绪状态,并将线程放入系统的就绪队列中;如果被恢复线程在所有就绪态线程中,位于最高优先级链表的第一位,那么系统将进行线程上下文的切换。


(1)入口参数:

thread:要恢复的线程句柄。

(2)返回值:

RT_EOK:线程恢复成功。
RT_ERROR:线程恢复失败,因为该线程的状态并不是RT_THREAD_SUSPEND状态。

10、使线程让出处理器资源函数

在执行莫一个线程,当该线程需要完成的事情已经完成了,但还有时间片剩余,那么这个时候可以考虑主动要求让出处理器资源,那么可以用使线程让出处理器资源函数,函数如下。


(1)返回值:

RT_EOK:让出处理器资源成功。

(2)调用该函数后,当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优先级队列链表的尾部,然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。

(3)rt_thread_yield() 函数和 rt_schedule() 函数比较相像,但在有相同优先级的其他就绪态线程存在时,系统的行为却完全不一样。执行 rt_thread_yield() 函数后,当前线程被换出,相同优先级的下一个就绪线程将被执行。而执行 rt_schedule() 函数后,当前线程并不一定被换出,即使被换出,也不会被放到就绪线程链表的尾部,而是在系统中选取就绪的优先级最高的线程执行(如果系统中没有比当前线程优先级更高的线程存在,那么执行完 rt_schedule() 函数后,系统将继续执行当前线程)。

四、基于STM32的线程应用示例

前面都是讲了线程的一些概念,接下来,使用RTT&正点原子联合出品的IoT Board潘多拉开发板来进行实际的操作,创建两个线程,一个是动态创建用于实现潘多拉开发板上面的RGB绿灯循环每隔500ms亮后再个500ms灭,一个静态创建用于实现潘多拉开发板的按键控制RGB红灯亮和灭,按下KEY0时RGB红灯亮同时FinSH打印led_on,按下KEY1时RGB红灯灭同时FinSH打印led_off。这里不实现线程删除、脱离、挂起与恢复,请有兴趣的读者自行尝试实现。

1、实现代码

(1)main.c:






(2)key.c:




(3)key.h:


2、FinSH观察结果:

可看到有led线程和key线程,优先级和代码配置的一样,同时我们可以看到线程的stack最大使用率,根据这个,我们就可以在程序中去调节stack的大小,从而节约资源。


五、线程的设计要点

在设计之初就应该考虑一些因素:线程运行的上下文环境、线程的执行时间合理设计。

1、RT-Thread 中程序运行的上下文

(1)中断服务函数:中断服务函数是一种需要特别注意的上下文环境,它运行在非线程的执行环境下(一般为芯片的一种特殊运行模式(也被称作特权模式)),在这个上执行环境中不能使用挂起当前线程的操作,不允许调用任何会阻塞运行的 API 函数接口。另外需要注意的是,中断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发生,让对应线程去执行相关处理,因为中断服务函数的优先级高于任何优先级的线程,如果中断处理时间过长,将会导致整个系统的线程无法正常运行。所以在设计的时候必须考虑中断的频率、中断的处理时间等重要因素,以便配合对应中断处理线程的工作。

(2)普通线程:做为一个优先级明确的实时系统,如果一个线程中的程序出现了死循环操作(此处的死循环是指没有不带阻塞机制的线程循环体),那么比这个线程优先级低的线程都将无法执行,当然也包括了空闲线程,因为死循环的时候,线程不会主动让出 CPU,低优先级的线程是不可能得到 CPU 的使用权的,而高优先级的线程就可以抢占 CPU。这个情况在实时操作系统中是必须注意的一点,所以在线程中不允许出现死循环。如果一个线程只有就绪态而无阻塞态,势必会影响到其他低优先级线程的执行,所以在进行线程设计时,就应该保证线程在不活跃的时候,线程可以进入阻塞态以交出 CPU 使用权,这就需要我们自己明确知道什么情况下让线程进入阻塞态,保证低优先级线程可以正常运行。在实际设计中,一般会将紧急的处理事件的线程优先级设置得高一些。例如上面的KEY线程入口函数key_thread_entry后面加了rt_thread_mdelay(1);,如果不加,会影响FinSH的使用。

(3)空闲线程:请看前面第三章的空闲线程。

2、线程的执行时间

线程的执行时间一般是指两个方面,一是线程从开始到结束的时间,二是线程的周期。在系统设计的时这两个时间候我们都需要考虑,例如,对于事件 A对应的服务线程 Ta,系统要求的实时响应指标是 10ms,而 Ta 的最大运行时间是 1ms,那么 10ms 就是线程 Ta 的周期了,1ms 则是线程的运行时间,简单来说线程 Ta 在 10ms 内完成对事件 A 的响应即可。此时,系统中还存在着以 50ms 为周期的另一线程 Tb,它每次运行的最大时间长度是100us。在这种情况下,即使把线程 Tb的优先级抬到比 Ta更高的位置,对系统的实时性指标也没什么影响,因为即使在 Ta 的运行过程中,Tb 抢占了 Ta 的资源,等到 Tb 执行完毕,消耗的时间也只不过是 100us,还是在事件 A 规定的响应时间内(10ms),Ta 能够安全完成对事件 A的响应。但是假如系统中还存在线程 Tc,其运行时间为 20ms,假如将 Tc的优先级设置比 Ta 更高,那么在 Ta运行的时候,突然间被 Tc打断,等到 Tc执行完毕,那 Ta已经错过对事件 A(10ms)的响应了,这是不允许的。所以在我们设计的时候,必须考虑线程的时间,一般来说处理时间更短的线程优先级应设置更高一些。

更多回帖

发帖
×
20
完善资料,
赚取积分