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>閱讀更多 正點原子 的文章