RT-Thread学习--内核基础2

上一篇博客简单介绍了一下实时内核库包含的一些内容:线程调度、时钟管理、内存管理、I/O设备管理、线程间同步、线程间通信,在这篇博客中,继续内核基础的相关学习。

RT-Thread启动流程

rtthread_startup()函数是RT-Thread规定的统一启动入口。一般执行顺序是:系统先从启动文件开始运行,然后进入 RT-Thread 的启动 rtthread_startup() ,最后进入用户入口main(),如下图所示:
RT-Thread学习--内核基础2_第1张图片
在上图中,先从汇编代码startup_xxx.s开始执行,然后进入rtthread_startup(),开始系统的启动,在启动程序中,主要包括四个部分

  • 初始化与系统相关的硬件;
  • 初始化系统内核对象,如:定时器、调度器、信号;
  • 创建main线程,在main线程中对各类模块依次进行初始化;
  • 初始化定时器线程、空闲线程,并启动调度器。

创建main线程的函数:rt_application_init()
调度器启动函数 :rt_system_scheduler_start()
rt_hw_board_init()函数实现系统时钟的设置,为系统提供心跳、串口初始化,将系统输入输出终端绑定在这个串口,后续的系统运行信息就可以通过该串口打印出来了。
在rtthread_startup()中会创建很多线程,但是在调度器启动(启动函数被rtthread_startup调用)之前,这些线程都不会在创建后立马就运行,而是将状态变为就绪态等待系统的调度;待调度启动之后,系统才开始第一个线程的运行,然后根据调度规则(基于优先级的全抢占式多线程调度),选择优先级最高的线程进行执行。

最后进入用户的main函数,开始执行用户代码。

RT-Thread程序内存分布

一般MCU包含的存储空间:片内Flash(相当于硬盘)片内RAM(相当于内存)。编译器会将一个程序分类为好几个部分,分别存储到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包含了一下几个部分:
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)

其中,

  • RO Size:包含了Code和RO-data,表示程序占用Flash空间的大小
  • RW Size:包含了RW-data和ZI-data,表示在运行的时候占用RAM的大小
  • ROM Size:包含了Code、RO-data以及RW-data,表示烧写程序所占用的Flash大小

在程序运行之前,需要有文件实体被烧录到Flash中,一般是bin文件或者是hex文件,该文件被称作是可执行映像文件

RT-Thread自动初始化机制

自动初始化机制:

  • 初始化函数不被显式调用
  • 通过共定义的方式在函数定义处进行申明就可以实现系统启动过程中被执行。
    这样的示例在资料中有很多处,这里以串口驱动初始化为例:
int rt_hw_usart_init(void) /* 串 口 初 始 化 函 数 */
{
	... ...
	/* 注 册 串 口 1 设 备 */
	rt_hw_serial_register(&serial1, "uart1",
	RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,
	uart);
	return 0;
}
INIT_BOARD_EXPORT(rt_hw_usart_init); /* 使 用 组 件 自 动 初 始 化 机 制 */

在上述代码中,INIT_BOARD_EXPORT(rt_hw_usart_init)就是自动初始化功能的使用,系统就会自动调用该函数。调用的位置为启动流程图中浅蓝色底的相应位置。同样,流程图中所有浅蓝色为底的函数都表示被自动初始化。说明如下:

  1. “board init functions” 为所有通过 INIT_BOARD_EXPORT(fn) 申明的初始化函数。主要初始化硬件相关环境。
  2. “pre-initialization functions” 为所有通过 INIT_PREV_EXPORT(fn) 申明的初始化函数。
  3. “device init functions” 为所有通过 INIT_DEVICE_EXPORT(fn) 申明的初始化函数。
  4. “components init functions” 为所有通过 INIT_COMPONENT_EXPORT(fn) 申明的初始化函数。在创建的main线程中被调用,此时硬件环境和操作系统已经初始化完成。
  5. “enviroment init functions” 为所有通过 INIT_ENV_EXPORT(fn) 申明的初始化函数。
  6. “application init functions” 为所有通过 INIT_APP_EXPORT(fn) 申明的初始化函数。

RT-Thraed自动初始化机制使用了自定义的RTI符号段(位于内存分布的 RO 段中),将需要在启动时进行初始化的函数指针放到该段中,形成一张初始化函数表,在系统启动过程中遍历该表,并调用表中的函数,达到自动初始化的目的。
用来实现自动初始化功能的宏接口定义详细描述:
初始化顺序 宏接口 描述
1 INIT_BOARD_EXPORT(fn) 非常早期的初始化,此时调度器还未启动
2 INIT_PREV_EXPORT(fn) 主要是用于纯软件的初始化、没有太多依赖的函数
3 INIT_DEVICE_EXPORT(fn) 外设驱动初始化相关,比如网卡设备
4 INIT_COMPONENT_EXPORT(fn) 组件初始化,比如文件系统或者 LWIP
5 INIT_ENV_EXPORT(fn) 系统环境初始化,比如挂载文件系统
6 INIT_APP_EXPORT(fn) 应用初始化,比如 GUI 应用

RT-Thread内核对象模型

系统级的基础设施都是一种内核对象,如:线程、邮箱、信号量、互斥量、定时器、消息队列等。

静态对象和动态对象

静态对象:通常放在RW和ZI段,系统启动后在程序中初始化;占用RAM空间,不依赖于内存堆管理器,内存分配时间确定。

动态对象:从内存堆中创建,依赖内存堆管理器,手动初始化;运行时申请RAM空间,当对象被删除后,占用的RAM空间被释放。

静态线程对象thread1,动态线程对象thread2,示例代码如下:

/* 线 程 1 的 对 象 和 运 行 时 用 到 的 栈 */
static struct rt_thread thread1;
static rt_uint8_t thread1_stack[512];
/* 线 程 1 入 口 */
void thread1_entry(void* parameter)
{
	int i;
	while (1)
	{
		for (i = 0; i < 10; i ++)
		{
			rt_kprintf("%d\n", i);
			/* 延 时 100ms */
			rt_thread_mdelay(100);
		}
	}
}
/* 线 程 2 入 口 */
void thread2_entry(void* parameter)
{
	int count = 0;
	while (1)
	{
		rt_kprintf("Thread2 count:%d\n", ++count);
		/* 延 时 50ms */
		rt_thread_mdelay(50);
	}
}
/* 线 程 例 程 初 始 化 */
int thread_sample_init()
{
	rt_thread_t thread2_ptr;
	rt_err_t result;
	/* 初 始 化 线 程 1 */
	/* 线 程 的 入 口 是 thread1_entry, 参 数 是 RT_NULL
	* 线 程 栈 是 thread1_stack
	* 优 先 级 是 200, 时 间 片 是 10 个 OS Tick
	*/
	result = rt_thread_init(&thread1, "thread1",
							thread1_entry, RT_NULL,
							&thread1_stack[0], 
							sizeof(thread1_stack),
							200, 10);
	/* 启 动 线 程 */
	if (result == RT_EOK) 
		rt_thread_startup(&thread1);
	/* 创 建 线 程 2 */
	/* 线 程 的 入 口 是 thread2_entry, 参 数 是 RT_NULL
	* 栈 空 间 是 512, 优 先 级 是 250, 时 间 片 是 25 个 OS Tick
	*/
	thread2_ptr = rt_thread_create("thread2",thread2_entry, 
									RT_NULL, 512, 250, 25);
	/* 启 动 线 程 */
	if (thread2_ptr != RT_NULL) 
		rt_thread_startup(thread2_ptr);
	return 0;
}

内核对象管理架构

  • RT-Thread通过内核对象管理系统来访问/管理所有内核对象。
  • 通过这种内核对象的设计方式,RT-Thread做到了不依赖于具体的内存分配方式,系统的灵活性得到了极大的提高。
  • 对象容器中包含了每类内核对象的信息(对象类型、大小等),并给每类内核对象分配一个链表,并将所有内核对象都链接到该链表中。如下图所示:
    RT-Thread学习--内核基础2_第2张图片

由于采用了面向对象的设计方法,所以RT-Thread的内核对象是可以继承的。与C++中类的继承类似,在对象管理模块中,定义了通用的数据结构,用来保存各种对象的共同属性,各种具体对象加上自己私有的一些属性就可以清楚的表达自己的特征了。这种方式有以下优点:

  • 提高代码的可重用性和扩展性
  • 提供统一的对象操作方式,简化了各种具体对象的操作,提高了系统的可靠性

用树状图可以表示如下:
RT-Thread学习--内核基础2_第3张图片

如上对象控制块rt_object提供了name、type、flag、list属性,由rt_object派生出来了rt_thread、rt_ipc_object、rt_device等对象,由rt_ipc_object派生出信号量、互斥量、邮箱、事件消息队列等对象。

对象控制块

内核对象控制块的数据结构(结构体rt_object):

struct rt_object
{
	/* 内 核 对 象 名 称 */
	char name[RT_NAME_MAX];
	/* 内 核 对 象 类 型 */
	rt_uint8_t type;
	/* 内 核 对 象 的 参 数 */
	rt_uint8_t flag;
	/* 内 核 对 象 管 理 链 表 */
	rt_list_t list;
};

目前内核对象支持的类型有:

enum rt_object_class_type
{
	RT_Object_Class_Thread = 0, /* 对 象 为 线 程 类 型 */
#ifdef RT_USING_SEMAPHORE
	RT_Object_Class_Semaphore, /* 对 象 为 信 号 量 类 型 */
#endif
#ifdef RT_USING_MUTEX
	RT_Object_Class_Mutex, /* 对 象 为 互 斥 量 类 型 */
#endif
#ifdef RT_USING_EVENT
	RT_Object_Class_Event, /* 对 象 为 事 件 类 型 */
#endif
#ifdef RT_USING_MAILBOX
	RT_Object_Class_MailBox, /* 对 象 为 邮 箱 类 型 */
#endif
#ifdef RT_USING_MESSAGEQUEUE
	RT_Object_Class_MessageQueue, /* 对 象 为 消 息 队 列 类 型 */
#endif
#ifdef RT_USING_MEMPOOL
	RT_Object_Class_MemPool, /* 对 象 为 内 存 池 类 型 */
#endif
#ifdef RT_USING_DEVICE
	RT_Object_Class_Device, /* 对 象 为 设 备 类 型 */
#endif
	RT_Object_Class_Timer, /* 对 象 为 定 时 器 类 型 */
#ifdef RT_USING_MODULE
	RT_Object_Class_Module, /* 对 象 为 模 块 */
#endif
	RT_Object_Class_Unknown, /* 对 象 类 型 未 知 */
	RT_Object_Class_Static = 0x80 /* 对 象 为 静 态 对 象 */换成二进制为1000,0000,十进制为128
};

目前系统最多能够容纳的对象类别数为127个。

内核对象管理方式

内核对象容器数据结构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)

object:需要初始化的对象指针,必须指向具体的对象内存块,而不能是空指针或者野指针。
type :rt_object_class_type枚举类型中除RT_Object_Class_Static以外的类型,得到静态对象。
name :对象的名字,名字的最大长度由RT_NAME_MAX指定。

脱离对象

从内核对象管理器中脱离一个对象。注意不是删除对象。

void rt_object_detach(rt_object_t object);

可以使得一个静态内核对象从内核对象容器中脱离出来,即从对象容器链表上删除相应的对象节点。并且对象脱离之后,其占用的内存并没有被释放

分配对象

对象的初始化以及脱离都是在面向对象内存块已经有的情况下进行的。在使用动态对象的时候,内存空间可以根据需要动态申请或者释放。申请内存接口为:

rt_object_t rt_object_allocate(enum rt_object_class_type type ,
								const char* name)
  • type:rt_object_class_type枚举类型中除RT_Object_Class_Static以外的类型,得到动态类型对象。
  • name:对象名字,最大长度由RT_NAME_MAX决定。

返回值:

  • 成功:对象句柄
  • 失败:RT_NULL

删除对象

对于动态对象,可以使用rt_object_delete从对象容器列表中脱离对象并释放相应对象所占用的内存资源。接口为:

void rt_object_delete(rt_object_t object);

辨别对象

判断指定对象是否是系统对象(静态内核对象,在RT-Thread中,一个系统对象就是一个静态对象)。并且通常使用rt_object_init()方式初始化的对象都是系统对象。对象辨别的接口为:

rt_err_t rt_object_is_systemobject(rt_object_t object);

RT-Thread内核配置

对于RT-Thread的内核配置是通过修改rtconfig.h文件来进行的。通过在文件中打开或者关闭相应的宏实现对代码的条件编译,最终达到系统配置和剪裁的目的。这种方式也保证了RT-Thread高度的可剪裁性。在实际应用中,系统配置文件 rtconfig.h 是由配置工具自动生成的,无需手动更改。
对于内核的配置,宏打开表示开启,否则表示关闭。配置内容主要包括以下几部分:

  • RT-Thread内核部分
    该部分的时钟节拍宏控制用#define RT_TICK_PER_SECOND 100表示,定义的时钟节拍表示100个tick为1秒,一个tick就是10ms.
  • 线程间同步与通信部份(包括信号量、互斥量、邮箱、事件、消息队列、消息等)
  • 内存管理部分
  • 内核设备对象
  • 自动初始化方式
  • FinSH调试工具
  • 关于MCU

常见宏定义

  • static表示只在当前文件中使用;inline表示内联,用static修饰后在调用该函数的时候,会建议编译器进行内联展开
#define rt_inline static __inline
  • RT_USED,定义如下,该宏的作用是向编译器说明这段代码有用,即使函数中没有调用也要保留编译。例如 RT-Thread 自动初始化功能使用了自定义的段,使用 RT_USED 会将自定义的代码段保留。
  • RT_UNUSED,定义如下,表示函数或变量可能不使用,这个属性可以避免编译器产生警告信息。
#define RT_USED __attribute__((used))
#define RT_UNUSED __attribute__((unused))
  • RT_WEAK,定义如下,常用于定义函数,编译器在链接函数时会优先链接没有该关键字前缀的函数,如果找不到则再链接由 weak 修饰的函数。
#define RT_WEAK __weak
  • ALIGN(n),定义如下,作用是在给某对象分配地址空间时,将其存放的地址按照 n 字节对齐,这里 n 可取 2 的幂次方。
  • 字节对齐的作用不仅便于CPU的快速访问,同时合理的利用字节对齐可以有效地节省存储空间。
#define ALIGN(n) __attribute__((aligned(n)))

内核基础部分到这就结束了,通过堆内核基础地学习,我们了解到了内核的组成部分(内核库和实时内核),在实时内核中,分别简单介绍了

  • 线程调度(基于优先级的全抢占式)
  • 时钟管理(时钟节拍、单次/周期触发)
  • 线程同步(互斥量、信号量、事件集、优先级翻转等)
  • 线程通信(消息队列:不定长度,缓存在内存空间;邮箱:定长、高效)
  • I/O设备管理
  • 内存管理(静态内存池、动态内存堆)

在后续中,介绍了
系统启动流程

  • startup_xxx.s文件
  • rtthread_startup()
    (1)硬件初始化;
    (2)系统内核对象初始化;
    (3)创建main线程,对各模块初始化;
    (4)初始化定时器线程、空闲线程,并启动调度器。
  • main()

自动初始化
硬件相关->纯软件->外设->相关组件->系统环境->应用

静态/动态对象
初始化、脱离、分配、删除、辨别。静态动态的区别,对象的继承和派生等

内存分布
RO Size、RW Size、ROM Size等

内核配置
各种宏定义

在接下来的博客中,将详细介绍线程管理。

你可能感兴趣的:(RT-Thread学习,内核)