RT-Thread 内核介绍
内核是操作系统的核心,负责管理系统的线程、线程间通信、系统时钟、中断以及内存等。
内核位于硬件层之上,内核部分包括内核库、实时内核实现。
内核库是为了保证内核能够独立运行的一套小型的类似C库的函数实现子集。
这部分根据编译器的不同自带C库的情况也会有些不同,当使用GNU GCC编译器时,会携带更多的标准C库实现。
C库:也叫C运行库,它提供了类似strcpy、memcpy等函数,有些也会包括printf、scanf函数的实现。
RT-Thread Kernel service Library仅提供内核用到的一小部分C库函数实现。为了避免与标准C库重名,这些函数前都加上了rt_.
内核最小的资源占用情况是 3KB ROM,1.2KB RAM。
线程调度
线程是RT-Thread操作系统中最小的调度单位,线程调度算法是基于优先级的全抢占式调度算法,即在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统其它部分都可以抢占,包括线程调度器自身。支持 256 个线程优先级(也可通过配置文件更改为最大支持 32 个或 8 个线程优先级,针对 STM32 默认配置是 32 个线程优先级),0 优先级代表最高优先级,最低优先级留给空闲线程使用;同时它也支持创建多个具有相同优先级的线程,相同优先级的线程间采用时间片的轮转调度算法进行调度,使每个线程运行相应时间;另外调度器在寻找那些处于就绪状态的具有最高优先级的线程时,所经历的时间是恒定的,系统也不限制线程数量的多少,线程数目只和硬件平台的具体内存相关。
时钟管理
RT-Thread的时钟管理以时钟节拍为基础,时钟节拍是RTT操作系统中最小的时钟单位。
RT-Thread 的定时器提供两类定时器机制:第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动的停止定时器否则将永远持续执行下去。
另外,根据超时函数执行时所处的上下文环境,RT-Thread 的定时器可以设置为 HARD_TIMER 模式或者 SOFT_TIMER 模式。
通常使用定时器定时回调函数(即超时函数),完成定时服务。用户根据自己对定时处理的实时性要求选择合适类型的定时器。
线程间同步
RT-Thread 采用信号量、互斥量与事件集实现线程间同步。线程通过对信号量、互斥量的获取与释放进行同步;互斥量采用优先级继承的方式解决了实时系统常见的优先级翻转问题。
线程同步机制支持线程按优先级等待方式获取信号量或互斥量。线程通过对事件的发送与接收进行同步;
事件集支持多事件的 “或触发” 和“与触发”,适合于线程等待多个事件的情况。
线程间通信
RT-Thread 支持邮箱和消息队列等通信机制。邮箱中一封邮件的长度固定为 4 字节大小;消息队列能够接收不固定长度的消息,并把消息缓存在自己的内存空间中。邮箱效率较消息队列更为高效。邮箱和消息队列的发送动作可安全用于中断服务例程中。通信机制支持线程按优先级等待方式获取。
RT-Thread启动流程
一般了解一份代码大多从启动部分开始,同样这里也采用这种方式,先寻找启动的源头。
RT-Thread支持多种平台和多种编译器,**rtthread_startup()**函数是RT-Thread规定的统一启动入口。
一般执行顺序是:系统先从启动文件开始,然后进入rtthread_startup(),最后进入用户入口函数main()。
以MDK-ARM为例,用户程序入口为main()函数,位于main.c文件中。
系统启动后先从汇编代码startup_stm32f103ex.s开始运行,然后跳转到C代码,进行RT-Thread系统启动,最后进入用户程序入口函数main()。
为了在进入main()之前完成系统功能初始化,使用了MDK的扩展功能 S u b Sub Sub$ 和 S u p e r Super Super$。
在main函数前加上这个前缀,可以先调用一些补偿在main之前的功能函数。
在汇编代码内调用了启动文件rtthread_startup()函数,
int rtthread_startup(void){
rt_hw_interrupt_disable();
/* 板级初始化,需要在该函数内部进行系统堆的初始化 */
rt_hw_board_init();
/* 定时器初始化 */
rt_system_timer_init();
/* 调度器初始化 */
rt_system_scheduler_init();
#ifdef RT_USING_SIGNALS
/* 信号初始化 */
rt_system_signal_init();
#endif
/* 创建一个用户main线程 */
rt_application_init();
/* 定时器线程初始化 */
rt_system_timer_thread_init();
/*空闲线程初始化*/
rt_thread_idle_init();
/* 启动调度器 */
rt_system_scheduler_start();
return 0;
}
这部分启动代码,大致可以分为四个部分:
- 初始化与系统相关的硬件;
- 初始化系统内核对象,如定时器、调度器、信号
- 创建main线程,在main线程中对各类模块依次进行初始化
- 初始化定时器线程、空闲线程,并启动调度器。
启动调度器之前,系统所创建的线程在执行rt_thread_startup()后并不会立马运行,它们会处于就绪状态等待系统调度;待启动调度器之后,系统才转入第一个线程开始运行,根据调度规则,选择的是就绪队列中优先级最高的线程。
rt_hw_board_init()中完成系统时钟设置,为系统提供心跳,串口初始化,将系统输入输出终端绑定到这个串口,后续系统运行信息就会从串口打印出来。
RT-Thread程序内存分布
一般MCU包含的存储空间有:片内Flash与片内RAM,RAM相当于内存,Flash相当于硬盘。
编译器会将一个程序分类为好几个部分,分别存储在MCU不同的存储区。
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包含:
- Code:代码段,存放程序的代码部分;
- RO-data:只读数据段,存放程序中定义的常量;
- RW-data:读写数据段,存放初始化为非零值的全局变量;
- ZI-data:0数据段,存放未初始化的全局变量以及初始化为0的变量。
编译完工程会生成一个.map文件,该文件说明了各个函数占用的尺寸和地址,在文件的最后几行也说明了上面几个字段的关系:
- RO Size包含了Code以及RO-data,表示程序占用Flash空间的大小。
- RW Size包含了RW-data以及ZI-data,表示运行时占用的RAM的大小。
- ROM Size包含了Code、RO-data以及RW-data,表示程序烧写时所占用的Flash空间大小。
程序运行之前,需要有文件实体被烧录到STM32的Flash中,一般是bin或者hex文件,该被烧录文件称为可执行映像文件。
STM32上电之后默认从Flash启动,会将RW段中的RW-data搬运到RA中,但不会搬运RO段,即CPU的执行代码从Flash中读取,另外根据编译器给出的ZI地址和大小分配出ZI段,并将这块RAM区域清零。
其中动态内存堆为未使用的RAM空间,应用程序申请和释放的内存块都来自该空间。
RT-Thread内核对象模型
RT-Thread内核采用面向对象的设计思想进行设计,系统级的基础设施都是一种内核对象,如线程,信号量,互斥量,定时器等。
内核对象分为两类:静态内核对象放在RW段和ZI段中,在系统启动后在程序中初始化;动态内核对象则是从内存堆中创建的,而后手工做初始化。
thread1对象的内存空间,包括线程控制块thread1与栈空间thread1_stack都是编译时决定的。
内核对象管理架构
RTT采用内核对象管理系统来访问/管理所有内核对象,内核对象包含了内核中绝大部分设施,这些内核对象可以是静态的也可以是动态的。
RTT内核对象包括:线程、信号量、互斥量等。
对象容器包含了每类内核对象的信息,包括对象类型、大小等。
对象容器给每类内核对象分配了一个链表,所有的内核对象都被链接到该链表上。
下图显示了RTT各类内核对象的派生和继承关系。
对于每一种具体内核对象和对象控制块,除了基本结构外,还有自己的扩展属性(私有属性),例如,对于线程控制块,在基类对象上进行扩展,增加了线程状态、优先级等属性。
内核对象管理方式
内核对象容器的数据结构:
struct rt_object_information{
enum rt_object_class_type type;
rt_list_t object_list;
rt_size_t object_size;
};
一类对象由一个rt_object_information结构体来管理,每一个这类对象的具体实例都通过链表的形式挂接在object_list上。
而这一类对象的内存块尺寸由object_size标识出来(每一类对象的具体实例,它们占用的内存块大小是相同的)。
初始化对象
在使用一个未初始化的静态对象前必须先对其进行初始化。
void rt_object_init(struct rt_object* object, enum rt_object_class_type type ,
const char* name)
遍历所有线程
rt_thread_t thread = RT_NULL;
struct rt_list_node *node = RT_NULL;
struct rt_object_information *information = RT_NULL;
information = rt_object_get_information(RT_Object_Class_Thread);
rt_list_for_each(node, &(information->object_list))
{
thread = (rt_thread_t)rt_list_entery(node, struct object, list);
rt_kprintf("name:%s\n", thread->name);
}
RT-Thread
RT-Thread的一个重要特性是高度可裁剪性,支持对内核进行精细调整,对组件进行灵活拆卸。
配置主要是通过修改工程目录下的rtconfig.h文件来进行,用户可以通过打开/关闭该文件中的宏定义来对代码进行条件编译,最终达到系统配置和裁剪的目的。