图解内存分配算法 – 小内存分配算法
文章目录
- 图解内存分配算法 -- 小内存分配算法
- 1. 算法介绍
- 2. 算法图解
- 2.1 约定
- 2.2 数据结构介绍
- 2.3 初始化
- 2.4 第一次 malloc 40字节
- 2.5 第二次 malloc 18 字节
- 2.6 第三次 malloc 20字节
- 2.7 第四次 malloc 40字节
- 2.8 第一次 free
- 2.9 第二次 free 内存合并处理碎片
- 3. 总结
1. 算法介绍
小内存分配算法常用于内存大小在 2M 字节以下的内存单元分配管理,小内存分配算法是一种动态内存管理算法,且在内存块释放时支持合并处理以减少内存碎片,是一种高效的内存分配算法,常用于嵌入式MCU中的内存管理。
此内存分配算法源码参考自:rt-thread mem.c
2. 算法图解
2.1 约定
为了方便大家阅读,以及本博客编写,对以下名称进行约定,以下约定适用于本篇博文所有内容:
-
内存单元:内存分配算法所管理的整个内存区域
-
内存块:每次malloc申请时所从内存单元上分割下来的一块内存区域(包含内存块数据管理部分字节)
2.2 数据结构介绍
每一个分配的内存块均由一个数据结构管理,其位于申请的每一块内存块的头部,其数据结构如下:
struct heap_mem
{
uint16_t magic;
uint16_t used;
int next;
int prev;
}
对应成员含义如下:
-
magic
: 代表一个内存段的头部,固定填充为一个固定字段,如 0x1ea0 -
used
: 指示当前内存段是否使用 -
next
/prev
: 双向链表,通过此两个指针将每个内存段链接到链表进行管理
如下图所示:
2.3 初始化
将一块内存单元分配给小内存分配算法进行管理,其首先第一步便是对此内存单元进行初始化。
小内存分配算法其管理数据结构、信息将保存在此内存单元中,因此首先需要做的是:
-
以默认值填充内存单元(可选)
-
在对应内存地址上构造对应的数据结构,存储内存管理结构信息(重点)
-
对传入的内存单元进行字节对齐处理,内存管理单元实际使用的起始地址将是传入地址进行字节对齐后的地址。这通常是为了提高cpu读写速率,内存对齐有利于提升 cpu 访问速度。
小内存管理算法在初始化的时候会在内存单元的头部和尾部各创建两个内存块,用来标记头和尾,此两个内存块内无有效内存,仅包含管理数据头,如下图所示:
同时会创建以下三个关键指针:
rt_uint8_t *heap_ptr;
struct heap_mem *lfree;
struct heap_mem *heap_end;
其各自的含义分别是:
heap_ptr
: 指向内存单元的有效起始地址,这个地址不一定时内存单元的地址,其是内存单元进行字节对齐之后的地址lfree
: 此指针随着内存单元的不断申请和释放会不停的移动,其始终指向起始地址最小的那一块空闲内存块,注意是起始地址最小,而不是大小最小!heap_end
: 始终指向内存单元的最后一块内存,代表此内存单元的尾部
为了方便大家理解,从更全局的视角体会内存分配过程,后续图解中不再对每块内存头部进行展开;使用蓝色矩形代表 used = 0
的空闲块的管理头;使用粉红色矩形代表 used = 1
的已使用块的管理头。
初始化之后的内存单元如下图所示:
2.4 第一次 malloc 40字节
从 lfree
所在的节点开始,遍历整个链表,找到具备申请字节大小(40Byte)+ 内存块管理头大小的空间,之后进行分配,此时会将一个大的内存块进行拆分,拆分成两个,一个用于此次的malloc申请,剩余的空闲块保留再链表上。
分配的时候会再创建一个新的内存块头部,记录被剩余的那个空闲块,并将其链接到链表里。
注意 malloc 返回的地址实际是内存管理头后的一个字节地址!
此外记得移动 lfree 指针指向新的地址最小的内存块~
2.5 第二次 malloc 18 字节
和第一次一样,继续从 lfree
开始找能满足的空闲内存块:
- 如果大小刚好满足申请内存块 + 管理头,则直接修改内存块管理头
used
字段为 1 - 如果大小小于申请内存块 + 管理头,则继续往链表后面查找下一个空闲块
- 如果大小大于申请内存块+ 管理头,则对此大内存块进行拆分,一部分用于用户使用,剩余的新建一个空闲块管理头将其链接到链表里面便于后续使用及管理。
- 之后修改
lfree
节点指向新的地址最小的内存块
发现没有每次,都是从 lfree
这个节点下手从链表内寻找符合要求的空闲块,由于 lfree
用于指向地址最小的空闲内存块,因此寻找的时候可以每次单方向寻找即可,这也是为什么每次把内存分配完之后都需要调整 lfree
指向的根本原因了!
2.6 第三次 malloc 20字节
此次 malloc
和之前的 malloc
有一个差异点,从图中我们可以看到新申请的内存和前面的内存没有连续,之间存在部分空闲字节未使用,这是由于字节对齐所导致的细小碎片,比如我们内存单元是按照4字节对齐,那个此碎片大小可能为 1Byte、2Byte、3Byte,同样这是为了提升cpu访问速率而支持的内存对齐的,后面也讲到如何处理这些碎片,所以不用担心
2.7 第四次 malloc 40字节
2.8 第一次 free
好了,接下来要进行 free 操作了,传入 malloc 的时候返回的地址,在 malloc 的时候返回给用户的是此块内存块的内存管理头后的一个字节的地址,因此通过此地址往前移内存管理头大小的字节便可以找到对应的内存块管理头了!
同样通过 magic 字段验证此块管理头是正常的头,否则应该报错进入异常处理!
找到内存头之后将此块内存头的 used
字段修改为0,即可完成释放了,后面malloc的时候便可使用此块内存,如此简单!对了,free的时候也需要记得判断是否需要移动 lfree
指针~
2.9 第二次 free 内存合并处理碎片
紧接着,我们进行第二次 free 操作,如下图,操作依旧如第一次操作。
进行第二次 free 之后通过链表进行检查,发现 next 指向的下一个内存管理头,其 used
成员也是0,因此判断到此时可以进行内存块合并了!那合并又是如何实现的呢?其实这也非常的简单!
不就是将此次释放的内存块的 next
的内容修改为 next -> next
就好了嘛!!!也就是 next 指向下一块内存管理头内记录的它的下一块内存,这样把后面这块空闲内存从链表里面移除掉了,后续malloc分配的时候,后面的空间就都是我的了!
ps: 当然记得把后面的后面那块的 prev
也修改为 prev->prev
此外,经过上面的这一手骚操作,之前残留的几个字节为了字节对齐而造成的碎片是不是也合并进去了,碎片也就处理完了!
到此为止,大家有没有想过,这样子的话,搞个单向链表不就好了吗,干嘛大费周章搞个双向链表,是不是有点浪费?
其实并没有,上面我们展示的是第二次释放的时候,释放的内存后面有一个空闲内存,如果其前面也有一个空闲内存,那么如何快速按照上述操作进行内存合并呢?这不就要使用到 prev 指针了,这也就是为什么需要使用双向链表的原因了!
3. 总结
以上便是对于小内存分配算法的完整图解,思路还是很清晰的,理解清楚了之后不妨思考一下此算法的优缺点是什么,欢迎大家将各自的思考在评论区进行交流讨论!也建议大家对其源码进行阅读,有了这份指南再去阅读源码,相信会吃的更加透彻!
创作不易,转载请注明出处!
关注、点赞+收藏,可快速查收博主有关分享!
如果你觉得我文章写的不错,欢迎点赞、关注+收藏,谢谢~,以下我的一些推荐:
相关推荐:
-
专栏:文件系统专栏(点击跳转)
-
专栏:RT-Thread内核(点击跳转)
-
专栏:物联网ESP32(点击跳转)
-
专栏:电机控制专栏(点击跳转)
-
其他专栏,去主页看看吧~