「正點原子NANO STM32F103開發板資料連載」第29章 內存管理實驗

1)實驗平臺:【正點原子】 NANO STM32F103 開發板

2)摘自《正點原子STM32 F1 開發指南(NANO 板-HAL 庫版)》關注官方微信號公眾號,獲取更多資料:正點原子

「正點原子NANO STM32F103開發板資料連載」第29章 內存管理實驗

第二十九章 內存管理實驗

上一章,我們在 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 所示:

「正點原子NANO STM32F103開發板資料連載」第29章 內存管理實驗

圖 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 所示界面:

「正點原子NANO STM32F103開發板資料連載」第29章 內存管理實驗

圖 29.4.1 程序運行效果圖


可以看到,提示我們通過按鍵去操作,此時我們按下 KEY0,就可以看到內部 SRAM 內存

被使用 20%了,同時看到下面提示了指針 p 所指向的地址(其實就是被分配到的內存地址)和

內容。多按幾次 KEY0,可以看到內存使用率持續上升(注意對比 p 的值,可以發現是遞減的,

說明是從頂部開始分配內存!),此時如果按下 KEY2,可以發現內存使用率降低了 20%,但

是再按 KEY2 將不再降低,說明“內存洩露”了。這就是前面提到的對一個指針多次申請內存,

而之前申請的內存又沒釋放,導致的“內存洩露”。

KEY1 鍵用於更新 p 的內容,更新後的內容將重新打印在串口調試助手上面。

本章,我們還可以藉助 USMART,測試內存的分配和釋放,有興趣的朋友可以動手試試。

如圖 29.4.2 所示:

「正點原子NANO STM32F103開發板資料連載」第29章 內存管理實驗

圖 29.4.2 USMART 測試內存管理函數

圖中,我們先申請了 4660 字節的內存,然後得到申請到的內存首地址:0X200017EC,說

明我們申請內存成功(如果不成功,則會收到 0),然後釋放內存的時候,參數是指針的地址,

即執行:myfree(0X200017EC),就可以釋放我們申請到的內存。其他情況,大家可以自行測試

並分析。

"/<nmemb>

/<memsize>

/<nmemb>

/<memtblsize>


分享到:


相關文章: