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
- Code:代码段,存放程序的代码部分;
- RO-data:只读数据段,存放程序中定义的常量;
- RW-data:读写数据段,存放初始化为非0值的全局变量;
- ZI-data:0数据段,存放未初始化的全局变量以及初始化为0的变量;
1)RO Size 包含了 Code 及 RO-data,表示程序占用 Flash 空间的大小;
2)RW Size 包含了 RW-data 及 ZI-data,表示运行时占用的 RAM 的大小;
3)ROM Size 包含了 Code、RO-data 以及 RW-data,表示烧写程序所占用的 Flash 空间的大小;
程序运行之前,需要有文件实体被烧录到STM32的Flash中,一般是bin或者hex文件,该被烧录文件称为可执行映像文件。
由于ZI-data都是0,所以未包含在映像文件中。
STM32在上电启动之后默认从Flash启动,启动之后会将RW段中的RW-data(初始化的全局变量)搬运到RAM中,但不会搬运RO段,即CPU的执行代码从Flash中读取,另外根据编译器给出的ZI地址和大小分配出ZI段,并将这块RAM区域清零。
动态内存堆为未使用的RAM空间,应用程序申请和释放的内存块都来自该空间。
rt_uint8_t* msg_ptr;
msg_ptr = (rt_uint8_t*)rt_malloc(128);
rt_memset(msg_ptr, 0, 128);
代码中的msg_ptr指针指向的128字节内存空间位于动态内存堆空间中。
RT-Thread自动初始化机制
自动初始化机制是指初始化函数不需要被显示调用,只需要在函数定义处通过宏定义的方式进行申明,就会在系统启动过程中被执行。
例如在串口驱动中调用一个宏定义告知系统初始化需要调用的函数
int rt_hw_usart_init(void){
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); //使用组件自动初始化机制
rt_conponents_board_init()函数执行的比较早,主要初始化相关硬件环境,执行这个函数时会遍历通过INIT_BOARD_EXPORT(fn)申明的初始化函数表,并调用各个函数。
rt_components_init()函数会在操作系统运行起来之后创建的main线程里被调用执行,这个时候硬件环境和操作系统已经初始化完成,可以执行应用相关代码,rt_components_init()函数会遍历通过剩下的其它几个宏申明的初始化函数表。
RT-Thread的自动初始化机制使用了自定义RTI符号段,将需要在启动时进行初始化的函数指针放到了该段中,形成一张初始化函数表,在系统启动过程中会遍历该表,并调用表中的函数,达到自动初始化的目的。
用来实现自动初始化功能的宏接口定义详细描述如下:
- INIT_BOARD_EXPORT(fn):非常早期的初始化,此时调度器未启动
- INIT_PREV_EXPORT(fn):主要用于纯软件的初始化,没有太多依赖的函数
- INIT_DEVICE_EXPORT(fn):外设驱动初始化相关,比如网卡设备
- INIT_COMPONENTS_EXPORT(fn):组件初始化,比如文件系统或者LWIP
- INIT_ENV_EXPORT(fn):系统环境初始化,比如挂载文件系统
- INIT_APP_EXPORT(fn):应用初始化,比如GUI应用
初始化函数主动通过这些宏接口进行申明,如INIT_BOARD_EXPORT(rt_hw_usart_init),链接器会自动收集所有被申明的初始化函数,放到RTI符号段中,该符号段位于内存分布的RO段中,该RTI符号段中的所有函数在系统初始化时会被自动调用。
RTT内核对象模型
RTT内核采用面向对象的设计思想进行设计,系统级的基础设施是一种内核对象,例如线程,信号量,互斥量,定时器等。
内核对象分为两类:静态内核对象和动态内核对象,静态内核对象通常放在RW段和ZI段中,在系统启动后在程序中初始化;动态内核对象则是从内存堆中创建的,而后手工做初始化。
/*线程1的对象和运行时用到的栈*/
static struct rt_thread thread1;
static rt_uint8_t thread1_stack[512];
void thread1_entry(void* parameter){
int i;
while(1){
for(i=0; i<10; i++){
rt_kprintf("%d\n", i);
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;
result = rt_thread_init(&thread1, "thread1", thread_entry, RT_NULL, &thread1_stack[0], sizeof(thread1_stack), 200, 10);
if(result == RT_EOK){
rt_thread_startup(&thread1);
}
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;
}
thread1是一个静态线程对象,而thread2是一个动态线程对象。
thread1对象的内存空间,包括线程控制块thread1与栈空间thread1_stack都是编译时决定的,因为代码中都不存在初始值,统一放在未初始化数据段中。
thread2运行中用到的空间都是动态分配的,包括线程控制块(thread2_ptr指向的内存)和栈空间。
静态对象会占用RAM空间,不依赖于内存堆管理器,内存分配时间确定。
动态对象依赖于内存堆管理器,运行时申请RAM空间,当对象被删除后,占用的RAM空间被释放。
内核对象管理架构
RTT采用内核对象管理系统来访问/管理所有内核对象,内核对象包含了内核中绝大部分设施,这些内核对象可以是静态分配的静态对象,也可以是系统内存堆中分配的动态对象。
通过这种内核对象的设计方式,RTT做到了不依赖于具体的内存分配方式,系统的灵活性得到极大的提高。
RTT内核对象包括:线程,信号量,互斥量,事件,邮箱,消息队列和定时器,内存池,设备驱动等。
对象容器中包含了每类内核对象的信息,包括对象类型,大小等。
对象容器给每类内核对象分配了一个链表,所有的内核对象都被链接到该链表上。
对于每一种具体内核对象和对象控制块,除了基本结构外,还有自己的扩展属性(私有属性)。
例如,对于线程控制块,在基类对象基础上进行扩展,增加了线程状态、优先级等属性。这些属性在基类对象的操作中不会用到,只有在具体线程相关的操作中才会使用。
因此从面向对象的观点,可以认为每一种具体对象是抽象对象的派生,继承了基本对象的属性并在此基础上扩展了与自己相关的属性。
在对象管理模块中,定义了通用的数据结构,用来保存各种对象的共同属性,各种具体对象只需要在此基础上加上自己的某些特别的属性,就可以清楚的表示自己的特征。
这种设计方法的优点有:
- 提高了系统的可重用性和扩展性,增加新的对象类别很容易,只需要继承通用对象的属性再加少量扩展即可。
- 提供统一的对象操作方式,简化了各种具体对象的操作,提高了系统的可靠性。
上图中由对象控制块rt_object派生出来的有:线程对象、内存池对象、定时器对象、设备对象和IPC对象(IPC:Inter-Process Communication,进程间通信。在RT-Thread实时操作系统中,IPC对象的作用是进行线程间同步与通信);由 IPC 对象派生出信号量、互斥量、事件、邮箱与消息队列、信号等对象。
对象控制块
内核对象控制块的数据结构:
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_Null = 0x00, /**< The object is not used. */
RT_Object_Class_Thread = 0x01, /**< The object is a thread. */
RT_Object_Class_Semaphore = 0x02, /**< The object is a semaphore. */
RT_Object_Class_Mutex = 0x03, /**< The object is a mutex. */
RT_Object_Class_Event = 0x04, /**< The object is a event. */
RT_Object_Class_MailBox = 0x05, /**< The object is a mail box. */
RT_Object_Class_MessageQueue = 0x06, /**< The object is a message queue. */
RT_Object_Class_MemHeap = 0x07, /**< The object is a memory heap. */
RT_Object_Class_MemPool = 0x08, /**< The object is a memory pool. */
RT_Object_Class_Device = 0x09, /**< The object is a device. */
RT_Object_Class_Timer = 0x0a, /**< The object is a timer. */
RT_Object_Class_Module = 0x0b, /**< The object is a module. */
RT_Object_Class_Memory = 0x0c, /**< The object is a memory. */
RT_Object_Class_Channel = 0x0d, /**< The object is a channel */
RT_Object_Class_Custom = 0x0e, /**< The object is a custom object */
RT_Object_Class_Unknown = 0x0f, /**< The object is unknown. */
RT_Object_Class_Static = 0x80 /**< The object is a static object. */
};
从上面的类型说明,如果是静态对象,那么对象类型的最高位是1(是RT_Object_Class_Static与其它类型的或操作),否则就是动态对象,系统最多能够容纳的对象类别数目是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以外的类型(对于静态对象,或使用rt_object_init接口进行初始化的对象,系统会把它标识成RT_Object_Class_Static类型)
- name:对象的名字,每个对象可以设置一个名字,这个名字的最大长度由RT_NAME_MAX指定,并且系统不关心它是否以’\0’作为终结符
脱离对象
从内核对象管理器中脱离一个对象。脱离对象使用以下接口:
void rt_object_detach(rt_object_t object);
调用该接口,可使得一个静态对象从内核对象容器中脱离出来,即从内核对象容器链表上删除相应的对象节点。
对象脱离后,对象占用的内存并不会被释放。
分配对象
上述描述的都是对象初始化、脱离的接口,都是面向对象内存块已经有的情况下,而动态的对象则可以在需要时申请,不需要时释放出内存空间给其它应用使用。申请分配新的对象可以使用:
rt_object_t rt_object_allocate(enum rt_object_class_type type, const char* name)
在调用以上接口时,系统首先需要根据对象类型获取对象信息(特别是对象类型的大小信息以用于系统能够分配正确大小的内存数据块),而后从内存堆中分配对象所对应大小的内存空间,然后再对该对象进行必要的初始化,最后将其插入到它所在的对象容器链表中。
分配成功则返回对象句柄
删除对象
对于一个动态对象,当不再使用时,可以调用如下接口删除对象,并释放相应的系统资源:
voidrt_object_delate(rt_object_t object)
判别对象
判断指定对象是否是系统对象(静态内核对象)。辨别对象使用以下接口:
rt_err_t rt_object_is_systemobject(rt_object_t object);
判断一个对象是否是系统对象,通常使用rt_object_init()方式初始化的对象都是系统对象。
如何遍历内核对象
以遍历所有线程为例
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_entry(node, struct rt_obejct, list);
rt_kprintf("name:%s\n", thread->parent.name);
}