RT-Thread内核学习(认真系列) ---- (5)内存管理

 

一、概述 

计算机系统中,变量、中间数据一般存放在 RAM 中,只有在实际使用时才将它们从 RAM 调入到CPU 中进行运算。一些数据需要的内存大小需要在程序运行过程中根据实际情况确定,这就要求系统具有对内存空间进行动态管理的能力,在用户需要一段内存空间时,向系统申请,系统选择一段合适的内存空间分配给用户,用户使用完毕后,再释放回系统,以便系统将该段内存空间回收再利用。

RT-Thread 中的两种内存管理方式,分别是动态内存堆管理静态内存池管理。

1.1、内存管理的特点

由于实时系统中对时间的要求非常严格,内存管理往往要比通用操作系统要求苛刻得多:
           1)分配内存的时间必须是确定的。一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面。而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。
          2)内存碎片管理要及时,内存碎片随着内存不断被分配和释放,整个内存区域会产生越来越多的碎片(因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去),系统中还有足够的空闲内存,但因为它们地址并非连续,不能组成一块连续的完整内存块,会使得程序不能申请到大的内存。对于通用系统而言,这种不恰当的内存分配算法可以通过重新启动系统来解决 (每个月或者数个月进行一次),但是对于那些需要常年不间断地工作于野外的嵌入式系统来说,变得让人无法接受了。
        3)嵌入式系统的资源环境也是不尽相同,有些系统的资源比较紧张,只有数十 KB 的内存可供分配,而有些系统则存在数 MB 的内存,如何为这些不同的系统,选择适合它们的高效率的内存分配算法,就将变得复杂化

RT-Thread 操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。总体上可分为两类:内存堆管理与内存池管理。

二、内存堆管理

内存堆可以在当前资源满足的情况下,根据用户的需求分配任意大小的内存块。而当用户不需要再使用这些内存块时,又可以释放回堆中供其他应用分配使用。

内存堆管理根据具体内存设备划分为三种情况:
           1)针对小内存块的分配管理(小内存管理算法)。
           2)是针对大内存块的分配管理(slab 管理算法)。
           3)是针对多内存堆的分配情况(memheap 管理算法)。

小内存管理算法主要针对系统资源比较少,一般用于小于 2MB 内存空间的系统;而 slab 内存管理算法则主要是在系统资源比较丰富时,提供了一种近似多内存池管理算法的快速算法。除上述之外,RT-Thread还有一种针对多内存堆的管理算法,即 memheap 管理算法。memheap 方法适用于系统存在多个内存堆的情况,它可以将多个内存 “粘贴” 在一起,形成一个大的内存堆,用户使用起来会非常方便。

这几类内存堆管理算法在系统运行时只能选择其中之一或者完全不使用内存堆管理器,他们提供给应用程序的 API 接口完全相同。

注意:因为内存堆管理器要满足多线程情况下的安全分配,会考虑多线程间的互斥问题,所以请不要在中断服务例程中分配或释放动态内存块。因为它可能会引起当前上下文被挂起等待。

内存堆管理用于管理一段连续的内存空间,如下图所示,RT-Thread 将 “ZI 段结尾处” 到内存尾部的空间用作内存堆。

RT-Thread内核学习(认真系列) ---- (5)内存管理_第1张图片

2.1、小内存管理算法 

小内存管理算法是一个简单的内存分配算法。初始时,它是一块大的内存。当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来(两者是连在一起的),如下图所示:

RT-Thread内核学习(认真系列) ---- (5)内存管理_第2张图片

每个内存块(不管是已分配的内存块还是空闲的内存块)都包含一个数据头(占据12字节的空间,其中包括:
           1)magic:变数(或称为幻数),它会被初始化成 0x1ea0(即英文单词 heap),用于标记这个内存块是一个内存管理用的内存数据块;变数不仅仅用于标识这个数据块是一个内存管理用的内存数据块,实质也是一个内存保护字:如果这个区域被改写,那么也就意味着这块内存块被非法改写(正常情况下只有内存管理器才会去碰这块内存)。
          2)used:指示出当前内存块是否已经分配。                                                                                                                                     3)别的数据,诸如指向后面和前面的指针。

2.1.1、使用举例

内存管理的表现主要体现在内存的分配与释放上,小型内存管理算法可以用以下例子体现出来。如下图所示的内存分配情况,空闲链表指针 lfree 初始指向 32 字节的内存块。当用户线程要再分配一个 64 字节的内存块时,但此 lfree 指针指向的内存块只有 32 字节并不能满足要求,内存管理器会继续寻找下一内存块,当找到再下一块内存块,128 字节时,它满足分配的要求。因为这个内存块比较大,分配器将把此内存块进行拆分,余下的内存块(52 字节)继续留在 lfree 链表中,如下图分配 64 字节后的链表结构所示。

RT-Thread内核学习(认真系列) ---- (5)内存管理_第3张图片

另外,在每次分配内存块前,都会留出 12 字节数据头用于 magic、used 信息及链表节点使用返回给应用的地址实际上是这块内存块 12 字节以后的地址,前面的 12 字节数据头是用户永远不应该碰的部分(注:12 字节数据头长度会与系统对齐差异而有所不同)。

释放时则是相反的过程,但分配器会查看前后相邻的内存块是否空闲,如果空闲则合并成一个大的空闲内存块。

2.2、slab 管理算法(未能深入理解)

RT-Thread 的 slab 分配器是在 DragonFly BSD 创始人 Matthew Dillon 实现的 slab 分配器基础上,针对嵌入式系统优化的内存分配算法。最原始的 slab 算法是 Jeff Bonwick 为 Solaris 操作系统而引入的一种高效内核内存分配算法。

RT-Thread 的 slab 分配器实现主要是去掉了其中的对象构造及析构过程,只保留了纯粹的缓冲型的内存池算法。slab 分配器会根据对象的大小分成多个区(zone),也可以看成每类对象有一个内存池,如下图所示:

RT-Thread内核学习(认真系列) ---- (5)内存管理_第4张图片

一个 zone 的大小在 32K 到 128K 字节之间,分配器会在堆初始化时根据堆的大小自动调整。系统中的 zone 最多包括 72 种对象,一次最大能够分配 16K 的内存空间,如果超出了 16K 那么直接从页分配器中分配。每个 zone 上分配的内存块大小是固定的,能够分配相同大小内存块的 zone 会链接在一个链表中,而 72 种对象的 zone 链表则放在一个数组(zone_array[])中统一管理。

下面是内存分配器主要的两种操作:

(1)内存分配

假设分配一个 32 字节的内存,slab 内存分配器会先按照 32 字节的值,从 zone array 链表表头数组中找到相应的 zone 链表。如果这个链表是空的,则向页分配器分配一个新的 zone,然后从 zone 中返回第一个空闲内存块。如果链表非空,则这个 zone 链表中的第一个 zone 节点必然有空闲块存在(否则它就不应该放在这个链表中),那么就取相应的空闲块。如果分配完成后,zone 中所有空闲内存块都使用完毕,那么分配器需要把这个 zone 节点从链表中删除。

(2)内存释放

分配器需要找到内存块所在的 zone 节点,然后把内存块链接到 zone 的空闲内存块链表中。如果此时zone 的空闲链表指示出 zone 的所有内存块都已经释放,即 zone 是完全空闲的,那么当 zone 链表中全空闲 zone 达到一定数目后,系统就会把这个全空闲的 zone 释放到页面分配器中去。

2.3、memheap 管理算法

memheap 管理算法适用于系统含有多个地址可不连续的内存堆。使用 memheap 内存管理可以简化系统存在多个内存堆时的使用:当系统中存在多个内存堆的时候,用户只需要在系统初始化时将多个所需的memheap 初始化,并开启 memheap 功能就可以很方便地把多个 memheap(地址可不连续)粘合起来用于系统的 heap 分配。

注意:在开启 memheap 之后原来的 heap 功能将被关闭,两者只可以通过打开或关闭RT_USING_MEMHEAP_AS_HEAP 来选择其一。

2.3.1、实现机制

memheap 工作机制如下图所示,首先将多块内存加入 memheap_item 链表进行粘合。当分配内存块时,会先从默认内存堆去分配内存,当分配不到时会查找 memheap_item 链表,尝试从其他的内存堆上分配内存块。应用程序不用关心当前分配的内存块位于哪个内存堆上,就像是在操作一个内存堆。

RT-Thread内核学习(认真系列) ---- (5)内存管理_第5张图片

2.4、内存堆配置和初始化

针对小内存管理算法和slab管理算法,在使用内存堆时,必须要在系统初始化的时候进行堆的初始化,可以通过下面的函数接口完成:

void rt_system_heap_init(void* begin_addr,   //堆开始地址
                         void* end_addr);    //堆结束地址

在使用 memheap 堆内存时,必须要在系统初始化的时候进行堆内存的初始化,可以通过下面的函数接口完成:

rt_err_t rt_memheap_init(struct rt_memheap *memheap, //memheap 控制块
                         const char *name,           //内存堆的名称
                         void *start_addr,           //堆内存区域起始地址(新加入管理的堆内存)
                         rt_uint32_t size)           //堆内存大小

如果有多个不连续的 memheap 可以多次调用该函数将其初始化并加入 memheap_item 链表。

2.5、内存堆的管理方式

对内存堆的操作如下图所示,包含:初始化、申请内存块、释放内存,所有使用完成后的动态内存都应该被释放,以供其他程序的申请使用。

2.5.1、分配和释放内存块

从内存堆上分配用户指定大小的内存块,rt_malloc 函数会从系统堆空间中找到合适大小的内存块,然后把内存块可用地址返回给用户。函数接口如下:

void *rt_malloc(rt_size_t nbytes);

2.5.2、释放内存块

应用程序使用完从内存分配器中申请的内存后,必须及时释放,否则会造成内存泄漏。rt_free 函数会把待释放的内存还回给堆管理器中。在调用这个函数时用户需传递待释放的内存块指针,如果是空指针直接返回。

释放内存块的函数接口如下:

void rt_free (void *ptr);

2.5.3、重分配内存块

在已分配内存块的基础上重新分配内存块的大小(增加或缩小)。在进行重新分配内存块时,原来的内存块数据保持不变(缩小的情况下,后面的数据被自动截断)。可以通过下面的函数接口完成:

void *rt_realloc(void *rmem,  //指向已分配的内存块大小
                 rt_size_t newsize); //重新分配后总的大小

2.5.4、分配多内存块

从内存堆中分配连续内存地址的多个内存块,可以通过下面的函数接口完成:

void *rt_calloc(rt_size_t count,  //内存块数量
                rt_size_t size);  //内存块容量(每块大小)
//返回:指向第一个内存块地址的指针,并且所有分配的内存块都被初始化成零(成功)
//RT_NULL(失败)

2.5.5、设置内存钩子函数

在分配内存块过程中,用户可设置一个钩子函数,设置的钩子函数会在内存分配完成后进行回调。回调时,会把分配到的内存块地址和大小做为入口参数传递进去。

分配内存调用的函数接口如下:

void rt_malloc_sethook(void (*hook)(void *ptr, rt_size_t size));

//钩子函数形式
void hook(void *ptr,        //分配的内存块指针
          rt_size_t size); //内存块大小

释放内存调用的函数接口如下:

void rt_free_sethook(void (*hook)(void *ptr));

//钩子函数形式
void hook(void *ptr);//待释放的内存块指针

三、静态内存池

内存堆管理器可以分配任意大小的内存块,非常灵活和方便。但其也存在明显的缺点:一是分配效率不高,在每次分配时,都要空闲内存块查找;二是容易产生内存碎片

为了提高内存分配的效率,并且避内存碎片,RT-Thread 提供了另外一种内存管理方法:内存池(Memory Pool)。

内存池是一种内存分配方式,用于分配大量大小相同的小内存块(使用的时候也是一块一块的使用),它可以极大地加快内存分配与释放的速度,且能尽量避免内存碎片化。此外,RT-Thread 的内存池支持线程挂起功能,当内存池中无空闲内存块时,申请线程会被挂起,直到内存池中有新的可用内存块,再将挂起的申请线程唤醒。

3.1、内存块的分配机制

内存池在创建时先向系统申请一大块内存,然后分成同样大小的多个小内存块,小内存块直接通过链表连接起来(此链表也称为空闲链表)。每次分配的时候,从空闲链表中取出链头上第一个内存块,提供给申请者。从下图中可以看到,物理内存中允许存在多个大小不同的内存池,每一个内存池又由多个空闲内存块组成,内核用它们来进行内存管理。当一个内存池对象被创建时,内存池对象就被分配给了一个内存池控制块,内存控制块的参数包括内存池名,内存缓冲区,内存块大小,块数以及一个等待线程队列。

RT-Thread内核学习(认真系列) ---- (5)内存管理_第6张图片

内核负责给内存池分配内存池控制块,它同时也接收用户线程的分配内存块申请,当获得这些信息后,内核就可以从内存池中为内存池分配内存。内存池一旦初始化完成,内部的内存块大小将不能再做调整。每一个内存池对象由上述结构组成,其中 suspend_thread 形成了一个申请线程等待列表,即当内存池中无可用内存块,并且申请线程允许等待时,申请线程将挂起在 suspend_thread 链表上。

3.2、内存池的管理方式(API)

3.2.1、创建和删除内存池

创建内存池操作将会创建一个内存池对象并从堆上分配一个内存池。创建内存池是从对应内存池中分配和释放内存块的先决条件,创建内存池后,线程便可以从内存池中执行申请、释放等操作。创建内存池使用下面的函数接口:

rt_mp_t rt_mp_create(const char* name,      //
                     rt_size_t block_count, //内存块数量
                     rt_size_t block_size); //每块内存块容量

使用该函数接口可以创建一个与需求的内存块大小、数目相匹配的内存池,前提当然是在系统资源允许的情况下(最主要的是内存堆内存资源)才能创建成功。创建内存池时,需要给内存池指定一个名称。然后内核从系统中申请一个内存池对象,然后从内存堆中分配一块由块数目和块大小计算得来的内存缓冲区,接着初始化内存池对象,并将申请成功的内存缓冲区组织成可用于分配的空闲块链表。

删除内存池,会首先唤醒等待在该内存池对象上的所有线程(返回 -RT_ERROR),然后再释放已从内存堆上分配的内存池数据存放区域,然后删除内存池对象。

rt_err_t rt_mp_delete(rt_mp_t mp); //rt_mp_create 返回的内存池对象句柄

3.2.2、初始化和脱离内存池

初始化内存池跟创建内存池类似,只是初始化内存池用于静态内存管理模式,内存池控制块来源于用户在系统中申请的静态对象。另外与创建内存池不同的是,此处内存池对象所使用的内存空间是由用户指定的一个缓冲区空间,用户把缓冲区的指针传递给内存池控制块,其余的初始化工作与创建内存池相同。内存池块个数 = size / (block_size + 4 链表指针大小),计算结果取整数。

rt_err_t rt_mp_init(rt_mp_t mp,
                    const char* name,
                    void *start,           //内存池的起始位置
                    rt_size_t size,        //内存池数据区域大小(总的字节数)
                    rt_size_t block size); //内存块容量

脱离内存池将把内存池对象从内核对象管理器中脱离。使用该函数接口后,内核先唤醒所有等待在该内存池对象上的线程,然后将内存池对象从内核对象管理器中脱离。

rt_err_t rt_mp_detach(rt_mp_t mp);

3.2.3、分配和释放内存块

从指定的内存池中分配一个内存块,其中 time 参数的含义是申请分配内存块的超时时间。如果内存池中有可用的内存块,则从内存池的空闲块链表上取下一个内存块,减少空闲块数目并返回这个内存块;如果内存池中已经没有空闲内存块,则
判断超时时间设置:若超时时间设置为零,则立刻返回空内存块;若等待时间大于零,则把当前线程挂起在该内存池对象上,直到内存池中有可用的自由内存块,或等待时间到达。.

void *rt_mp_alloc (rt_mp_t mp, rt_int32_t time);

任何内存块使用完后都必须被释放,否则会造成内存泄露,释放内存块使用如下接口:

void rt_mp_free (void *block);

使用该函数接口时,首先通过需要被释放的内存块指针计算出该内存块所在的(或所属于的)内存池对象,然后增加内存池对象的可用内存块数目,并把该被释放的内存块加入空闲内存块链表上。接着判断该内存池对象上是否有挂起的线程,如果有,则唤醒挂起线程链表上的首线程。

 

 

 

 

 

 

 

 

 

 

 

 

 

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