改进一个实用的、可应用于单片机的内存管理模块
一般单片机的内存都比较小,而且没有MMU,malloc 与free的使用容易造成内存碎片。而且可能因为空间不足而分配失败,从而导致系统崩溃,因此应该慎用,或者自己实现内存管理。heap_malloc就是一个不会产生内存碎片的、适合单片机使用的内存管理模块。该模块主要改进于mem_malloc模块,一位大佬写的应用于单片机内存管理模块,原代码仓库:
https://gitee.com/liming2019/mem_malloc
该模块的使用并不会产生内存碎片,可以高效地利用单片机ram空间。但是原模块仅能将一个数组作为内存堆来使用,我尝试对其进行改进,使得可以声明多个数组作为内存堆来使用,且还可以实现堆的嵌套创建使用(还没尝试过,我想这个想法再多进程或多线程、又或者内存回收利用方面估计会有很好的应用)。改进后的模块地址为:
https://gitee.com/liu-guanglin/lgl_malloc
消除内存碎片的原理:
在该模块中,堆内的空间被划分成为三部分,低位地址的堆管理表空间,存储着三种参数:分配给用户的表序号,对应的分配内存的起始地址,还有分配的内存大小;中间的空闲内存空间,用来分配给用户于管理表存储的待分配空间;高位地址则是分配给用户的内存空间。
该堆空间是处于动态分配的过程中,因而当用户程序对自己申请到的堆空间进行重申请或释放自己申请到的空间时,为保证堆空间内不产生内存碎片,在移动内存空间的过程中会影响到其他无关的申请块。也就是用户申请到的内存空间的起始地址是处于可变化的状态下的,但是用户拿到的表序号是唯一不变的。因而如果用户需要操控自己所申请的对应的内存空间,那么唯一可行且安全的渠道就是先通过拿着的表序号来获取到对于的起始地址,然后再根据地址对自己处于堆空间中的内存地址进行操作。每一次操作都需要重复该过程。
mem_malloc测试验证(改进的模块兼容了原模块,即保留了默认堆空间)
实验过程很简单。准备一份开发板带串口打印的工程,下载mem_malloc,把mem_malloc.c、mem_malloc.h复制到工程目录下,并添加到工程里:
然后进行编译,编译过程可能会报错:
..\Src\mem_malloc.c(119): error: #852: expression must be a pointer to a complete object type
这份代码在不同编译器下编译情况不同。gcc下编译不会报错,在keil下编译报如上错误。
keil编译器更严格些。报错原因是对mem_block结构体的mem_ptr成员进行操作,而mem_ptr成员的类型是void*,而mem_ptr成员参与运算时的增、减偏移量取决于mem_ptr的类型,所以这里我们需要指定类型。
我们把相关报错代码修改如:
再次编译就正常了。
下面简单看一下mem_malloc的代码。
「mem_malloc.h:」
#ifndef __MEM_MALLOC_H__
#define __MEM_MALLOC_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
//内存堆-对象
typedef struct mem_heap
{
unsigned int sum; /*用于记录已使用的长度*/
unsigned int size; /*内存堆的长度*/
unsigned char* mem; /*内存堆的首地址*/
}mem_heap;
void print_heap_info(const mem_heap* heap);
void print_heap_hex(const mem_heap* heap);
int heap_malloc(mem_heap* heap,unsigned int msize);
int heap_realloc(mem_heap* heap,int id, unsigned int msize);
void *heap_buffer(const mem_heap* heap,int id);
int heap_free(mem_heap* heap,int id);
/**---------------保留实现默认堆空间--------------------**/
#define MEM_SIZE 128
static char mem[MEM_SIZE];
static mem_heap MEM={.sum=0,.size=MEM_SIZE,.mem=mem};
#define print_mem_info() print_heap_info(&MEM)
#define print_mem_hex() print_heap_hex(&MEM)
#define mem_malloc(msize) heap_malloc(&MEM,msize)
#define mem_realloc(id,msize) heap_realloc(&MEM,id,msize)
#define mem_buffer(id) heap_buffer(&MEM,id)
#define mem_free(id) heap_free(&MEM,id)
#ifdef __cplusplus
}
#endif
#endif
「mem_malloc.c:」
下面对mem_malloc进行测试验证。(与上面的不同,我使用的是Linux编译)
测试代码作者也有给出,这里我们简单测试即可,进行了一些删减及增加了一些注释:
#include "mem_malloc.h"
void main(void){
print_mem_info(); //打印内存信息
printf("------test_malloc-------\n");
int size=30;
printf("未使用的堆内存空间:\r\n");
print_mem_hex();
//1、使用 mem_malloc 或 mem_realloc 函数申请指定大小的内存块(返回内存块的表序号)
printf("使用mem_malloc()来申请堆内存空间\r\n");
unsigned int memId=mem_malloc(size);
if (memId==0)
{
printf("申请堆内存失败!\r\n");
return -1;
}
printf("得到的表序号为:memId:%d\r\n",memId);
print_mem_hex();
//2、当需要使用该内存块,使用表序号获取内存块的起始地址
char *p = mem_buffer(memId);
printf("得到第%d块堆空间的起始地址为:%o\r\n",memId,p);
print_mem_hex();
printf("往该块堆内存块填充数据0xFF\r\n");
memset(p, 0xFF, size);
print_mem_hex();
printf("重申请堆内存大小\r\n");
size=10;
if(mem_realloc(memId, size)==0){
printf("申请失败");
mem_free(memId);
return -1;
}
p = mem_buffer(memId);
printf("得到第%d块堆空间的起始地址为:%o\r\n",memId,p);
print_mem_hex();
printf("往该块堆内存块填充数据0xAA\r\n");
memset(p, 0xAA, size);
print_mem_hex();
mem_free(memId);
}
这里设定一个128字节的数组作为堆空间使用。其中数组前面存放的是申请到的内存块的信息,包括内存块地址、大小、索引信息,这三个数据各占4个字节,共12个字节。这里有设计到一个大小端模式的问题,STM32平台为小端模式,即数据的低位存储在内存的低地址中。
申请的内存块从128字节的尾部开始分配,再次申请的内存块依次往前移,释放的内存,则整体内存块往后移动,内存块之前不留空隙,即不产生内存碎片。
运行结果及解析:
测试2:使用新的数组作为堆空间(推荐静态数组)
#define SIZE 128
static char memer[128];
int main2(){
//1、声明静态内存堆对象
static mem_heap heap={.size=SIZE,.sum=0,.mem=&memer[0]};
printf("测试内存堆信息打印\r\n");
print_heap_info(&heap);
print_heap_hex(&heap);
int heapId=heap_malloc(&heap,50);
printf("申请堆内存的表序号:%d\r\n",heapId);
print_heap_hex(&heap);
//2、通过表序号获取堆空间的起始地址
printf("往申请到的内存空间打印0xAA数据\r\n");
char* p=heap_buffer(&heap,heapId);
if(p==NULL){
printf("获取地址错误\r\n");
return -1;
}
memset(p,0xAA,50);
print_heap_hex(&heap);
printf("重新申请内存空间\r\n");
if(heap_realloc(&heap,heapId,20)==0){
printf("重新申请内存空间失败\r\n");
}
print_heap_hex(&heap);
printf("往申请到的内存空间打印0xFF数据\r\n");
p=heap_buffer(&heap,heapId);
if(p==NULL){
printf("获取地址错误\r\n");
return -1;
}
memset(p,0xFF,50);
print_heap_hex(&heap);
printf("释放堆内存\r\n");
heap_free(&heap,heapId);
print_heap_hex(&heap);
}=
测试内存堆信息打印
------------mem_info--------------
sizeof(mem_block)=16
mem_start = -1305308960(0xb23290e0)
MEM_END = -1305308832(0xb2329160)
MEM_SIZE = 128(0x80)
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
申请堆内存的表序号:1
2e 91 32 b2 59 55 00 00 32 00 00 00
01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
往申请到的内存空间打印0xAA数据
2e 91 32 b2 59 55 00 00 32 00 00 00
01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 aa aa aa aa aa aa
aa aa aa aa aa aa aa aa aa aa aa aa
aa aa aa aa aa aa aa aa aa aa aa aa
aa aa aa aa aa aa aa aa aa aa aa aa
aa aa aa aa aa aa aa aa
重新申请内存空间
4c 91 32 b2 59 55 00 00 14 00 00 00
01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
aa aa aa aa aa aa aa aa aa aa aa aa
aa aa aa aa aa aa aa aa
往申请到的内存空间打印0xFF数据
4c 91 32 b2 59 55 00 00 14 00 00 00
01 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
ff ff ff ff ff ff ff ff ff ff ff ff
ff ff ff ff ff ff ff ff
释放堆内存
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00