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); /* 分配失败*/

    }

/*-----------------------------------------------

寻找空闲块:如果内存块足够大,预设它,否则复制PQ,然后尝试下一个空闲块

-----------------------------------------------*/

  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,然后尝试定位QP,使 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.如果QP0是连续的,则合并P0Q

-----------------------------------------------*/

if ((((char _MALLOC_MEM_ *)q) + q->len + HLEN) == p0)

  {

  q->len += p0->len + HLEN;

  q->next = p0->next;

  }

else

  {

  q->next = p0;

  }

}

    多次申请内存后,再释放内存,可能的内存图像如下:

 

 

 

posted @ 2022-03-02 11:36  勇敢蘑菇  阅读(2244)  评论(0编辑  收藏  举报