既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新
需要这些体系化资料的朋友,可以加我V获取:vip1024c (备注嵌入式)
/\*
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_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
之前,我们需要定义两个东西,一个结构体,一个数组:
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线程管理。
这一块在官网其实有详细的说明,官方的链接如下:
3.1 线程调度的基本特点
我这边按照自己的理解认知记录几个重要的点:
1、RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到 CPU 的使用权。
调度器开启以后,就不停的在查询列表,所有的线程根据优先级,状态,在列表中排序,调度器总是找到排序“第一位”的线程执行。RTOS的核心就是链表,这个有时间会单独的介绍。
2、当一个运行着的线程使一个比它优先级高的线程满足运行条件,当前线程的 CPU 使用权就被剥夺了,或者说被让出了,高优先级的线程立刻得到了 CPU 的使用权。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新
需要这些体系化资料的朋友,可以加我V获取:vip1024c (备注嵌入式)
链表,这个有时间会单独的介绍。
2、当一个运行着的线程使一个比它优先级高的线程满足运行条件,当前线程的 CPU 使用权就被剥夺了,或者说被让出了,高优先级的线程立刻得到了 CPU 的使用权。
[外链图片转存中…(img-RcfhkprL-1715903021621)]
[外链图片转存中…(img-1eUjB1oK-1715903021621)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新
需要这些体系化资料的朋友,可以加我V获取:vip1024c (备注嵌入式)