内存管理
在计算系统中,通常存储空间可以分为两种:内部存储空间和外部存储空间。
内部存储空间通常访问速度比较快,能够按照变量地址随机访问,也就是我们通常所说的RAM(随机存储器),可以把它理解为电脑的内存。
外部存储空间内所保存的内容相对来说比较固定,即使掉电后数据也不会丢失,也就是通常所讲的ROM(只读存储器),可以把它理解为电脑的硬盘。
计算机系统中,变量、中间数据一般存放在RAM中,只有在实际使用时才将它们从RAM调入到CPU中进行运算。
一些数据需要的内存大小需要在程序运行过程中根据实际情况确定,这就要求系统具有对内存空间进行动态管理的能力,在用户需要一段内存空间时,向系统申请,系统选择一段合适的内存空间分配给用户,用户使用完毕后,再释放回系统,以便系统将该段内存空间回收再利用。
内存管理的功能特点
由于实时系统中对时间的要求非常严格,内存管理往往要比通用操作系统要求苛刻得多:
- 分配内存的时间必须是确定的。一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面。而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。
- 随着内存不断被分配和释放,整个内存区域会产生越来越多的碎片(因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,但因为它们地址并非连续,不能组成一块连续的完整内存块,会使得程序不能申请到大的内存。)对于通用系统来说,这种不恰当的内存分配算法可以通过重新启动系统来解决(每个月或者数个月进行一次),但是对于那些需要常年不间断地工作于野外的嵌入式系统来说,就变得让人无法接受了。
- 嵌入式系统的资源环境也是不尽相同,有些系统的资源比较紧张,只有数十KB的内存可供分配,而有些系统则存在数MB的内存,如何为这些不同的系统,选择适合它们的高效率的内存分配算法,就将变得复杂化。
RT-Thread操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。
总体上可分为两类:内存堆管理与内存池管理,而内存堆管理又根据具体内存设备划分为三种情况:
- 针对小内存块的分配管理(小内存管理算法);
- 针对大内存块的分配管理(slab管理算法);
- 针对多内存堆的分配情况(memheap管理算法)。
内存堆管理
内存堆管理用于管理一段连续的内存空间。
RT-Thread将“ZI段结尾处”到内存尾部的空间用作内存堆。
内存堆可以在当前资源满足的情况下,根据用户的需求分配任意大小的内存块。
而当用户不需要再使用这些内存块时,又可以释放回堆中共其它应用分配使用。
小内存管理算法主要针对系统资源比较少,一般用于小于2MB内存空间的系统。
小内存管理算法
小内存管理算法是一个简单的内存分配算法。
初始时,它是一块大的内存。当需要分配内存块时,将这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来。
每个内存块(不管是已分配的内存块还是空闲的内存块)都包含一个数据头,其中包括:
- magic:变数(或称为幻数),它会被初始化成0x1ea0(即英文单词heap),用于标记这个内存块是一个内存管理用的内存数据块;实质也是一个内存保护字:如果这个区域被改写,那么也就意味着这块内存块被非法改写(正常情况下只有内存管理器才会去碰这块内存)。
- used:指示出当前内存块是否已经分配。
内存管理的表现主要体现在内存的分配与释放上。
空闲链表指针lfree初始指向32字节的内存块。当用户线程要求再分配一个64字节的内存块时,但此lfree指针指向的内存块只有32字节,并不能满足要求,内存管理器会继续寻找下一内存块,当找到再下一块内存块,128 字节时,它满足分配的要求。
因为这个内存块比较大,分配器将此内存块进行拆分,余下的内存块(52字节)继续留在lfree链表中。
另外,在每次分配内存块前,都会留出 12 字节数据头用于 magic、used 信息及链表节点使用。返回给应用的地址实际上是这块内存块 12 字节以后的地址,前面的 12 字节数据头是用户永远不应该碰的部分(注:12 字节数据头长度会与系统对齐差异而有所不同)。
释放时则是相反的过程,但分配器会查看前后相邻的内存块是否空闲,如果空闲则合并成一个大的空闲内存块。
内存堆配置和初始化
在使用内存堆时,必须要在系统初始化的时候进行堆的初始化,可以通过下面的函数接口完成:
void rt_system_heap_init(void* begin_addr, void* end_addr);
这个函数会把参数 begin_addr,end_addr 区域的内存空间作为内存堆来使用。
分配和释放内存块
void *rt_malloc(rt_size_t nbytes);
rt_malloc函数会从系统堆空间中找到合适大小的内存块,然后把内存块可用地址返回给用户。
- nbytes:需要分配的内存块的大小,单位为字节。
- 分配的内存块地址:成功。
- RT_NULL:失败。
对rt_malloc的返回值进行判空是非常有必要的。
应用程序使用完从内存分配其中申请的内存后,必须及时释放,否则会造成内存泄漏。
int *pi;
pi = rt_malloc(100);
if(pi == NULL)
{
rt_kprintf("malloc failed\r\n");
}
rt_free函数会把释放内存还给堆管理器中。
在调用这个函数时用户需传递待释放的内存块指针。
重分配内存块
在已分配内存块的基础上重新分配内存块的大小(增加或缩小),可以通过下面的函数接口完成:
void *rt_realloc(void *rmem,rt_size_t newsize);
在进行重新分配内存块时,原来的内存块数据保持不变(缩小的情况下,后面的数据被自动截断)。
- rt_realloc函数用于修改一个原先已经分配的内存块的大小。使用这个函数,你可以使一块内存扩大或缩小。若用于扩大一个内存块,那么这块内存原先的内容依然保留,新增加的内存添加到原先内存块的后面,新内存并未以任何方式进行初始化。如果它用于缩小一个内存块,该内存尾部的部分内存便被拿掉,剩余部分内存的原先内容依然保留。
- 如果原先的内存块无法改变大小,rt_realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,在使用rt_realloc之后,就不能再使用指向旧内存块的指针,而是应该改用rt_realloc所返回的新指针。
分配多内存块
从内存堆中分配连续内存地址的多个内存块。
void *rt_calloc(rt_size_t count, rt_size_t size);
- count:内存块数量
- size:内存块容量
- 返回:指向第一个内存块地址的指针,成功,并且所有分配的内存块都被初始化成0。RT_NULL:分配失败。
设置内存钩子函数
在分配内存块过程中,用户可设置一个钩子函数,调用的函数接口如下:
void rt_malloc_sethook(void (*hook)(void *ptr, rt_size_t size));
设置的钩子函数会在内存分配完成后进行回调。
回调时,会把分配到的内存块地址和大小作为入口参数传递进去。
在释放内存时,用户可设置一个钩子函数,调用的函数接口如下:
void rt_free_sethook(void (*hook)(void *ptr));
设置的钩子函数会在调用内存释放完成前进行回调。回调时,释放的内存块地址会作为入口参数传递进去(此时内存块并没有被释放)。
总结
动态内存使用总结:
- 检查从rt_malloc函数返回的指针是否为NULL
- 不要访问动态分配内存之外的内存
- 不要向rt_free传递一个并非由rt_malloc函数返回的指针
- 在释放动态内存之后不要再访问它
- 使用sizeof计算数据类型的长度,提高程序的可移植性
常见的动态内存错误:
- 对NULL指针进行解引用
- 对分配的内存进行操作时越过边界
- 释放并非动态分配的内存
- 释放一块动态分配的内存的一部分(rt_free(ptr+4))
- 动态内存被释放后继续使用
内存碎片:频繁地调用内存分配和释放接口会导致内存碎片,一个避免内存碎片的策略是使用内存池+内存堆混用的方法。