在计算系统中,通常存储空间可以分为两种:内部存储空间和外部存储空间。
计算机系统中,变量、中间数据一般存放在RAM中,只有在实际使用时才将它们从RAM调入到CPU中进行运算。
一些数据需要的内存大小需要在程序运行过程中根据实际情况确定,这就要求系统具有对内存空间进行动态管理的能力:在用户需要一段内存空间时,向系统申请,系统选择一段合适的内存空间分配给用户,用户使用完毕后,再释放回系统,以便系统将该段内存空间回收再利用。
RT-Thread有两种内存管理方式:分别是动态内存堆管理和静态内存池管理。
由于实时系统中对时间的要求非常严格,内存管理往往要比通用操作系统要求苛刻得多:
RT-Thread操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性的提供了不同的内存分配管理算法。
总体上可分为两类:内存堆管理与内存池管理,而内存堆管理又根据具体内存设备划分为三种情况:
RT-Thread将“ZI段结尾处”到内存尾部的空间用作内存堆。
内存堆可以在当前资源满足的情况下,根据用户的需求分配任意大小的内存块。而当用户不需要再使用这些内存块时,又可以释放回堆中供其他应用分配使用。
RT-Thread系统为了满足不同的需求,提供了不同的内存管理算法,分别是小内存管理算法、slab管理算法和memheap管理算法。
这几类内存堆管理算法在系统运行时只能选择其中之一或者完全不使用内存堆管理器,他们提供给应用程序的API接口完全相同。
因为内存堆管理器要满足多线程情况下的安全分配,会考虑多线程间的互斥问题,所以不要在中断服务例程中分配或释放动态内存块。
小内存管理算法是一个简单的内存分配算法。
初始时,它是一块大的内存。
当需要分配内存块时,将这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。
每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来,如图所示。
每个内存块(不管是已分配的内存块还是空闲的内存块)都包含一个数据头,其中包括:
另外,在每次分配内存块前,都会留出12字节数据头用于magic、used信息及链表节点使用。
返回给应用的地址实际上是这块内存块12字节以后的地址,前面的12字节数据头是用户永远不应该碰的部分。
释放时则是相反的过程,但分配器会查看前后相邻的内存块是否空闲,如果空闲则合并成一个大的空闲内存块。
RT-Thread 的 slab 分配器是在 DragonFly BSD 创始人 Matthew Dillon 实现的 slab 分配器基础上,针对嵌入式系统优化的内存分配算法。最原始的 slab 算法是 Jeff Bonwick 为 Solaris 操作系统而引入的一种高效内核内存分配算法。
RT-Thread的slab分配器实现主要是去掉了其中的对象构造及析构过程,只保留了纯粹的缓冲型的内存池算法。
slab分配器会根据对象的大小分成多个区(zone),也可以看成每类对象有一个内存池。
一个zone的大小在32K到128K字节之间,分配器会在堆初始化时根据堆的大小自动调整。
系统中的zone最多包括72种对象,一次最大能够分配16K的内存空间,如果超出了16K那么直接从页分配器中分配。
每个zone上分配的内存块大小是固定的,能够分配相同大小内存块的zone会链接在一个链表中,而72种对象的zone链表则放在一个数组(zone_array[])中统一管理。
内存分配
假设分配一个32字节的内存,slab内存分配器会先按照32字节的值,从zone array链表表头数组中找到相应的zone链表。
如果这个链表是空的,则向页分配器分配一个新的zone,然后从zone中返回第一个空闲内存块。
如果链表非空,则这个zone链表中的第一个zone节点必然有空闲块存在,那么就取相应的空闲块。
如果分配完成后,zone中所有空闲内存块都使用完毕,那么分配器需要把这个zone节点从链表中删除。
内存释放
分配器要找到内存块所在的zone节点,然后把内存块链接到zone的空闲内存块链表中。
如果此时zone的空闲链表指示出zone的所有内存块都已经释放,即zone是完全空闲的,那么当zone链表中全空闲zone达到一定数目后,系统就会把这个全空闲的zone释放到页面分配器中区。
memheap管理算法适用于系统含有多个地址可不连续的内存堆。
使用memheap内存管理可以简化系统存在多个内存堆时的使用:当系统中存在多个内存堆的时候,用户只需要在系统初始化时将多个所需的memheap初始化,并开启memheap功能就可以很方便地把多个memheap(地址可不连续)粘合起来用于系统的heap分配。
在开启memheap之后原来的heap功能将被关闭,两者只可以通过打开或关闭RT_USING_MEMHEAP_AS_HEAP来选择其一。
首先将多块内存加入memheap_item链表进行粘合。
当分配内存块时,会先从默认内存堆去分配内存,当分配不到时会查找memheap_item链表,尝试从其它的内存堆上分配内存块。应用程序不用关心当前分配的内存块位于哪个内存堆上,就像是在操作一个内存堆。
内存堆管理器可以分配任意大小的内存块,非常灵活和方便。
但存在明显的缺点:一是分配效率不高,在每次分配时,都要空闲内存块查找,二是容易产生内碎片。
为了提高内存分配的效率,并且避免内存碎片,RT-Thread 提供了另外一种内存管理方法:内存池(Memory Pool)。
内存池是一种内存分配方式,用于分配大量大小相同的小内存块,它可以极大地加快内存分配与释放的速度,且能尽量避免内存碎片化。
此外,RT-Thread的内存池支持线程挂起功能,当内存池中无空闲内存块时,申请线程会被挂起,直到内存池中有新的可用内存块,再将挂起的申请线程唤醒。
内存池的线程挂起功能非常适合需要通过内存资源进行同步的场合。
例如,播放音乐时,播放器线程会对音乐文件进行解码,然后发送到声卡驱动,从而驱动硬件播放音乐。
内存池控制块是操作系统用于管理内存池的一个数据结构,它会存放内存池的一些信息,例如内存池数据区域开始地址,内存块大小和内存块列表等,也包含内存块与内存块之间连接用的链表结构,因内存块不可用而挂起的线程等待事件集合等。
在 RT-Thread 实时操作系统中,内存池控制块由结构体 struct rt_mempool 表示。另外一种 C 表达方式 rt_mp_t,表示的是内存块句柄,在 C 语言中的实现是指向内存池控制块的指针。
struct rt_mempool
{
struct rt_object parent;
void *start_address; /* 内存池数据区域开始地址 */
rt_size_t size; /* 内存池数据区域大小 */
rt_size_t block_size; /* 内存块大小 */
rt_uint8_t *block_list; /* 内存块列表 */
/* 内存池数据区域中能够容纳的最大内存块数 */
rt_size_t block_total_count;
/* 内存池中空闲的内存块数 */
rt_size_t block_free_count;
/* 因为内存块不可用而挂起的线程列表 */
rt_list_t suspend_thread;
/* 因为内存块不可用而挂起的线程数 */
rt_size_t suspend_thread_count;
};
typedef struct rt_mempool* rt_mp_t;
内存池在创建时先向系统申请一大块内存,然后分成同样大小的多个小内存块,小内存块直接通过链表连接起来(此链表也称为空闲链表)。每次分配的时候,从空闲链表中取出链头上第一个内存块,提供给申请者。
物理内存中允许存在多个大小不同的内存池,每一个内存池又由多个空闲内存块组成,内核用它们来进行内存管理。当一个内存池对象被创建时,内存池对象就被分配给了一个内存池控制块,内存控制块的参数包括内存池名,内存缓冲区,内存块大小,块数以及一个等待线程队列。
内核负责给内存池分配内存池控制块,它同时也接收用户线程的分配内存块申请,当获得这些信息后,内核就可以从内存池中为内存池分配内存。内存池一旦初始化完成,内部的内存块大小将不能再做调整。
每一个内存池对象由上述结构组成,其中 suspend_thread 形成了一个申请线程等待列表,即当内存池中无可用内存块,并且申请线程允许等待时,申请线程将挂起在 suspend_thread 链表上。
创建内存池操作将会创建一个内存池对象并从堆上分配一个内存池。创建内存池是从对应内存池中分配和释放内存块的先决条件,创建内存池后,线程便可以从内存池中执行申请、释放等操作。创建内存池使用下面的函数接口,该函数返回一个已创建的内存池对象。
rt_mp_t rt_mp_create(const char* name,
rt_size_t block_count,
rt_size_t block_size);
使用该函数接口可以创建一个与需求的内存块大小、数目相匹配的内存池,前提当然是在系统资源允许的情况下(最主要的是内存堆内存资源)才能创建成功。创建内存池时,需要给内存池指定一个名称。然后内核从系统中申请一个内存池对象,然后从内存堆中分配一块由块数目和块大小计算得来的内存缓冲区,接着初始化内存池对象,并将申请成功的内存缓冲区组织成可用于分配的空闲块链表。