目錄
一、內存管理的基本概念
二、內存管理的運作機制
三、內存管理的應用場景
四、內存管理函數接口講解
1、內存池創建函數?OSMemCreate()
2、內存申請函數 OSMemGet()
3、內存釋放函數?OSMemPut()
五、實現
一、內存管理的基本概念
????????在計算系統中,變量、中間數據一般存放在系統存儲空間中,只有在實際使用時才將它們從存儲空間調入到中央處理器內部進行運算。通常存儲空間可以分為兩種:內部存儲空間和外部存儲空間。內部存儲空間訪問速度比較快,能夠按照變量地址隨機地訪問,也就是我們通常所說的 RAM(隨機存儲器),或電腦的內存;而外部存儲空間內所保存的內容相對來說比較固定,即使掉電后數據也不會丟失,可以把它理解為電腦的硬盤。在這一章中我們主要討論內部存儲空間(RAM)的管理——內存管理。
????????在嵌入式系統設計中,內存分配應該是根據所設計系統的特點來決定選擇使用動態內存分配還是靜態內存分配算法,一些可靠性要求非常高的系統應選擇使用靜態的,而普通的業務系統可以使用動態來提高內存使用效率。靜態可以保證設備的可靠性但是需要考慮內存上限,內存使用效率低,而動態則是相反。
????????uCOS 的內存管理是采用內存池的方式進行管理,也就是創建一個內存池,靜態劃分一大塊連續空間作為內存管理的空間,里面劃分為很多個內存塊,我們在使用的時候就從這個內存池中獲取一個內存塊,使用完畢的時候用戶可以將其放回內存池中,這樣子就不會導致內存碎片的產生。
????????uCOS 內存管理模塊管理用于系統中內存資源,它是操作系統的核心模塊之一, 主要包括內存池的創建、分配以及釋放。
????????很多人會有疑問,什么不直接使用 C 標準庫中的內存管理函數呢?在電腦中我們可以用 malloc()和 free()這兩個函數動態的分配內存和釋放內存。但是,在嵌入式實時操作系統中,調用 malloc()和 free()卻是危險的,原因有以下幾點:
- 這些函數在小型嵌入式系統中并不總是可用的,小型嵌入式設備中的 RAM 不足。
- 它們的實現可能非常的大,占據了相當大的一塊代碼空間。
- 他們幾乎都不是安全的。
- 它們并不是確定的,每次調用這些函數執行的時間可能都不一樣。
- 它們有可能產生碎片。
- 這兩個函數會使得鏈接器配置得復雜。
- 如果允許堆空間的生長方向覆蓋其他變量占據的內存,它們會成為 debug 的災難。
????????在一般的實時嵌入式系統中,由于實時性的要求,很少使用虛擬內存機制。所有的內存都需要用戶參與分配,直接操作物理內存,所分配的內存不能超過系統的物理內存,所有的系統堆棧的管理,都由用戶自己管理。
????????同時,在嵌入式實時操作系統中,對內存的分配時間要求更為苛刻,分配內存的時間必須是確定的。一般內存管理算法是根據需要存儲的數據的長度在內存中去尋找一個與這段數據相適應的空閑內存塊,然后將數據存儲在里面, 而尋找這樣一個空閑內存塊所耗費的時間是不確定的,因此對于實時系統來說,這就是不可接受的,實時系統必須要保證內存塊的分配過程在可預測的確定時間內完成,否則實時任務對外部事件的響應也將變得不可確定。
????????在嵌入式系統中,內存是十分有限而且是十分珍貴的,用一塊內存就少了一塊內存,而在分配中隨著內存不斷被分配和釋放,整個系統內存區域會產生越來越多的碎片,因為在使用過程中,申請了一些內存,其中一些釋放了,導致內存空間中存在一些小的內存塊,它們地址不連續,不能夠作為一整塊的大內存分配出去,所以一定會在某個時間,系統已經無法分配到合適的內存了,導致系統癱瘓。其實系統中實際是還有內存的,但是因為小塊的內存的地址不連續,導致無法分配成功,所以我們需要一個優良的內存分配算法來避免這種情況的出現。
????????所以 uCOS 提供的內存分配算法是只允許用戶分配固定大小的內存塊,當使用完成就將其放回內存池中,這樣子分配效率極高,時間復雜度是 O(1),也就是一個固定的時間常數,并不會因為系統內存的多少而增加遍歷內存塊列表的時間, 并且還不會導致內存碎片的出現,但是這樣的內存分配機制會導致內存利用率的下降以及申請內存大小的限制
二、內存管理的運作機制
????????內存池(Memory Pool)是一種用于分配大量大小相同的內存對象的技術,它可以極大加快內存分配/釋放的速度。
????????在系統編譯的時候,編譯器就靜態劃分了一個大數組作為系統的內存池,然后在初始化的時候將其分成大小相等的多個內存塊,內存塊直接通過鏈表連接起來(此鏈表也稱為空閑內存塊列表)。每次分配的時候,從空閑內存塊列表中取出表頭上第一個內存塊,提供給申請者。物理內存中允許存在多個大小不同的內存池,每一個內存池又由多個大小相同的空閑內存塊組成。
???????? 我們必須先創建內存池才能去使用內存池里面的內存塊,在創建的時候,我們必須定義一個內存池控制塊,然后進行相關初始化,內存控制塊的參數包括內存池名稱,內存池起始地址,內存塊大小, 內存塊數量等信息, 在以后需要從內存池取出內存塊或者釋放內存塊的時候,我們只需根據內存控制塊的信息就能很輕易做到。
三、內存管理的應用場景
????????首先,在使用內存分配前,必須明白自己在做什么,這樣做與其他的方法有什么不同,特別是會產生哪些負面影響,在自己的產品面前,應當選擇哪種分配策略。
????????內存管理的主要工作是動態劃分并管理用戶分配好的內存區間,主要是在用戶需要使用大小不等的內存塊的場景中使用, 當用戶需要分配內存時,可以通過操作系統的內存申請函數索取指定大小內存塊,一旦使用完畢,通過動態內存釋放函數歸還所占用內存,使之可以重復使用(heap_1.c 的內存管理除外)。
例如我們需要定義一個 float 型數組: floatArr[];
????????但是,在使用數組的時候,總有一個問題困擾著我們:數組應該有多大?在很多的情況下,你并不能確定要使用多大的數組,可能為了避免發生錯誤你就需要把數組定義得足夠大。即使你知道想利用的空間大小,但是如果因為某種特殊原因空間利用的大小有增加或者減少,你又必須重新去修改程序,擴大數組的存儲范圍。這種分配固定大小的內存分配方法稱之為靜態內存分配。這種內存分配的方法存在比較嚴重的缺陷,在大多數情況下會浪費大量的內存空間,在少數情況下,當你定義的數組不夠大時,可能引起下標越界錯誤,甚至導致嚴重后果。
????????uCOS 將系統靜態分配的大數組作為內存池,然后進行內存池的初始化,然后分配固定大小的內存塊。
????????注意: uCOS 也不能很好解決這種問題,因為內存塊的大小是固定的,無法解決這種彈性很大的內存需求,只能按照最大的內存塊進行分配。但是 uCOS 的內存分配能解決內存利用率的問題,在不需要使用內存的時候,將內存釋放到內存池中,讓其他任務能正常使用該內存塊。
四、內存管理函數接口講解
1、內存池創建函數?OSMemCreate()
????????在使用內存池的時候首先要創建一個內存池,需要用戶靜態分配一個數組空間作為系統的內存池,且用戶還需定義一個內存控制塊。創建內存池后,任務才可以通過系統的內存 申 請 、 釋 放函數從內存池中申請或釋放內存,uCOS提供內存池創建函數OSMemCreate()。
2、內存申請函數 OSMemGet()
????????這個函數用于申請固定大小的內存塊, 從指定的內存池中分配一個內存塊給用戶使用,該內存塊的大小在內存池初始化的時候就已經決定的。如果內存池中有可用的內存塊,則從內存池的空閑內存塊列表上取下一個內存塊并且返回對應的內存地址;如果內存池中已經沒有可用內存塊,則返回 0 與對應的錯誤代碼 OS_ERR_MEM_NO_FREE_BLKS。
3、內存釋放函數?OSMemPut()
????????嵌入式系統的內存對我們來說是十分珍貴的, 任何內存塊使用完后都必須被釋放,否則會造成內存泄露,導致系統發生致命錯誤。 uCOS 提供了 OSMemPut()函數進行內存的釋放管理,使用該函數接口時,根據指定的內存控制塊對象,將內存塊插入內存池的空閑內存塊列表中,然后增加該內存池的可用內存塊數目。
五、實現
#include <includes.h>
#include <string.h>OS_MEM mem; //聲明內存管理對象
uint8_t ucArray [ 3 ] [ 20 ]; //聲明內存分區大小static OS_TCB AppTaskStartTCB; //任務控制塊static OS_TCB AppTaskPostTCB;
static OS_TCB AppTaskPendTCB;static CPU_STK AppTaskStartStk[APP_TASK_START_STK_SIZE]; //任務堆棧static CPU_STK AppTaskPostStk [ APP_TASK_POST_STK_SIZE ];
static CPU_STK AppTaskPendStk [ APP_TASK_PEND_STK_SIZE ];static void AppTaskStart (void *p_arg); //任務函數聲明static void AppTaskPost ( void * p_arg );
static void AppTaskPend ( void * p_arg );int main (void)
{OS_ERR err;OSInit(&err); //初始化 uC/OS-III/* 創建起始任務 */OSTaskCreate((OS_TCB *)&AppTaskStartTCB, //任務控制塊地址(CPU_CHAR *)"App Task Start", //任務名稱(OS_TASK_PTR ) AppTaskStart, //任務函數(void *) 0, //傳遞給任務函數(形參p_arg)的實參(OS_PRIO ) APP_TASK_START_PRIO, //任務的優先級(CPU_STK *)&AppTaskStartStk[0], //任務堆棧的基地址(CPU_STK_SIZE) APP_TASK_START_STK_SIZE / 10, //任務堆棧空間剩下1/10時限制其增長(CPU_STK_SIZE) APP_TASK_START_STK_SIZE, //任務堆棧空間(單位:sizeof(CPU_STK))(OS_MSG_QTY ) 5u, //任務可接收的最大消息數(OS_TICK ) 0u, //任務的時間片節拍數(0表默認值OSCfg_TickRate_Hz/10)(void *) 0, //任務擴展(0表不擴展)(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), //任務選項(OS_ERR *)&err); //返回錯誤類型OSStart(&err); //啟動多任務管理(交由uC/OS-III控制)}static void AppTaskStart (void *p_arg)
{CPU_INT32U cpu_clk_freq;CPU_INT32U cnts;OS_ERR err;(void)p_arg;BSP_Init(); //板級初始化CPU_Init(); //初始化 CPU 組件(時間戳、關中斷時間測量和主機名)cpu_clk_freq = BSP_CPU_ClkFreq(); //獲取 CPU 內核時鐘頻率(SysTick 工作時鐘)cnts = cpu_clk_freq / (CPU_INT32U)OSCfg_TickRate_Hz; //根據用戶設定的時鐘節拍頻率計算 SysTick 定時器的計數值OS_CPU_SysTickInit(cnts); //調用 SysTick 初始化函數,設置定時器計數值和啟動定時器Mem_Init(); //初始化內存管理組件(堆內存池和內存池表)#if OS_CFG_STAT_TASK_EN > 0u //如果使能(默認使能)了統計任務OSStatTaskCPUUsageInit(&err); //計算沒有應用任務(只有空閑任務)運行時 CPU 的(最大)
#endif //容量(決定 OS_Stat_IdleCtrMax 的值,為后面計算 CPU //使用率使用)。CPU_IntDisMeasMaxCurReset(); //復位(清零)當前最大關中斷時間/* 創建內存管理對象 mem */OSMemCreate ((OS_MEM *)&mem, //指向內存管理對象(CPU_CHAR *)"Mem For Test", //命名內存管理對象(void *)ucArray, //內存分區的首地址(OS_MEM_QTY )3, //內存分區中內存塊數目(OS_MEM_SIZE )20, //內存塊的字節數目(OS_ERR *)&err); //返回錯誤類型/* 創建 AppTaskPost 任務 */OSTaskCreate((OS_TCB *)&AppTaskPostTCB, //任務控制塊地址(CPU_CHAR *)"App Task Post", //任務名稱(OS_TASK_PTR ) AppTaskPost, //任務函數(void *) 0, //傳遞給任務函數(形參p_arg)的實參(OS_PRIO ) APP_TASK_POST_PRIO, //任務的優先級(CPU_STK *)&AppTaskPostStk[0], //任務堆棧的基地址(CPU_STK_SIZE) APP_TASK_POST_STK_SIZE / 10, //任務堆棧空間剩下1/10時限制其增長(CPU_STK_SIZE) APP_TASK_POST_STK_SIZE, //任務堆棧空間(單位:sizeof(CPU_STK))(OS_MSG_QTY ) 5u, //任務可接收的最大消息數(OS_TICK ) 0u, //任務的時間片節拍數(0表默認值OSCfg_TickRate_Hz/10)(void *) 0, //任務擴展(0表不擴展)(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), //任務選項(OS_ERR *)&err); //返回錯誤類型/* 創建 AppTaskPend 任務 */OSTaskCreate((OS_TCB *)&AppTaskPendTCB, //任務控制塊地址(CPU_CHAR *)"App Task Pend", //任務名稱(OS_TASK_PTR ) AppTaskPend, //任務函數(void *) 0, //傳遞給任務函數(形參p_arg)的實參(OS_PRIO ) APP_TASK_PEND_PRIO, //任務的優先級(CPU_STK *)&AppTaskPendStk[0], //任務堆棧的基地址(CPU_STK_SIZE) APP_TASK_PEND_STK_SIZE / 10, //任務堆棧空間剩下1/10時限制其增長(CPU_STK_SIZE) APP_TASK_PEND_STK_SIZE, //任務堆棧空間(單位:sizeof(CPU_STK))(OS_MSG_QTY ) 50u, //任務可接收的最大消息數(OS_TICK ) 0u, //任務的時間片節拍數(0表默認值OSCfg_TickRate_Hz/10)(void *) 0, //任務擴展(0表不擴展)(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), //任務選項(OS_ERR *)&err); //返回錯誤類型OSTaskDel ( & AppTaskStartTCB, & err ); //刪除起始任務本身,該任務不再運行}static void AppTaskPost ( void * p_arg )
{OS_ERR err;char * p_mem_blk;uint32_t ulCount = 0;(void)p_arg;while (DEF_TRUE) { //任務體/* 向 mem 獲取內存塊 */p_mem_blk = OSMemGet ((OS_MEM *)&mem, //指向內存管理對象(OS_ERR *)&err); //返回錯誤類型sprintf ( p_mem_blk, "%d", ulCount ++ ); //向內存塊存取計數值/* 發布任務消息到任務 AppTaskPend */OSTaskQPost ((OS_TCB *)&AppTaskPendTCB, //目標任務的控制塊(void *)p_mem_blk, //消息內容的首地址(OS_MSG_SIZE )strlen ( p_mem_blk ), //消息長度(OS_OPT )OS_OPT_POST_FIFO, //發布到任務消息隊列的入口端(OS_ERR *)&err); //返回錯誤類型OSTimeDlyHMSM ( 0, 0, 1, 0, OS_OPT_TIME_DLY, & err ); //每20ms發送一次}}static void AppTaskPend ( void * p_arg )
{OS_ERR err;OS_MSG_SIZE msg_size;CPU_TS ts;CPU_INT32U cpu_clk_freq;CPU_SR_ALLOC();char * pMsg;(void)p_arg;cpu_clk_freq = BSP_CPU_ClkFreq(); //獲取CPU時鐘,時間戳是以該時鐘計數while (DEF_TRUE) { //任務體/* 阻塞任務,等待任務消息 */pMsg = OSTaskQPend ((OS_TICK )0, //無期限等待(OS_OPT )OS_OPT_PEND_BLOCKING, //沒有消息就阻塞任務(OS_MSG_SIZE *)&msg_size, //返回消息長度(CPU_TS *)&ts, //返回消息被發布的時間戳(OS_ERR *)&err); //返回錯誤類型ts = OS_TS_GET() - ts; //計算消息從發布到被接收的時間差LED1_TOGGLE; //切換LED1的亮滅狀態OS_CRITICAL_ENTER(); //進入臨界段,避免串口打印被打斷printf ( "\r\n接收到的消息的內容為:%s,長度是:%d字節。",pMsg, msg_size ); printf ( "\r\n任務消息從被發布到被接收的時間差是%dus\r\n",ts / ( cpu_clk_freq / 1000000 ) ); OS_CRITICAL_EXIT(); //退出臨界段/* 退還內存塊 */OSMemPut ((OS_MEM *)&mem, //指向內存管理對象(void *)pMsg, //內存塊的首地址(OS_ERR *)&err); //返回錯誤類型}}