在使用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;
}
}
多次申請內存后,再釋放內存,可能的內存圖像如下: