RT-Thread 记录(三、RT-Thread 线程操作函数及线程管理与 FreeRTOS 的比较)
前 2 课讲完了 RT-Thread 开发环境,启动流程,启动以后当然是开始跑线程了,
那么自然我们得学会如何创建线程以及线程的有关操作。
前言
前段时间写完 RT-Thread 版本,开发环境,启动流程后,停了好一段时间,因为完成了前面 2 课的讲解,感觉直接用起来都问题不大了,为啥,因为 RTOS 的调度,线程通讯等机制,学习过 FreeRTOS,看看 RT-Thread 官方的文档说明,很多东西就很清楚了= =!以至于在写本文的时候,都感觉,是不是太简单了?
但是后来又想了想:
1、本系列博文的目的在于总结记录,为的是以后在项目中使用起来,我可以直接参考自己总结的博文,而不是去翻官方的文档资料。
2、尽量使得没有学习过 RT-Thread 的同学根据系列博文能够对 RT-Thread 有个认识,然后在一些细节的点上面有一定的理解,同时在遇到 RT-Thread 与 FreeRTOS 不同的地方,会加以说明。
3、当初的 FreeRTOS 系列,真就是很随意的按照自己学习测试的流程来走,对小白来说并不友好,回头看起来,虽然我是真的画了精力和事件去说明遇到的问题以及解决办法,但是少了循序渐进的过程,整体也没有一个好的框架体系,所以好像没有帮到太多人(看的人不多哈= =!)。所以在 RT-Thread 系列上面,该系统的还是得系统起来,即便有些东西简单基础,官方和网上文档详细,当做一个笔记,该记录的还是得记录!
好的,题外话说到这里,我们回到 RT-Thread 本身,上回我们已经把启动流程讲清楚了,上文的最后讲到:整个系统就正常跑起来了,然后用户运行自己想要做的事情,可以在 main 中设计自己的应用代码,或者创建线程。
所以我们接下来当然得说说如何创建线程以及线程的一些操作。
本 RT-Thread 专栏记录的开发环境:
RT-Thread记录(一、RT-Thread 版本、RT-Thread Studio开发环境 及 配合CubeMX开发快速上手)
一、RT-Thread 线程操作函数
RT-Thread 线程操作包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程。
1.1 动态创建线程
函数比较简单,具体的看注释就好(本文余下的函数介绍类似,看注释):
1.2 静态创建线程
这里需要说明一下,为什么用户定义一个 char 类型的数组可以作为线程栈空间呢?
因为申请一个全局变量的数组,本质就是开辟了一段连续的内存空间!这是用户申请的,所以在编译的时候就被确定分配好了,这段内存空间申请出来,通过rt_thread_init
函数,就分配给了这个线程使用。
如果知道了上面的话,但是还不能理解内存空间和线程有什么关系的时候,这个就得慢慢来……简单来说就是,线程运行需要占用一段内存空间,这段内存空间每个线程的都不一样,他们是用来线程运行的时候,函数调用线程切换保存现场用的。
反正先记住必须给每个线程单独的一片内存空间,RTOS 才能正常运行,所有的 RTOS 都是。
动态创建同样的意思,只不过你看不到,由内核函数自动处理了就没那么直观。
在上面示例代码中,256 个 char 类型的数组,就是占用 256 个字节(char 类型占用 1 个字节),所以最后分配给线程的空间就是 256 个字节。
1.3 启动线程
创建完线程并不代表线程就运行了,在 RT-Thread 称为初始状态,要跑起来需要人为的给他“开”一下,这里与 FreeRTOS 创建任务后是不同的,FreeRTOS 是直接创建完成就开始运行参与调度了。
创建的线程状态处于初始状态,并未进入就绪线程的调度队列,我们可以在线程创建成功后调用rt_thread_startup
函数接口让该线程进入就绪态:
这里又有一个小细节需要说明一下,动态和静态创建线程的rt_thread_startup
使用的小区别!
上面代码的注释中,两个 Demo:
一个是rt_thread_startup(&led1_thread);
(静态)
一个是rt_thread_startup(led2_thread);
(动态)
静态线程为什么需要取地址,动态可以直接用,不仔细看的话还不一定发现这个问题,其实从他们的定义就已经不同了,只不过rt_thread_t
和rt_thread
一眼看去还真可能傻傻分不清楚 = =!以前我刚用的时候也在这里迷糊了一会:
static struct rt_thread led1_thread
静态类型为struct rt_thread
类型就是线程控制块结构体
static rt_thread_t led2_thread
动态类型为rt_thread_t
类型是一个指针,
如下解释:
rt_thread_t
这个类型他是经过 typedef 重名命的:
所以回到开始的问题,搞清楚了rt_thread_startup
函数的参数是线程控制块结构体指针, 再结合动态静态创建线程的线程句柄定义,这么问题就清楚了!明白了这个,那么这里又可以说明一个细节问题!如下
线程创建的一个细节—创建和初始化?
在文中,我介绍 API 使用的标题是“动态创建线程” 和“静态创建线程”,个人认为看上去好理解,也没问题,但是这里注意官方的用语:
动态是 -- 创建和删除线程
静态是 -- 初始化和脱离线程
说白了都是新建线程,但是用词却不一样,为什么动态用创建,而静态用初始化呢?带着疑问我们回头再去看看两种方式的不同。在使用rt_thread_init
之前,我们需要定义两个东西,一个结构体,一个数组:
在编译的时候,这个结构体和数组,就被分配了一定的内存空间,这段空间默认一般是初始化为 0,就是空间给你留着了,但是等着你去放数据。不管在程序后面使不使用rt_thread_init
,这段空间都已经存在了的! 这样来说,调用rt_thread_init
只是对已经存在的一段内存空间的赋值,对一个存在的东西的设置,不就是叫做 初始化吗。所以使用静态的创建严格的来说,更应该称之为初始化线程!
而在使用rt_thread_create
之前,我们只需要定义一个rt_thread_t
类型的指针,初始化是NULL
就没有了,只有在调用rt_thread_create
成功之后,才会开辟出一块存放线程控制块的内存空间,从无到有的一个过程,所以叫做 创建。
不得不佩服,官方还是用词严谨,其实想想也能更好的理解函数功能!
句柄是什么?
讲到这里,为了让有些小伙伴更容易看懂,我们再插一个细节,我们经常听到返回句柄,函数句柄,任务句柄,那么句柄是什么?
记住一句话:句柄其实就是指针,它是指向指针的指针。
在我们的rt_thread_create
函数中,如果成功返回值是 线程句柄,类型为rt_thread_t
,我们前面又讲过rt_thread_t
是一个结构体指针,这个结构体是线程控制块结构体,所以 在上面示例代码中返回句柄的意思 ,就是返回了一个指针,这个指针指向线程控制块。
(如果指针,指向指针的指针不明白,这是 C 语言基础知识,可以查看相关资料,我有一篇博文也提到过一二:C语言学习点滴笔记 中 4、指针: 一种特殊的变量 和 多元指针,指向指针的指针)
1.4 删除线程和脱离线程
针对上面动态静态方法创建的线程,RT-Thread 有不同的删除函数:对于使用rt_thread_create
动态创建的线程,我们使用rt_thread_delete
函数,如下:
调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放。实际上,用 rt_thread_delete()
函数删除线程接口,仅仅是把相应的线程状态更改为 RT_THREAD_CLOSE
状态,然后放入到 rt_thread_defunct
队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作。
对于使用rt_thread_init
静态创建的线程,我们使用rt_thread_detach
函数,如下:
官方在介绍rt_thread_detach
有一句话,同样,线程本身不应调用这个接口脱离线程本身。这句话我理解就是不管动态删除还是静态删除,不能在线程函数中自己把自己删除。
这里也与 FreeRTOS 任务后不同,FreeRTOS 可以直接在任务中调用函数删除自己。
但是需要特别说明的是,在 RT-Thread 中执行完毕的线程系统会自动将其删除!用户无需多余操作,如何理解呢,看下面的例子:
我们一般线程函数都是死循环,通过延时释放 CPU 控制权,比如:
我们需要删除的线程往往只是为了做某一件事,某一次特殊的事情,比如:
其实这个线程是为了某一件特殊事情而创建的,它是需要删除的,我们并不需要做任何特殊处理,因为执行是没有循环的,执行完成以后,RT-Thread 内核会自动把线程删除!!
1.5 挂起和恢复线程
线程挂起和恢复,在官方有单独的说明:
既然官方强烈不建议在程序中使用该接口,我们这里就不说明了,因为以应用为主,我们就不去用了。
需要说明的一点是,这里和 FreeRTOS 也是不同的,FreeRTOS 用户可以随意用,最典型的就是使一段代码进入临界区挂起其他任务。
1.6 其他线程辅助函数
其他的线程辅助函数,除了线程睡眠函数,其他的在一般的应用中都可以不需要。所以我们简单的过一遍,引用一下官方的介绍。如果后期应用的时候有用到,再来加以详细说明:
1.6.1 获得当前线程
在程序的运行过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄,把下面的函数加在这段代码中的,哪个线程调用就返回哪个线程句柄:
1.6.2 让出处理器资源
调用该函数后,当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优先级队列链表的尾部,然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。
1.6.3 线程睡眠(延时函数)
线程睡眠,直白点说,就是延时函数,只不过 RTOS 中的延时函数,是会释放 CPU 使用权的,释放 CPU 使用权,就等于线程睡眠了。
1.6.4 线程控制函数
1.6.5 设置和删除空闲钩子
空闲钩子函数是<font color=#FF0033>空闲线程的钩子函数</font>(不要和调度器钩子函数搞混了),如果设置了空闲钩子函数,就可以在系统执行空闲线程时,自动执行空闲钩子函数来做一些其他事情,比如系统指示灯。设置 / 删除空闲钩子的接口如下:
官方有一段注意说明如下:
1.6.6 设置调度器钩子
在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:
注:请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个钩子函数中,基本上不允许调用系统 API,更不应该导致当前运行的上下文挂起)。
二、RT-Thread 线程创建示例
虽然上面介绍了有一部分的线程操作函数,但是正常需要也就前面几个,记住线程创建,启动,一般的应用就足够了,其他的一些辅助函数在实际中有很多情况是出了问题以后找 bug 的时候才会想起来。
所以我们演示起来也很简单,还记得在 RT-Thread 记录 第一篇博文中:
RT-Thread记录(一、RT-Thread 版本、RT-Thread Studio开发环境 及 配合CubeMX开发快速上手)
在上面博文的最后一节:3.3 创建一个跑马灯任务 我上传了一段源码,这里我就不再重复上一边了,我们直接通过截图说明的方式讲解下示例:
2.1 静态创建线程示例
2.1 动态创建线程示例
三、RT-Thread 线程管理简析
经过上面的说明,我们其实能够使用 RT-Thread 对于的函数创建线程进行一般的设计了,但是为了加深对 RT-Thread 的理解,我们还得聊聊 RT-Thread 线程管理。
这一块在官网其实有详细的说明,官方的链接如下:
3.1 线程调度的基本特点
我这边按照自己的理解认知记录几个重要的点:
1、RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到 CPU 的使用权。
调度器开启以后,就不停的在查询列表,所有的线程根据优先级,状态,在列表中排序,调度器总是找到排序“第一位”的线程执行。RTOS 的核心就是链表,这个有时间会单独的介绍。
2、当一个运行着的线程使一个比它优先级高的线程满足运行条件,当前线程的 CPU 使用权就被剥夺了,或者说被让出了,高优先级的线程立刻得到了 CPU 的使用权。
如果是中断服务程序使一个高优先级的线程满足运行条件,中断完成时,被中断的线程挂起,优先级高的线程开始运行。
还是上面说到的调度器的作用,使得高优先级的能够及时执行。
3、当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该线程的上下文信息恢复。RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。
要理解上面的话,推荐一篇博文:FreeRTOS记录(三、FreeRTOS任务调度原理解析_Systick、PendSV、SVC)
虽然说的是 FreeRTOS 的,但是都是基于 Cortex-M 内核的,原理机制类似。
4、每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。
时间片只有在优先级相同的线程间会根据用户的设置进行对应的分配。
5、线程中不能陷入死循环操作,必须要有让出 CPU 使用权的动作,如循环中调用延时函数或者主动挂起。
使用 rtos 延时函数,是实际使用最常见的一种方式,切记,delay 是需要在 while(1){}大括号里面的:
3.2 线程控制块
在我们上面介绍线程操作函数的时候,经常提到一个词语,线程控制块,线控控制块结构体,RT-Thread 内核对于线程的管理,都是基于这个结构体进行的。这里我们先有个基本的认识,如果真的深入探讨,还是要说到 RTOS 的链表,需要单独的开篇博文说明。
我们现在要了解的是,内核对于线程的管理是通过这个线程控制块结构体,里面包括 RT-Thread 线程所有的“属性”,对这些属性的查看,修改就可以对实现对这个线程的管理控制。
我们来看看控制块结构体(不是直接复制官网的哦!):
3.3 线程状态
线程的状态我们借用官方的几张图,加以说明:
来看看 RT-Thread 的任务状态:
在上图中除了今天我们介绍的线程操作函数,还有一些函数还没有介绍过,比如rt_sem_take(),rt_mutex_take(),rt_mb_recv()
,这是我们后期会介绍到的关于线程间通信的一些信号量,互斥量相关的函数。
作为对比,再来看看 FreeRTOS 的任务状态:
3.4 系统线程
在 RT-Thread 内核中的系统线程有空闲线程和主线程。
空闲线程 IDLE 线程:
空闲线程是系统创建的最低优先级的线程,线程状态永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。这点其实所有 RTOS 都是一样的。
但是,空闲线程在 RT-Thread 也有着它的特殊用途:
若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。
空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。
主线程:
在我们上一篇博文中介绍 RT-Thread 启动流程的时候,说到了系统启动会创建 main 线程:
FreeRTOS 只有空闲线程,并不会创建主线程,所以在 FreeRTOS 中,一般在 main() 之前开启调度,永远不会执行到 main()。
结语
本文的主要目的是认识 RT-Thread 线程操作函数,同时简单的说明了一下 RT-Thread 线程管理的一些要点,说明了一下 RT-Thread 与 FreeRTOS 在线程操作某些地方的不同,此外还加了一些博主认为的细节的问题, 希望懂的小伙伴可以多多指教,不懂的小伙伴看完还是不明白的可以留言。讲得不好的地方还希望能够指出,博主一定加以修正。
总的来说,本文内容还是比较简单的,小伙伴们可以开动起来,线程创建跑起来玩玩。优先级,任务调度,线程死循环什么的情况都可以试试。更能加加深线程调度的理解。
下一篇 RT-Thread 记录,我会讲一讲 RT-Thread 时钟管理的内容,系统时钟,软件定时器相关。
谢谢!
版权声明: 本文为 InfoQ 作者【矜辰所致】的原创文章。
原文链接:【http://xie.infoq.cn/article/1d2e8e030ae689d6b8ee44b05】。文章转载请联系作者。
评论