1)实验平台:【正点原子】 NANO STM32F103 开发板
2)摘自《正点原子STM32 F1 开发指南(NANO 板-HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子
第二十九章 内存管理实验
上一章,我们在 STM32 FLASH 写入的时候,需要一个 512 字节的 16 位数组,实际上占用了 1K 字节,而这个数组几乎只能给 STM32FLASH_Write 一个函数使用,其实这是给常浪费内容的一种做法,好的办法是:我需要的时候,申请 1K 字节,用完了我释放掉。这样就不会出现一个大数组仅供一个函数使用的浪费现象了,这种内存的申请和释放,就需要用到内存管理。本章,我们将学习内存管理,实现对内存的动态管理。本章分为如下几个部分:
29.1 内存管理简介
29.2 硬件设计
29.3 软件设计
29.4 下载验证
29.1 内存管理简介
内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。内存管理的实现方法有很多种,他们其实最终都是要实现 2 个函数:malloc 和 free;malloc 函数用于内存申请,free 函数用于内存释放。
本章,我们介绍一种比较简单的办法来实现:分块式内存管理。下面我们介绍一下该方法的实现原理,如图 29.1.1 所示:
从上图可以看出,分块式内存管理由内存池和内存管理表两部分组成。内存池被等分为 n块,对应的内存管理表,大小也为 n,内存管理表的每一个项对应内存池的一块内存。内存管理表的项值代表的意义为:当该项值为 0 的时候,代表对应的内存块未被占用,当该项值非零的时候,代表该项对应的内存块已经被占用,其数值则代表被连续占用的内存块数。比如某项值为 10,那么说明包括本项对应的内存块在内,总共分配了 10 个内存块给外部的某个指针。
内寸分配方向如图所示,是从顶→底的分配方向。即首先从最末端开始找空内存。当内存管理刚初始化的时候,内存表全部清零,表示没有任何内存块被占用。
分配原理
当指针 p 调用 malloc 申请内存的时候,先判断 p 要分配的内存块数(m),然后从第 n 项
开始,向下查找,直到找到 m 块连续的空内存块(即对应内存管理表项为 0),然后将这 m 个
内存管理表项的值都设置为 m(标记被占用),最后,把最后的这个空内存块的地址返回指针
p,完成一次分配。注意,如果当内存不够的时候(找到最后也没找到连续的 m 块空闲内存),
则返回 NULL 给 p,表示分配失败。
释放原理
当 p 申请的内存用完,需要释放的时候,调用 free 函数实现。free 函数先判断 p 指向的内
存地址所对应的内存块,然后找到对应的内存管理表项目,得到 p 所占用的内存块数目 m(内
存管理表项目的值就是所分配内存块的数目),将这 m 个内存管理表项目的值都清零,标记释
放,完成一次内存释放。
关于分块式内存管理的原理,我们就介绍到这里。
29.2 硬件设计
本章实验功能简介:开机后,显示提示信息,等待外部输入。KEY0 用于申请内存,每次
申请 2K 字节内存。KEY1 用于写数据到申请到的内存里面。KEY2 用于释放内存。。DS0 用
于指示程序运行状态。本章我们还可以通过 USMART 调试,测试内存管理函数。
本实验用到的硬件资源有:
1) 指示灯 DS0
2) KEY0/KEY1/KEY2 等三个按键
3) 串口
这些我们都已经介绍过,接下来我们开始软件设计。
29.3 软件设计
本章,我们将内存管理部分单独做一个分组,在工程目录下新建一个 MALLOC 的文件夹,
然后新建 malloc.c 和 malloc.h 两个文件,将他们保存在 MALLOC 文件夹下。
在 MDK 新建一个 MALLOC 的组,然后将 malloc.c 文件加入到该组,并将 MALLOC 文件
夹添加到头文件包含路径。
打开 malloc.c 文件,代码如下:
//内存池(4 字节对齐)
__align(4) u8 membase[MEM_MAX_SIZE];
//SRAM 内存池
//内存管理表
u16 memmapbase[MEM_ALLOC_TABLE_SIZE]; //SRAM 内存池 MAP
//内存管理参数
const u32 memtblsize=MEM_ALLOC_TABLE_SIZE;//内存表大小
const u32 memblksize=MEM_BLOCK_SIZE; //内存分块大小
const u32 memsize=MEM_MAX_SIZE;
//内存总大小
//内存管理控制器
struct _m_mallco_dev mallco_dev=
{
mem_init,
//内存初始化
mem_perused,
//内存使用率
membase,
//内存池
memmapbase,
//内存管理状态表
0,
//内存管理未就绪
};
//复制内存
//*des:目的地址
//*src:源地址
//n:需要复制的内存长度(字节为单位)
void mymemcpy(void *des,void *src,u32 n)
{
u8 *xdes=des;
u8 *xclass="lazy" data-original=src;
while(n--)*xdes++=*xsrc++;
}
//设置内存
//*s:内存首地址
//c :要设置的值
//count:需要设置的内存大小(字节为单位)
void mymemset(void *s,u8 c,u32 count)
{
u8 *xs = s;
while(count--)*xs++=c;
}
//内存管理初始化
void mem_init(void)
{
mymemset(mallco_dev.memmap, 0,memtblsize*2);//内存状态表数据清零
mymemset(mallco_dev.membase, 0,memsize); //内存池所有数据清零
mallco_dev.memrdy=1;
//内存管理初始化 OK
}
//获取内存使用率
//返回值:使用率(0~100)
u8 mem_perused(void)
{
u32 used=0;
u32 i;
for(i=0;i<memtblsize>
{
if(mallco_dev.memmap[i])used++;
}
return (used*100)/(memtblsize);
}
//内存分配(内部调用)
//memx:所属内存块
//size:要分配的内存大小(字节)
//返回值:0XFFFFFFFF,代表错误;其他,内存偏移地址
u32 mem_malloc(u32 size)
{
signed long offset=0;
u16 nmemb; //需要的内存块数
u16 cmemb=0;//连续空内存块数
u32 i;
if(!mallco_dev.memrdy)mallco_dev.init(); //未初始化,先执行初始化
if(size==0)return 0XFFFFFFFF;
//不需要分配
nmemb=size/memblksize;
//获取需要分配的连续内存块数
if(size%memblksize)nmemb++;
for(offset=memtblsize-1;offset>=0;offset--) //搜索整个内存控制区
{
if(!mallco_dev.memmap[offset])cmemb++; //连续空内存块数增加
else cmemb=0;
//连续内存块清零
if(cmemb==nmemb)
//找到了连续 nmemb 个空内存块
{
for(i=0;i<nmemb>
//标注内存块非空
{
mallco_dev.memmap[offset+i]=nmemb;
}
return (offset*memblksize); //返回偏移地址
}
}
return 0XFFFFFFFF;//未找到符合分配条件的内存块
}
//释放内存(内部调用)
//offset:内存地址偏移
//返回值:0,释放成功;1,释放失败;
u8 mem_free(u32 offset)
{
int i;
if(!mallco_dev.memrdy)//未初始化,先执行初始化
{
mallco_dev.init();
return 1;//未初始化
}
if(offset<memsize>
{
int index=offset/memblksize; //偏移所在内存块号码
int nmemb=mallco_dev.memmap[index];//内存块数量
for(i=0;i<nmemb>
//内存块清零
{
mallco_dev.memmap[index+i]=0;
}
return 0;
}else return 2;//偏移超区了.
}
//释放内存(外部调用)
//ptr:内存首地址
void myfree(void *ptr)
{
u32 offset;
if(ptr==NULL)return;//地址为 0.
offset=(u32)ptr-(u32)mallco_dev.membase;
mem_free(offset); //释放内存
}
//分配内存(外部调用)
//size:内存大小(字节)
//返回值:分配到的内存首地址.
void *mymalloc(u32 size)
{
u32 offset;
offset=mem_malloc(size);
if(offset==0XFFFFFFFF)return NULL;
else return (void*)((u32)mallco_dev.membase+offset);
}
//重新分配内存(外部调用)
//*ptr:旧内存首地址
//size:要分配的内存大小(字节)
//返回值:新分配到的内存首地址.
void *myrealloc(void *ptr,u32 size)
{
u32 offset;
offset=mem_malloc(size);
if(offset==0XFFFFFFFF)return NULL;
else
{
mymemcpy((void*)((u32)mallco_dev.membase+offset),ptr,size);
//拷贝旧内存内容到新内存
myfree(ptr);
//释放旧内存
return (void*)((u32)mallco_dev.membase+offset); //返回新内存首地址
}
}
这里,我们通过内存管理控制器 mallco_dev 结构体(mallco_dev 结构体见 malloc.h),实
现对内存池的管理控制。内部 SRAM 内存池,定义为:
__align(4) u8 membase[MEM_MAX_SIZE];//SRAM 内存池
其中,MEM1_MAX_SIZE 是在 malloc.h 里面定义的内存池大小,__align(4)定义内存池为 4
字节对齐,这个非常重要!如果不加这个限制,在某些情况下(比如分配内存给结构体指针),
可能出现错误,所以一定要加上这个。
此部分代码的核心函数为:mem_malloc 和 mem_free,分别用于内存申请和内存释放。思
路就是我们在 29.1 接所介绍的那样分配和释放内存,不过这两个函数只是内部调用,外部调用
我们使用的是 mymalloc 和 myfree 两个函数。其他函数我们就不多介绍了,然后打开 malloc.h,
该文件代码如下:
#ifndef NULL
#define NULL 0
#endif
//内存参数设定.
#define MEM_BLOCK_SIZE
32
//内存块大小为 32 字节
#define MEM_MAX_SIZE
10*1024
//最大管理内存 10K
#define MEM_ALLOC_TABLE_SIZE MEM_MAX_SIZE/MEM_BLOCK_SIZE //内存表大小
//内存管理控制器
struct _m_mallco_dev
{
void (*init)(void);
//初始化
u8 (*perused)(void);
//内存使用率
u8 *membase;
//内存池
u16 *memmap;
//内存管理状态表
u8 memrdy;
//内存管理是否就绪
};
extern struct _m_mallco_dev mallco_dev; //在 mallco.c 里面定义
void mymemset(void *s,u8 c,u32 count);
//设置内存
void mymemcpy(void *des,void *src,u32 n);//复制内存
void mem_init(void);
//内存管理初始化函数(外/内部调用)
u32 mem_malloc(u32 size);
//内存分配(内部调用)
u8 mem_free(u32 offset);
//内存释放(内部调用)
u8 mem_perused(void);
//得内存使用率(外/内部调用)
////////////////////////////////////////////////////////////////////////////////
//用户调用函数
void myfree(void *ptr);
//内存释放(外部调用)
void *mymalloc(u32 size);
//内存分配(外部调用)
void *myrealloc(void *ptr,u32 size);
//重新分配内存(外部调用)
这部分代码,定义了很多关键数据,比如内存块大小的定义:MEM_BLOCK_SIZE 都是 32
字节。内存池总大小为 10K。MEM_ALLOC_TABLE_SIZE 代表内存池的内存管理表大小。
从这里可以看出,如果内存分块越小,那么内存管理表就越大,当分块为 2 字节 1 个块的
时候,内存管理表就和内存池一样大了(管理表的每项都是 u16 类型)。显然是不合适的,我
们这里取 32 字节,比例为 1:16,内存管理表相对就比较小了。
其他就不多说了,大家自行看代码理解就好。最后,打开 main.c 文件,修改代码如下:
int main(void)
{
u8 key;
u8 i=0;
u8 *p=0;
u8 *tp=0;
u8 paddr[18];
//存放 P Addr:+p 地址的 ASCII 值
HAL_Init(); //初始化 HAL 库
Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟,72M
delay_init(72); //初始化延时函数
uart_init(115200);
//串口初始化为 115200
LED_Init();
//初始化与 LED 连接的硬件接口
usmart_dev.init(72);
//初始化 USMART
KEY_Init();
//按键初始化
mem_init();
//初始化内存池
printf("NANO STM32\\r\\n");
printf("MALLOC TEST\\r\\n");
printf("KEY0:Malloc\\r\\n");
printf("KEY1:Write Data\\r\\n");
printf("KEY2:Free\\r\\n");
while(1)
{
key=KEY_Scan(0);//不支持连按
switch(key)
{
case 0:
//没有按键按下
break;
case KEY0_PRES: //KEY0 按下
p=mymalloc(2048);//申请 2K 字节
if(p!=NULL)sprintf((char*)p,"Memory Malloc Test%03d",i);
//向 p 写入一些内容
break;
case KEY1_PRES: //KEY1 按下
if(p!=NULL)
{
sprintf((char*)p,"Memory Malloc Test%03d",i);//更新显示内容
printf("%s\\r\\n",p);//显示 P 的内容
}
break;
case KEY2_PRES: //KEY2 按下
myfree(p);
//释放内存
p=0;
//指向空地址
break;
}
if(tp!=p)
{
tp=p;
printf("\\r\\nSRAM USED:%d%%\\r\\n",mem_perused());//显示内存使用率
sprintf((char*)paddr,"P Addr:0X%08X",(u32)tp);
printf("%s\\r\\n",paddr);//显示 p 的地址
if(p) printf("%s\\r\\n",p);//显示 P 的内容
}
delay_ms(10);
i++;
if((i%20)==0)//DS0 闪烁.
{
LED0=!LED0;
}
}
}
该部分代码比较简单,主要是对 mymalloc 和 myfree 的应用。不过这里提醒大家,如果对
一个指针进行多次内存申请,而之前的申请又没释放,那么将造成“内存泄露”,这是内存管
理所不希望发生的,久而久之,可能导致无内存可用的情况!所以,在使用的时候,请大家一
定记得,申请的内存在用完以后,一定要释放。
另外,本章希望利用 USMART 调试内存管理,所以在 USMART 里面添加了 mymalloc 和
myfree 两个函数,用于测试内存分配和内存释放。大家可以通过 USMART 自行测试
29.4 下载验证
在代码编译成功之后,我们先打开串口调试助手,然后下载代码到 ALIENTEK NANO
STM32F103 上,得到如图 29.4.1 所示界面:
可以看到,提示我们通过按键去操作,此时我们按下 KEY0,就可以看到内部 SRAM 内存
被使用 20%了,同时看到下面提示了指针 p 所指向的地址(其实就是被分配到的内存地址)和
内容。多按几次 KEY0,可以看到内存使用率持续上升(注意对比 p 的值,可以发现是递减的,
说明是从顶部开始分配内存!),此时如果按下 KEY2,可以发现内存使用率降低了 20%,但
是再按 KEY2 将不再降低,说明“内存泄露”了。这就是前面提到的对一个指针多次申请内存,
而之前申请的内存又没释放,导致的“内存泄露”。
KEY1 键用于更新 p 的内容,更新后的内容将重新打印在串口调试助手上面。
本章,我们还可以借助 USMART,测试内存的分配和释放,有兴趣的朋友可以动手试试。
如图 29.4.2 所示:
图中,我们先申请了 4660 字节的内存,然后得到申请到的内存首地址:0X200017EC,说
明我们申请内存成功(如果不成功,则会收到 0),然后释放内存的时候,参数是指针的地址,
即执行:myfree(0X200017EC),就可以释放我们申请到的内存。其他情况,大家可以自行测试
并分析。
"/<nmemb>/<memsize>/<nmemb>/<memtblsize>閱讀更多 正點原子 的文章