Keil的动态内存管理实现——mallo和free函数
在使用51单片机的时候,由于内存有限,大部分时候都不会使用到动态内存管理函数。而且对于内存管理概念比较模糊的情况下,也不建议在C51中使用malloc和free函数。但在需要使用链表的场景中,或者比较复杂的场景中,使用动态内存管理,则可以灵活,同时有效的降低内存使用。
使用51单片机keil自带的内存管理函数需要包含头文件STDLIB.H
Keil自带的内存管理函数包括如下几个函数:
extern void init_mempool (void _MALLOC_MEM_ *p, size_t size);
extern void _MALLOC_MEM_ *malloc (size_t size);
extern void free (void _MALLOC_MEM_ *p);
extern void _MALLOC_MEM_ *realloc (void _MALLOC_MEM_ *p, size_t size);
extern void _MALLOC_MEM_ *calloc (size_t nmemb, size_t size);
这些函数keil提供了源码,位置在:
C:\Keil\C51\LIB,如果是keil5和MDK同时安装的话,则在位置:\Keil_v5\C51\LIB。
一,概述
整体思路为在XDATA区设置一个大的内存池作为heap(堆),使用链表的方式把所有的未分配区域串起来,其中头指针独立于内存池之外,其他链表指针则存在于内存池中。
其中malloc和free是最常用的函数,init_mempool则必须且只能在初始化的时候调用一次,否则整个内存函数会丢失混乱。
链表的类型如下:
struct __mem__
{
struct __mem__ _MALLOC_MEM_ *next; /* single-linked list */
unsigned int len; /* length of following block */
};
内存管理模块首先定义了两个变量作为头指针
__memt__ _MALLOC_MEM_ __mem_avail__ [2] =
{
{ NULL, 0 }, /* 指向可用块的头*/
{ NULL, 0 }, /*不使用,为防止free函数把头指针释放掉*/
};
#define AVAIL (__mem_avail__[0])
AVAIL 指向内存池第一块可用内存块,每次分配内存的时候,都从这个头指针开始查找。对于内存的具体操作, 下面按函数分别分析。
二、具体实现
1,内存池初始化
void init_mempool (void _MALLOC_MEM_ *p, size_t size);
函数入口参数p为内存池的指针,内存池需要在xdata区分配,size为内存池的大小。调用 init_mempool初始化内存池的时候,首先会判断内存首地址是否为0,为0则调过这个字节。因为51单片机的内部RAM和外部RAM是独立编址的,内存池如果从X:0000H地址开始,则会使链表头指针变成0,成为无效指针。然后把头指针指向内存池,并把内存池中的唯一一块内存指针设置为NULL,初始化其大小。
void init_mempool (
void _MALLOC_MEM_ *pool,
unsigned int size)
{
/*-----------------------------------------------
如果内存指向0地址,则指向1并且把内存池大小-1
-----------------------------------------------*/
if (pool == NULL) {
pool = 1;
size--;
}
/*-----------------------------------------------
把AVAIL头指向内存池的开始,并且设置内存大小
-----------------------------------------------*/
AVAIL.next = pool;
AVAIL.len = size;
/*-----------------------------------------------
把内存池中的链接的块设置为NULL(因为这是唯一的块),并初始化 数据区大小
-----------------------------------------------*/
(AVAIL.next)->next = NULL;
(AVAIL.next)->len = size - HLEN;
}
一个空的内存池在初始化之后,会呈现如下的图像:
2,内存申请
调用malloc分配内存的时候,根据头指针,找到第一块空内存,如果空间大小够则分配成功,返回内存指针,并把剩余内存区域重新配置成未使用区域;如果长度不够,则继续寻找下一块内存,直到找到长度够的内存,或者最终找不到足够的内存区域,返回失败。
在分配过程中,分割长度超过申请长度的区域时,分配区会放到后部,把前部留出来,仅设置空闲块的大小;分配区则重新创建一个链表头,记录分配区的大小。
void _MALLOC_MEM_ *malloc (unsigned int size)
{
__memp__ q; /* 指向空闲区的指针 */
__memp__ p; /* q->next */
unsigned int k; /* 分配区的剩余长度 */
/*-----------------------------------------------
初始化:Q为下一个可用块的指针
----------------------------------------------*/
q = &AVAIL;
/*----------------------------------------------
链表结束: P指向下一块内存,如果块无效,则到了最后一链
-----------------------------------------------*/
while (1)
{
if ((p = q->next) == NULL)
{
return (NULL); /* 分配失败*/
}
/*-----------------------------------------------
寻找空闲块:如果内存块足够大,预设它,否则复制P到Q,然后尝试下一个空闲块
-----------------------------------------------*/
if (p->len >= size)
break;
q = p;
}
/*-----------------------------------------------
预设P:至少使用部分P块满足分配要求,这时按以下设置指针:
P指向我们要分配的块,Q->next指向P
-----------------------------------------------*/
k = p->len - size; /* calc. remaining bytes in block */
if (k < MIN_BLOCK) /* rem. bytes too small for new block */
{
q->next = p->next;
return (&p[1]); /* SUCCESS */
}
/*-----------------------------------------------
分割P块:如果比我们需要的大,我就把P分割为2块,剩余空间和分配空间,这意味着,我们需要在分配空间重新创建一个头
-----------------------------------------------*/
k -= HLEN;
p->len = k;
//这里把指针当数组用,相当于指针移动一个内存指针长度,再加上K,则让出了空闲区域
q = (__memp__ ) (((char _MALLOC_MEM_ *) (&p [1])) + k);
q->len = size;
return (&q[1]); /* SUCCESS */
}
在新的内存池分配一次和多次后的内存图像如下:
3,内存释放
调用free函数释放已申请内存时,首先从头查找空闲块,直到查找到我们需要的内存块后面的一块,则退出查找。如果找到后面的空闲块,则检查要释放块的上下边界,如果和前一块空闲区连续,则把要释放块合并进前面的空闲块;如果和后面空闲块连续,则把后面的空闲块合并到前面。
void free (void _MALLOC_MEM_ *memp)
{
/*-----------------------------------------------
FREE 尝试组织 Q、P0 和 P,使 Q < P0 < P。然后,P0 插入到空闲列表,那么整个列表按地址排列
FREE 还尝试合并小块进入大块。 所以,分配所有内存并释放所有内存,我们将得到一个跟内存池一样大的块(也就是初始化时那样)。 合并的开销非常小。
-----------------------------------------------*/
__memp__ q; /* ptr to free block */
__memp__ p; /* q->next */
__memp__ p0; /* block to free */
/*-----------------------------------------------
如果使用者试图释放NULL,直接返回。
否则,获取内存头指针P0,然后尝试定位Q和P,使 Q < P0 < P
-----------------------------------------------*/
if ((memp == NULL) || (AVAIL.len == 0))
return;
p0 = memp;
p0 = &p0 [-1]; /* get address of header */
/*-----------------------------------------------
初始化:Q = 第一个可用块.
-----------------------------------------------*/
q = &AVAIL;
/*-----------------------------------------------
B2. 前进P
遍历列表,直到我们要释放的块后面的一块内存,找到一个离释放块挨着的空闲块
----------------------------------------------*/
while (1)
{
p = q->next;
if ((p == NULL) || (p > memp))
break;
q = p;
}
/*-----------------------------------------------
B3. 检查上边界
如果 P0 和 P 是连续的,则将块P合并到块P0。
-----------------------------------------------*/
if ((p != NULL) && ((((char _MALLOC_MEM_ *)memp) + p0->len) == p))
{
p0->len += p->len + HLEN;
p0->next = p->next;
}
else
{
p0->next = p;
}
/*-----------------------------------------------
B4. 检查下边界
If Q and P0 are contiguous, merge P0 into Q.如果Q和P0是连续的,则合并P0到Q
-----------------------------------------------*/
if ((((char _MALLOC_MEM_ *)q) + q->len + HLEN) == p0)
{
q->len += p0->len + HLEN;
q->next = p0->next;
}
else
{
q->next = p0;
}
}
多次申请内存后,再释放内存,可能的内存图像如下:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示