RT-Thread core

RT-Thread 内核介绍

在这里插入图片描述

RT-Thread 启动流程

在这里插入图片描述

RT-Thread 程序内存分布

程序运行之前,需要有文件实体被烧录到 STM32 的 Flash 中,一般是 bin 或者 hex 文件,该被烧录文件称为可执行映像文件。如图 3-3 中左图所示,是可执行映像文件烧录到 STM32 后的内存分布,它包含 RO 段和 RW 段两个部分:其中 RO 段中保存了 Code、RO-data 的数据,RW 段保存了 RW-data 的数据,由于 ZI-data 都是 0,所以未包含在映像文件中。

STM32 在上电启动之后默认从 Flash 启动,启动之后会将 RW 段中的 RW-data(初始化的全局变量)搬运到 RAM 中,但不会搬运 RO 段,即 CPU 的执行代码从 Flash 中读取,另外根据编译器给出的 ZI 地址和大小分配出 ZI 段,并将这块 RAM 区域清零。在这里插入图片描述
Keil 工程在编译完之后,会有相应的程序所占用的空间提示信息,如下所示:

linking…
Program Size: Code=48008 RO-data=5660 RW-data=604 ZI-data=2124
After Build - User command #1: fromelf --bin.\build\rtthread-stm32.axf–output rtthread.bin
“.\build\rtthread-stm32.axf” - 0 Error(s), 0 Warning(s).
Build Time Elapsed: 00:00:07
上面提到的 Program Size 包含以下几个部分:

1)Code:代码段,存放程序的代码部分;

2)RO-data:只读数据段,存放程序中定义的常量;

3)RW-data:读写数据段,存放初始化为非 0 值的全局变量;

4)ZI-data:0 数据段,存放未初始化的全局变量及初始化为 0 的变量;

编译完工程会生成一个. map 的文件,该文件说明了各个函数占用的尺寸和地址,在文件的最后几行也说明了上面几个字段的关系:

Total RO Size (Code + RO Data) 53668 ( 52.41kB)
Total RW Size (RW Data + ZI Data) 2728 ( 2.66kB)
Total ROM Size (Code + RO Data + RW Data) 53780 ( 52.52kB)
1)RO Size 包含了 Code 及 RO-data,表示程序占用 Flash 空间的大小;

2)RW Size 包含了 RW-data 及 ZI-data,表示运行时占用的 RAM 的大小;

3)ROM Size 包含了 Code、RO Data 以及 RW Data,表示烧写程序所占用的 Flash 空间的大小;

RT-Thread 内核对象模型

RT-Thread 采用内核对象管理系统来访问 / 管理所有内核对象,内核对象包含了内核中绝大部分设施,这些内核对象可以是静态分配的静态对象,也可以是从系统内存堆中分配的动态对象。

通过这种内核对象的设计方式,RT-Thread 做到了不依赖于具体的内存分配方式,系统的灵活性得到极大的提高。

RT-Thread 内核对象包括:线程,信号量,互斥量,事件,邮箱,消息队列和定时器,内存池,设备驱动等。对象容器中包含了每类内核对象的信息,包括对象类型,大小等。对象容器给每类内核对象分配了一个链表,所有的内核对象都被链接到该链表上,如图 RT-Thread 的内核对象容器及链表如下图所示:在这里插入图片描述

下图则显示了 RT-Thread 中各类内核对象的派生和继承关系。

由对象控制块 rt_object 派生出来的有:线程对象、内存池对象、定时器对象、设备对象和 IPC 对象(IPC:Inter-Process Communication,进程间通信。在 RT-Thread 实时操作系统中,IPC 对象的作用是进行线程间同步与通信);由 IPC 对象派生出信号量、互斥量、事件、邮箱与消息队列、信号等对象。在这里插入图片描述

常见宏定义说明

RT-Thread 中经常使用一些宏定义,举例 Keil 编译环境下一些常见的宏定义:

1)rt_inline,定义如下,static 关键字的作用是令函数只能在当前的文件中使用;inline 表示内联,用 static 修饰后在调用函数时会建议编译器进行内联展开。

#define rt_inline static __inline
2)RT_USED,定义如下,该宏的作用是向编译器说明这段代码有用,即使函数中没有调用也要保留编译。例如 RT-Thread 自动初始化功能使用了自定义的段,使用 RT_USED 会将自定义的代码段保留。

#define RT_USED attribute((used))
3)RT_UNUSED,定义如下,表示函数或变量可能不使用,这个属性可以避免编译器产生警告信息。

#define RT_UNUSED attribute((unused))
4)RT_WEAK,定义如下,常用于定义函数,编译器在链接函数时会优先链接没有该关键字前缀的函数,如果找不到则再链接由 weak 修饰的函数。

#define RT_WEAK __weak
5)ALIGN(n),定义如下,作用是在给某对象分配地址空间时,将其存放的地址按照 n 字节对齐,这里 n 可取 2 的幂次方。字节对齐的作用不仅是便于 CPU 快速访问,同时合理的利用字节对齐可以有效地节省存储空间。

#define ALIGN(n) attribute((aligned(n)))
6)RT_ALIGN(size,align),定义如下,作用是将 size 提升为 align 定义的整数的倍数,例如,RT_ALIGN(13,4) 将返回 16。

#define RT_ALIGN(size, align) (((size) + (align) - 1) & ~((align) - 1))

线程管理

每个线程都有重要的属性,如线程控制块、线程栈、入口函数等在这里插入图片描述
RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到 CPU 的使用权。

线程栈的增长方向是芯片构架密切相关的,RT-Thread 3.1.0 以前的版本,均只支持栈由高地址向低地址增长的方式,对于 ARM Cortex-M 架构,线程栈可构造如下图所示。
在这里插入图片描述

线程状态
在这里插入图片描述
时间片
每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick)在这里插入图片描述
线程状态切换
线程通过调用函数 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 内核中的系统线程有空闲线程和主线程
空闲线程是系统创建的最低优先级的线程,线程状态永远为就绪态。

空闲线程在 RT-Thread 也有着它的特殊用途:

若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。

空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。

主线程
在系统启动时,系统会创建 main 线程,它的入口函数为 main_thread_entry(),用户的应用入口函数 main() 就是从这里真正开始的
在这里插入图片描述

线程的管理
下图描述了线程的相关操作,包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程。可以使用 rt_thread_create() 创建一个动态线程,使用 rt_thread_init() 初始化一个静态线程,动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。
在这里插入图片描述

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

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

设置和删除空闲钩子
空闲钩子函数是空闲线程的钩子函数,如果设置了空闲钩子函数,就可以在系统执行空闲线程时,自动执行空闲钩子函数来做一些其他事情,比如系统指示灯。设置 / 删除空闲钩子的接口如下:
rt_err_t rt_thread_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));
空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态,例如 rt_thread_delay(),rt_sem_take() 等可能会导致线程挂起的函数都不能使用。

设置调度器钩子
在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:
void rt_scheduler_sethook(void (hook)(struct rt_thread from, struct rt_thread* to));
请仔细编写你的钩子函数,稍有不慎将很可能导致整个系统运行不正常(在这个钩子函数中,基本上不允许调用系统 API,更不应该导致当前运行的上下文挂起)。

关于删除线程:大多数线程是循环执行的,无需删除;而能运行完毕的线程,RT-Thread 在线程运行完毕后,自动删除线程,在 rt_thread_exit() 里完成删除动作。用户只需要了解该接口的作用,不推荐使用该接口(可以由其他线程调用此接口或在定时器超时函数中调用此接口删除一个线程,但是这种使用非常少)。

发布了49 篇原创文章 · 获赞 30 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/liurunjiang/article/details/103524701