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 本身,上回我们已经把启动流程讲清楚了,时间太久了,容我贴一下链接:

RT-Thread记录(二、RT-Thread内核启动流程 — 启动文件和源码分析)
上文的最后讲到:整个系统就正常跑起来了,然后用户运行自己想要做的事情,可以在 main 中设计自己的应用代码,或者创建线程。

所以我们接下来当然得说说如何创建线程以及线程的一些操作。

一、RT-Thread线程操作函数

RT-Thread线程操作包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程。

1.1 动态创建线程

函数比较简单,具体的看注释就好(本文余下的函数介绍类似,看注释):

/*
demo,用来接收动态线程返回的句柄
比如 led2_thread  = rt_thread_create(......);
*/
static rt_thread_t led2_thread = RT_NULL; 

#ifdef RT_USING_HEAP      					//定义使用了HEAP才能动态创建线程
/*
参数的含义,放在上面看起来更加方便,要不然太长了
1、线程的名称;线程名称的最大长度由 rtconfig.h 中的宏 RT_NAME_MAX 指定,多余部分会被自动截掉
2、线程入口函数
3、线程入口函数参数,没有就用 RT_NULL
4、线程栈大小,单位是字节
5、线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),
如果支持的是 256 级优先级,那么范围是从 0~255,数值越小优先级越高,0 代表最高优先级
6、线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。
当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。
这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
返回值:
线程创建成功,返回线程句柄
线程创建失败,返回RT_BULL 
*/
rt_thread_t rt_thread_create(const char *name, 
                             void (*entry)(void *parameter),
                             void       *parameter,
                             rt_uint32_t stack_size,
                             rt_uint8_t  priority,
                             rt_uint32_t tick)

1.2 静态创建线程

static struct rt_thread led1_thread;   //demo,用户定义的线程句柄
static char led1_thread_stack[256];    //demo,用户定义的静态线程大小
/*
参数的含义
1、线程句柄。线程句柄由用户提供出来,并指向对应的线程控制块内存地址,上面的led1_thread。
2、线程的名称;线程名称的最大长度由 rtconfig.h 中定义的 RT_NAME_MAX 宏指定,多余部分会被自动截掉
3、线程入口函数
4、线程入口函数参数,没有就用 RT_NULL
5、线程栈起始地址,根据上面定义就是 &led1_thread_stack[0],
6、线程栈大小,单位是字节。根据上面定义就是 sizeof(led1_thread_stack),
在大多数系统中需要做栈空间地址对齐(例如 ARM 体系结构中需要向 4 字节地址对齐)
7、线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),
如果支持的是 256 级优先级,那么范围是从 0~255,数值越小优先级越高,0 代表最高优先级
8、线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。
当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。
这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
返回值:
线程创建成功,返回RT_EOK
线程创建失败,返回RT_ERROR 	
*/
rt_err_t rt_thread_init(struct rt_thread* thread,
                        const char* name,
                        void (*entry)(void* parameter), void* parameter,
                        void* stack_start, 
                        rt_uint32_t stack_size,
                        rt_uint8_t priority, 
                        rt_uint32_t tick);

这里需要说明一下,为什么用户定义一个 char 类型的数组可以作为线程栈空间呢?

因为申请一个全局变量的数组,本质就是开辟了一段连续的内存空间!这是用户申请的,所以在编译的时候就被确定分配好了,这段内存空间申请出来,通过rt_thread_init函数,就分配给了这个线程使用。
如果知道了上面的话,但是还不能理解内存空间和线程有什么关系的时候,这个就得慢慢来……简单来说就是,线程运行需要占用一段内存空间,这段内存空间每个线程的都不一样,他们是用来线程运行的时候,函数调用线程切换保存现场用的。
反正先记住必须给每个线程单独的一片内存空间,RTOS才能正常运行,所有的RTOS都是。
动态创建同样的意思,只不过你看不到,由内核函数自动处理了就没那么直观。

在上面示例代码中,256个char类型的数组,就是占用256个字节(char类型占用1个字节),所以最后分配给线程的空间就是256个字节。

1.3 启动线程

创建完线程并不代表线程就运行了,在RT-Thread称为初始状态,要跑起来需要人为的给他“开”一下,这里与FreeRTOS创建任务后是不同的,FreeRTOS是直接创建完成就开始运行参与调度了。

创建的线程状态处于初始状态,并未进入就绪线程的调度队列,我们可以在线程创建成功后调用rt_thread_startup函数接口让该线程进入就绪态:

/*
static rt_thread_t led2_thread = RT_NULL;
static struct rt_thread led1_thread;
上面的两个demo就是:
rt_thread_startup(&led1_thread);
rt_thread_startup(led2_thread);
*/
rt_err_t rt_thread_startup(rt_thread_t thread);

这里又有一个小细节需要说明一下,动态和静态创建线程的rt_thread_startup使用的小区别!

上面代码的注释中,两个Demo:
一个是rt_thread_startup(&led1_thread);(静态)
一个是rt_thread_startup(led2_thread);(动态)

静态线程为什么需要取地址,动态可以直接用,不仔细看的话还不一定发现这个问题, 其实从他们的定义就已经不同了,只不过rt_thread_trt_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之前,我们需要定义两个东西,一个结构体,一个数组:

static struct rt_thread led1_thread;   //demo,用户定义的线程句柄
static char led1_thread_stack[256];    //demo,用户定义的静态线程大小

在编译的时候,这个结构体和数组,就被分配了一定的内存空间,这段空间默认一般是初始化为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函数,如下:

/*
参数:thread 	要删除的线程句柄
返回值:
RT_EOK 		删除线程成功
-RT_ERROR 	删除线程失败
*/
rt_err_t rt_thread_delete(rt_thread_t thread);

调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放。实际上,用 rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为 RT_THREAD_CLOSE 状态,然后放入到 rt_thread_defunct 队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作。

对于使用rt_thread_init静态创建的线程,我们使用rt_thread_detach函数,如下:

/*
参数:线程句柄,它应该是由 rt_thread_init 进行初始化的线程句柄。
返回值:
RT_EOK 		线程脱离成功
-RT_ERROR  	线程脱离失败
*/
rt_err_t rt_thread_detach (rt_thread_t thread);

官方在介绍rt_thread_detach有一句话,同样,线程本身不应调用这个接口脱离线程本身。这句话我理解就是不管动态删除还是静态删除,不能在线程函数中自己把自己删除。
这里也与FreeRTOS任务后不同,FreeRTOS可以直接在任务中调用函数删除自己。

但是需要特别说明的是,在 RT-Thread 中执行完毕的线程系统会自动将其删除!用户无需多余操作,如何理解呢,看下面的例子:

我们一般线程函数都是死循环,通过延时释放CPU控制权,比如:

static void led1_thread_entry(void *par){
    
    
    while(1){
    
       		
        //do_something
        rt_thread_mdelay(100);
    }
}

我们需要删除的线程往往只是为了做某一件事,某一次特殊的事情,比如:

static void this_is_a_need_delete_task(void *par){
    
     	
  			//do_one_time_thing
}

其实这个线程是为了某一件特殊事情而创建的,它是需要删除的,我们并不需要做任何特殊处理,因为执行是没有循环的,执行完成以后,RT-Thread 内核会自动把线程删除!!

1.5 挂起和恢复线程

线程挂起和恢复,在官方有单独的说明:
在这里插入图片描述既然官方强烈不建议在程序中使用该接口,我们这里就不说明了,因为以应用为主,我们就不去用了。

需要说明的一点是,这里和FreeRTOS也是不同的,FreeRTOS用户可以随意用,最典型的就是使一段代码进入临界区挂起其他任务。

1.6 其他线程辅助函数

其他的线程辅助函数,除了线程睡眠函数,其他的在一般的应用中都可以不需要。所以我们简单的过一遍,引用一下官方的介绍。如果后期应用的时候有用到,再来加以详细说明:

1.6.1 获得当前线程

在程序的运行过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄,把下面的函数加在这段代码中的,哪个线程调用就返回哪个线程句柄:

/*
返回值
thread 	当前运行的线程句柄
RT_NULL 	失败,调度器还未启动
*/
rt_thread_t rt_thread_self(void);

1.6.2 让出处理器资源

rt_err_t rt_thread_yield(void);

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

1.6.3 线程睡眠(延时函数)

线程睡眠,直白点说,就是延时函数,只不过RTOS中的延时函数,是会释放CPU使用权的,释放CPU使用权,就等于线程睡眠了。

/*
参数:tick/ms 	
线程睡眠的时间:sleep/delay 的传入参数 tick 以 1 个 OS Tick 为单位 ;
mdelay 的传入参数 ms 以 1ms 为单位;
返回
RT_EOK 	操作成功,一般不需要
*/
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_t ms);

1.6.4 线程控制函数

/*
参数说明:
1、thread 	线程句柄
2、cmd 	指示控制命令
cmd 当前支持的命令包括:
•RT_THREAD_CTRL_CHANGE_PRIORITY:动态更改线程的优先级;
•RT_THREAD_CTRL_STARTUP:开始运行一个线程,等同于 rt_thread_startup() 函数调用;
•RT_THREAD_CTRL_CLOSE:关闭一个线程,
等同于 rt_thread_delete() 或 rt_thread_detach() 函数调

用。
3、arg 	控制参数
返回值:
RT_EOK 		控制执行正确
-RT_ERROR 	失败
*/
rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);

1.6.5 设置和删除空闲钩子

空闲钩子函数是空闲线程的钩子函数(不要和调度器钩子函数搞混了),如果设置了空闲钩子函数,就可以在系统执行空闲线程时,自动执行空闲钩子函数来做一些其他事情,比如系统指示灯。设置 / 删除空闲钩子的接口如下:

/*
参数:
hook 	设置的钩子函数,在函数中实现一些操作,但是不要有挂起操作
返回值:
RT_EOK 	设置成功
-RT_EFULL 	设置失败
*/
rt_err_t rt_thread_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));

官方有一段注意说明如下:在这里插入图片描述

1.6.6 设置调度器钩子

在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:

/*
参数:
hook 	表示用户定义的钩子函数指针
*/
void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to));
/*
钩子函数 hook() 的声明
参数说明:
1、from 	表示系统所要切换出的线程控制块指针
2、to 	表示系统所要切换到的线程控制块指针
*/
void hook(struct rt_thread* from, struct rt_thread* to);

注:请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个钩子函数中,基本上不允许调用系统 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线程管理。

这一块在官网其实有详细的说明,官方的链接如下:

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线程所有的“属性”,对这些属性的查看,修改就可以对实现对这个线程的管理控制。

我们来看看控制块结构体(不是直接复制官网的哦!):

/**
 * Thread structure
 */
struct rt_thread
{
    
    
    /* rt object */
    char        name[RT_NAME_MAX];        /**< the name of thread 线程名称*/
    rt_uint8_t  type;                     /**< type of object 对象类型*/
    rt_uint8_t  flags;                    /**< thread's flags 标志位*/

#ifdef RT_USING_MODULE
    void       *module_id;                /**< id of application module */
#endif

    rt_list_t   list;                     /**< the object list 对象列表*/
    rt_list_t   tlist;                    /**< the thread list 线程列表*/

    /* stack point and entry 栈指针与入口指针*/
    void       *sp;                       /**< stack point 栈指针*/
    void       *entry;                    /**< entry 入口函数指针*/
    void       *parameter;                /**< parameter 参数*/
    void       *stack_addr;               /**< stack address 栈地址指针 */
    rt_uint32_t stack_size;               /**< stack size 栈大小*/

    /* error code */
    rt_err_t    error;                    /**< error code 线程错误代码*/

    rt_uint8_t  stat;                     /**< thread status 线程状态 */

#ifdef RT_USING_SMP           		 /*多核相关支持,我们这里就一个M3内核*/
    rt_uint8_t  bind_cpu;                /**< thread is bind to cpu */
    rt_uint8_t  oncpu;                   /**< process on cpu` */

    rt_uint16_t scheduler_lock_nest;        /**< scheduler lock count */
    rt_uint16_t cpus_lock_nest;             /**< cpus lock count */
    rt_uint16_t critical_lock_nest;         /**< critical lock count */
#endif /*RT_USING_SMP*/

    /* priority 优先级*/
    rt_uint8_t  current_priority;           /**< current priority 当前优先级 */
    rt_uint8_t  init_priority;              /**< initialized priority 初始优先级 */
#if RT_THREAD_PRIORITY_MAX > 32
    rt_uint8_t  number;
    rt_uint8_t  high_mask;
#endif
    rt_uint32_t number_mask;

#if defined(RT_USING_EVENT)                /*使用事件集*/
    /* thread event */
    rt_uint32_t event_set;
    rt_uint8_t  event_info;
#endif

#if defined(RT_USING_SIGNALS)							
    rt_sigset_t     sig_pending;        /**< the pending signals */
    rt_sigset_t     sig_mask;           /**< the mask bits of signal */

#ifndef RT_USING_SMP					/*多核相关支持,我们这里就一个M3内核*/
    void            *sig_ret;           /**< the return stack pointer from signal */
#endif
    rt_sighandler_t *sig_vectors;       /**< vectors of signal handler */
    void            *si_list;           /**< the signal infor list */
#endif

    rt_ubase_t  init_tick;              /**< thread's initialized tick 线程初始化计数值*/
    rt_ubase_t  remaining_tick;         /**< remaining tick 线程剩余计数值*/

    struct rt_timer thread_timer;       /**< built-in thread timer 内置线程定时器*/

     /**< cleanup function when thread exit 
     线程退出清除函数
     cleanup 函数指针指向的函数,会在线程退出的时候,被idle 线程回调一次,
     执行用户的清理现场工作。
     */
    void (*cleanup)(struct rt_thread *tid);  

    /* light weight process if present */
#ifdef RT_USING_LWP
    void        *lwp;
#endif

    rt_ubase_t user_data;      /**< private user data beyond this thread  用户数据*/
};
typedef struct rt_thread *rt_thread_t;

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 时钟管理的内容,系统时钟,软件定时器相关。

谢谢!

猜你喜欢

转载自blog.csdn.net/weixin_42328389/article/details/123440027