RT-Thread内核----内存管理

内存管理

在计算系统中,通常存储空间可以分为两种:内部存储空间和外部存储空间。

内部存储空间通常访问速度比较快,能够按照变量地址随机地访问,也就是我们通常所说的 RAM(随机存储器),可以把它理解为电脑的内存;

而外部存储空间内所保存的内容相对来说比较固定,即使掉电后数据也不会丢失,这就是通常所讲的 ROM(只读存储器),可以把它理解为电脑的硬盘。

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

这章主要介绍 RT-Thread 中的两种内存管理方式,动态内存堆管理和静态内存池管理,学完本章会了解 RT-Thread 的内存管理原理及使用方式。

内存管理的功能特点

由于实时系统中对时间的要求非常严格,内存管理往往要比通用操作系统要求苛刻得多:

1)分配内存的时间必须是确定的。一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面。而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。

2)随着内存不断被分配和释放,整个内存区域会产生越来越多的碎片(因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去),系统中还有足够的空闲内存,但因为它们地址并非连续,不能组成一块连续的完整内存块,会使得程序不能申请到大的内存。对于通用系统而言,这种不恰当的内存分配算法可以通过重新启动系统来解决 (每个月或者数个月进行一次),但是对于那些需要常年不间断地工作于野外的嵌入式系统来说,就变得让人无法接受了。

3)嵌入式系统的资源环境也是不尽相同,有些系统的资源比较紧张,只有数十 KB 的内存可供分配,而有些系统则存在数 MB 的内存,如何为这些不同的系统,选择适合它们的高效率的内存分配算法,就将变得复杂化。

RT-Thread 操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。

总体上可分为两类:内存堆管理与内存池管理,而内存堆管理又根据具体内存设备划分为三种情况:

第一种是针对小内存块的分配管理(小内存管理算法);

第二种是针对大内存块的分配管理(slab 管理算法);

第三种是针对多内存堆的分配情况(memheap 管理算法)

内存堆管理

内存堆管理用于管理一段连续的内存空间,在参考文档中 《内核基础》 章节有介绍过 RT-Thread 的内存分布情况,

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

RT-Thread 内存分布

内存堆可以在当前资源满足的情况下,根据用户的需求分配任意大小的内存块。

而当用户不需要再使用这些内存块时,又可以释放回堆中供其他应用分配使用。

RT-Thread 系统为了满足不同的需求,提供了不同的内存管理算法,分别是小内存管理算法、slab 管理算法和 memheap 管理算法。

小内存管理算法主要针对系统资源比较少,一般用于小于 2MB 内存空间的系统;

而 slab 内存管理算法则主要是在系统资源比较丰富时,提供了一种近似多内存池管理算法的快速算法。

除上述之外,RT-Thread 还有一种针对多内存堆的管理算法,即 memheap 管理算法。memheap 方法适用于系统存在多个内存堆的情况,它可以将多个内存 “粘贴” 在一起,形成一个大的内存堆,用户使用起来会非常方便。

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

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

小内存管理算法

小内存管理算法是一个简单的内存分配算法。

初始时,它是一块大的内存。当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来。

在4.1.0之前使用此实现

如下图所示:

小内存管理工作机制图

每个内存块(不管是已分配的内存块还是空闲的内存块)都包含一个数据头,其中包括:

1)magic:变数(或称为幻数),它会被初始化成 0x1ea0(即英文单词 heap),用于标记这个内存块是一个内存管理用的内存数据块;变数不仅仅用于标识这个数据块是一个内存管理用的内存数据块,实质也是一个内存保护字:如果这个区域被改写,那么也就意味着这块内存块被非法改写(正常情况下只有内存管理器才会去碰这块内存)。

2)used:指示出当前内存块是否已经分配。

内存管理的表现主要体现在内存的分配与释放上,小型内存管理算法可以用以下例子体现出来。

4.1.0及以后版本使用此实现

如下图所示:

img

堆开始:堆起始地址存储内存使用信息堆起始地址指针堆结束地址指针最小堆空闲地址指针和内存大小

无论是已分配的内存块还是空闲的内存块都包含一个数据头,包括:

pool_ptr:小内存对象地址。如果内存块的最后一位为1,则标记为使用。如果内存块的最后一位为0,则标记为未使用。可以通过该地址计算快速获得小内存算法结构体成员。

如下图所示的内存分配情况,空闲链表指针 lfree 初始指向 32 字节的内存块。

当用户线程要再分配一个 64 字节的内存块时,但此 lfree 指针指向的内存块只有 32 字节并不能满足要求,内存管理器会继续寻找下一内存块,当找到再下一块内存块,128 字节时,它满足分配的要求。因为这个内存块比较大,分配器将把此内存块进行拆分,余下的内存块(52 字节)继续留在 lfree 链表中,如下图分配 64 字节后的链表结构所示。

小内存管理算法链表结构示意图 1

小内存管理算法链表结构示意图 2

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

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

slab 管理算法

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

RT-Thread 的 slab 分配器实现主要是去掉了其中的对象构造及析构过程,只保留了纯粹的缓冲型的内存池算法。

slab 分配器会根据对象的大小分成多个区(zone),也可以看成每类对象有一个内存池,如下图所示:

slab 内存分配结构图

一个 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 释放到页面分配器中去。

memheap 管理算法

memheap 管理算法适用于系统含有多个地址可不连续的内存堆

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

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

memheap 工作机制如下图所示,首先将多块内存加入 memheap_item 链表进行粘合。当分配内存块时,会先从默认内存堆去分配内存,当分配不到时会查找 memheap_item 链表,尝试从其他的内存堆上分配内存块

应用程序不用关心当前分配的内存块位于哪个内存堆上,就像是在操作一个内存堆。

memheap 处理多内存堆

内存堆配置和初始化

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

void rt_system_heap_init(void* begin_addr, void* end_addr);

这个函数会把参数 begin_addr,end_addr 区域的内存空间作为内存堆来使用。

下表描述了该函数的输入参数:

rt_system_heap_init() 的输入参数

参数 描述
begin_addr 堆内存区域起始地址
end_addr 堆内存区域结束地址

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

rt_err_t rt_memheap_init(struct rt_memheap  *memheap,
                        const char  *name,
                        void        *start_addr,
                        rt_uint32_t size)

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

下表描述了rt_memheap_init() 的输入参数与返回值

参数 描述
memheap memheap 控制块
name 内存堆的名称
start_addr 堆内存区域起始地址
size 堆内存大小
返回 ——
RT_EOK 成功

内存堆的管理方式

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

内存堆的操作

分配和释放内存块

从内存堆上分配用户指定大小的内存块,函数接口如下:

void *rt_malloc(rt_size_t nbytes);

rt_malloc 函数会从系统堆空间中找到合适大小的内存块,然后把内存块可用地址返回给用户。

下表描述了rt_malloc() 的输入参数和返回值:

参数 描述
nbytes 需要分配的内存块的大小,单位为字节 (1B = 8 bit)
返回 ——
分配的内存块地址 成功
RT_NULL 失败

对 rt_malloc 的返回值进行判空是非常有必要的。应用程序使用完从内存分配器中申请的内存后,必须及时释放,否则会造成内存泄漏。

使用动态分配内存示例
int *pi;
pi = rt_malloc(100);
if(pi == NULL) 
{
   
   
    rt_kprintf("malloc failed\r\n");
}

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

void rt_free (void *ptr);

rt_free 函数会把待释放的内存还回给堆管理器中。在调用这个函数时用户需传递待释放的内存块指针,如果是空指针直接返回。

下表rt_free() 的输入参数

参数 描述
ptr 待释放的内存块指针
#include "rtthread.h"
/*
使用实例:

rt_free 的参数必须是以下其一:

- 是 NULL 。
- 是一个先前从 rt_malloc、 rt_calloc 或 rt_realloc 返回的值。

*/

int main()
{
   
   

    /*
    进行对ptr赋值,以null值为例子
    */
    int *ptr = RT_NULL;
    /* 释放内存块指针 */
    rt_free(ptr);
}
重分配内存块

在已分配内存块的基础上重新分配内存块的大小(增加或缩小),可以通过下面的函数接口完成:

void *rt_realloc(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值