摘要:靜態記憶體實質上是一個靜態陣列,靜態記憶體池內的塊大小在初始化時設定,初始化後塊大小不可變更。靜態記憶體池由一個控制塊和若干相同大小的記憶體塊構成。控制塊位於記憶體池頭部,用於記憶體塊管理。記憶體塊的申請和釋放以塊大小為粒度。
本文分享自華為雲社群《鴻蒙輕核心M核原始碼分析系列八 靜態記憶體Static Memory》,原文作者:zhushy。
記憶體管理模組管理系統的記憶體資源,它是作業系統的核心模組之一,主要包括記憶體的初始化、分配以及釋放。
在系統執行過程中,記憶體管理模組通過對記憶體的申請/釋放來管理使用者和OS對記憶體的使用,使記憶體的利用率和使用效率達到最優,同時最大限度地解決系統的記憶體碎片問題。
鴻蒙輕核心的記憶體管理分為靜態記憶體管理和動態記憶體管理,提供記憶體初始化、分配、釋放等功能。
- 動態記憶體:在動態記憶體池中分配使用者指定大小的記憶體塊。
- 優點:按需分配。
- 缺點:記憶體池中可能出現碎片。
- 靜態記憶體:在靜態記憶體池中分配使用者初始化時預設(固定)大小的記憶體塊。
- 優點:分配和釋放效率高,靜態記憶體池中無碎片。
- 缺點:只能申請到初始化預設大小的記憶體塊,不能按需申請。
本文主要分析鴻蒙輕核心靜態記憶體(Memory Box),後續系列會繼續分析動態記憶體。靜態記憶體實質上是一個靜態陣列,靜態記憶體池內的塊大小在初始化時設定,初始化後塊大小不可變更。靜態記憶體池由一個控制塊和若干相同大小的記憶體塊構成。控制塊位於記憶體池頭部,用於記憶體塊管理。記憶體塊的申請和釋放以塊大小為粒度。
本文通過分析靜態記憶體模組的原始碼,幫助讀者掌握靜態記憶體的使用。本文中所涉及的原始碼,以OpenHarmony LiteOS-M核心為例,均可以在開源站點https://gitee.com/openharmony/kernel_liteos_m 獲取。
接下來,我們看下靜態記憶體的結構體,靜態記憶體初始化,靜態記憶體常用操作的原始碼。
1、靜態記憶體結構體定義和常用巨集定義
1.1 靜態記憶體結構體定義
靜態記憶體結構體在檔案kernel\include\los_membox.h中定義。原始碼如下,⑴處定義的是靜態記憶體節點LOS_MEMBOX_NODE結構體,⑵處定義的靜態記憶體的結構體池資訊結構體為LOS_MEMBOX_INFO,,結構體成員的解釋見註釋部分。
⑴ typedef struct tagMEMBOX_NODE { struct tagMEMBOX_NODE *pstNext; /**< 靜態記憶體池中空閒節點指標,指向下一個空閒節點 */ } LOS_MEMBOX_NODE; ⑵ typedef struct LOS_MEMBOX_INFO { UINT32 uwBlkSize; /**< 靜態記憶體池中空閒節點指標,指向下一個空閒節點 */ UINT32 uwBlkNum; /**< 靜態記憶體池的記憶體塊總數量 */ UINT32 uwBlkCnt; /**< 靜態記憶體池的已分配的記憶體塊總數量 */ #if (LOSCFG_PLATFORM_EXC == 1) struct LOS_MEMBOX_INFO *nextMemBox; /**< 指向下一個靜態記憶體池 */ #endif LOS_MEMBOX_NODE stFreeList; /**< 靜態記憶體池的空閒記憶體塊單向連結串列 */ } LOS_MEMBOX_INFO;
對靜態記憶體使用如下示意圖進行說明,對一塊靜態記憶體區域,頭部是LOS_MEMBOX_INFO資訊,接著是各個記憶體塊,每塊記憶體塊大小是uwBlkSize,包含記憶體塊節點LOS_MEMBOX_NODE和記憶體塊資料區。空閒記憶體塊節點指向下一塊空閒記憶體塊節點。
1.2 靜態記憶體常用巨集定義
靜態記憶體標頭檔案中還提供了一些重要的巨集定義。⑴處的LOS_MEMBOX_ALIGNED(memAddr)用於對齊記憶體地址,⑵處OS_MEMBOX_NEXT(addr, blkSize)根據當前節點記憶體地址addr和記憶體塊大小blkSize獲取下一個記憶體塊的記憶體地址。⑶處OS_MEMBOX_NODE_HEAD_SIZE表示記憶體塊中節點頭大小,每個記憶體塊包含記憶體節點LOS_MEMBOX_NODE和存放業務的資料區。⑷處表示靜態記憶體的總大小,包含記憶體池資訊結構體佔用的大小,和各個記憶體塊佔用的大小。
⑴ #define LOS_MEMBOX_ALIGNED(memAddr) (((UINTPTR)(memAddr) + sizeof(UINTPTR) - 1) & (~(sizeof(UINTPTR) - 1))) ⑵ #define OS_MEMBOX_NEXT(addr, blkSize) (LOS_MEMBOX_NODE *)(VOID *)((UINT8 *)(addr) + (blkSize)) ⑶ #define OS_MEMBOX_NODE_HEAD_SIZE sizeof(LOS_MEMBOX_NODE) ⑷ #define LOS_MEMBOX_SIZE(blkSize, blkNum) \ (sizeof(LOS_MEMBOX_INFO) + (LOS_MEMBOX_ALIGNED((blkSize) + OS_MEMBOX_NODE_HEAD_SIZE) * (blkNum)))
在檔案kernel\src\mm\los_membox.c中也定義了一些巨集和行內函數。⑴處定義OS_MEMBOX_MAGIC魔術字,這個32位的魔術字的後8位維護任務編號資訊,任務編號位由⑵處的巨集定義。⑶處巨集定義任務編號的最大值,⑷處的巨集從魔術字中提取任務編號資訊。
⑸處行內函數設定魔術字,在記憶體塊節點從靜態記憶體池中分配出來後,節點指標.pstNext不再指向下一個空閒記憶體塊節點,而是設定為魔術字。⑹處的行內函數用於校驗魔術字。⑺處的巨集根據記憶體塊的節點地址獲取記憶體塊的資料區地址,⑻處的巨集根據記憶體塊的資料區地址獲取記憶體塊的節點地址。
⑴ #define OS_MEMBOX_MAGIC 0xa55a5a00 ⑵ #define OS_MEMBOX_TASKID_BITS 8 ⑶ #define OS_MEMBOX_MAX_TASKID ((1 << OS_MEMBOX_TASKID_BITS) - 1) ⑷ #define OS_MEMBOX_TASKID_GET(addr) (((UINTPTR)(addr)) & OS_MEMBOX_MAX_TASKID) ⑸ STATIC INLINE VOID OsMemBoxSetMagic(LOS_MEMBOX_NODE *node) { UINT8 taskID = (UINT8)LOS_CurTaskIDGet(); node->pstNext = (LOS_MEMBOX_NODE *)(OS_MEMBOX_MAGIC | taskID); } ⑹ STATIC INLINE UINT32 OsMemBoxCheckMagic(LOS_MEMBOX_NODE *node) { UINT32 taskID = OS_MEMBOX_TASKID_GET(node->pstNext); if (taskID > (LOSCFG_BASE_CORE_TSK_LIMIT + 1)) { return LOS_NOK; } else { return (node->pstNext == (LOS_MEMBOX_NODE *)(OS_MEMBOX_MAGIC | taskID)) ? LOS_OK : LOS_NOK; } } ⑺ #define OS_MEMBOX_USER_ADDR(addr) \ ((VOID *)((UINT8 *)(addr) + OS_MEMBOX_NODE_HEAD_SIZE)) ⑻ #define OS_MEMBOX_NODE_ADDR(addr) \ ((LOS_MEMBOX_NODE *)(VOID *)((UINT8 *)(addr) - OS_MEMBOX_NODE_HEAD_SIZE))
2、靜態記憶體常用操作
當使用者需要使用固定長度的記憶體時,可以通過靜態記憶體分配的方式獲取記憶體,一旦使用完畢,通過靜態記憶體釋放函式歸還所佔用記憶體,使之可以重複使用。
2.1 初始化靜態記憶體池
我們分析下初始化靜態記憶體池函式UINT32 LOS_MemboxInit(VOID *pool, UINT32 poolSize, UINT32 blkSize)的程式碼。我們先看看函式引數,VOID *pool是靜態記憶體池的起始地址,UINT32 poolSize是初始化的靜態記憶體池的總大小,poolSize需要小於等於*pool開始的記憶體區域的大小,否則會影響後面的記憶體區域。還需要大於靜態記憶體的頭部大小sizeof(LOS_MEMBOX_INFO)。長度UINT32 blkSize是靜態記憶體池中的每個記憶體塊的塊大小。
我們看下程式碼,⑴處對傳入引數進行校驗。⑵處設定靜態記憶體池中每個記憶體塊的實際大小,已記憶體對齊,也算上記憶體塊中節點資訊。⑶處計算記憶體池中記憶體塊的總數量,然後設定已用記憶體塊數量.uwBlkCnt為0。
⑷處如果可用的記憶體塊為0,返回初始化失敗。⑸處獲取記憶體池中的第一個空閒記憶體塊節點。⑹處把空閒記憶體塊掛載在靜態記憶體池資訊結構體空閒記憶體塊連結串列stFreeList.pstNext上,然後執行⑺每個空閒記憶體塊依次指向下一個空閒記憶體塊,連結起來。
UINT32 LOS_MemboxInit(VOID *pool, UINT32 poolSize, UINT32 blkSize) { LOS_MEMBOX_INFO *boxInfo = (LOS_MEMBOX_INFO *)pool; LOS_MEMBOX_NODE *node = NULL; UINT32 index; UINT32 intSave; ⑴ if (pool == NULL) { return LOS_NOK; } if (blkSize == 0) { return LOS_NOK; } if (poolSize < sizeof(LOS_MEMBOX_INFO)) { return LOS_NOK; } MEMBOX_LOCK(intSave); ⑵ boxInfo->uwBlkSize = LOS_MEMBOX_ALIGNED(blkSize + OS_MEMBOX_NODE_HEAD_SIZE); if (boxInfo->uwBlkSize == 0) { MEMBOX_UNLOCK(intSave); return LOS_NOK; } ⑶ boxInfo->uwBlkNum = (poolSize - sizeof(LOS_MEMBOX_INFO)) / boxInfo->uwBlkSize; boxInfo->uwBlkCnt = 0; ⑷ if (boxInfo->uwBlkNum == 0) { MEMBOX_UNLOCK(intSave); return LOS_NOK; } ⑸ node = (LOS_MEMBOX_NODE *)(boxInfo + 1); ⑹ boxInfo->stFreeList.pstNext = node; ⑺ for (index = 0; index < boxInfo->uwBlkNum - 1; ++index) { node->pstNext = OS_MEMBOX_NEXT(node, boxInfo->uwBlkSize); node = node->pstNext; } node->pstNext = NULL; #if (LOSCFG_PLATFORM_EXC == 1) OsMemBoxAdd(pool); #endif MEMBOX_UNLOCK(intSave); return LOS_OK; }
2.2 清除靜態記憶體塊內容
我們可以使用函式VOID LOS_MemboxClr(VOID *pool, VOID *box)來清除靜態記憶體塊中的資料區內容,需要2個引數,VOID *pool是初始化過的靜態記憶體池地址。VOID *box是需要清除內容的靜態記憶體塊的資料區的起始地址,注意這個不是記憶體塊的節點地址,每個記憶體塊的節點區不能清除。下面分析下原始碼。
⑴處對引數進行校驗,⑵處呼叫memset_s()函式把記憶體塊的資料區寫入0。寫入的開始地址是記憶體塊的資料區的起始地址VOID *box,寫入長度是資料區的長度boxInfo->uwBlkSize - OS_MEMBOX_NODE_HEAD_SIZE。
VOID LOS_MemboxClr(VOID *pool, VOID *box) { LOS_MEMBOX_INFO *boxInfo = (LOS_MEMBOX_INFO *)pool; ⑴ if ((pool == NULL) || (box == NULL)) { return; } ⑵ (VOID)memset_s(box, (boxInfo->uwBlkSize - OS_MEMBOX_NODE_HEAD_SIZE), 0, (boxInfo->uwBlkSize - OS_MEMBOX_NODE_HEAD_SIZE)); }
2.3 申請、釋放靜態記憶體
初始化靜態記憶體池後,我們可以使用函式VOID *LOS_MemboxAlloc(VOID *pool)來申請靜態記憶體,下面分析下原始碼。
⑴處獲取靜態記憶體池空閒記憶體塊連結串列頭結點,如果連結串列不為空,執行⑵,把下一個可用節點賦值給nodeTmp。⑶處把連結串列頭結點執行下一個的下一個連結串列節點,然後執行⑷把分配出來的記憶體塊設定魔術字,接著把記憶體池已用記憶體塊數量加1。⑸處返回時呼叫巨集OS_MEMBOX_USER_ADDR()計算出記憶體塊的資料區域地質。
VOID *LOS_MemboxAlloc(VOID *pool) { LOS_MEMBOX_INFO *boxInfo = (LOS_MEMBOX_INFO *)pool; LOS_MEMBOX_NODE *node = NULL; LOS_MEMBOX_NODE *nodeTmp = NULL; UINT32 intSave; if (pool == NULL) { return NULL; } MEMBOX_LOCK(intSave); ⑴ node = &(boxInfo->stFreeList); if (node->pstNext != NULL) { ⑵ nodeTmp = node->pstNext; ⑶ node->pstNext = nodeTmp->pstNext; ⑷ OsMemBoxSetMagic(nodeTmp); boxInfo->uwBlkCnt++; } MEMBOX_UNLOCK(intSave); ⑸ return (nodeTmp == NULL) ? NULL : OS_MEMBOX_USER_ADDR(nodeTmp); }
對申請的記憶體塊使用完畢,我們可以使用函式UINT32 LOS_MemboxFree(VOID *pool, VOID *box)來釋放靜態記憶體,需要2個引數,VOID *pool是初始化過的靜態記憶體池地址。VOID *box是需要釋放的靜態記憶體塊的資料區的起始地址,注意這個不是記憶體塊的節點地址。下面分析下原始碼。
⑴處根據待釋放的記憶體塊的資料區域地址獲取節點地址node,⑵對要釋放的記憶體塊先進行校驗。⑶處把要釋放的記憶體塊掛在記憶體池空閒記憶體塊連結串列上,然後執行⑷把已用數量減1。
LITE_OS_SEC_TEXT UINT32 LOS_MemboxFree(VOID *pool, VOID *box) { LOS_MEMBOX_INFO *boxInfo = (LOS_MEMBOX_INFO *)pool; UINT32 ret = LOS_NOK; UINT32 intSave; if ((pool == NULL) || (box == NULL)) { return LOS_NOK; } MEMBOX_LOCK(intSave); do { ⑴ LOS_MEMBOX_NODE *node = OS_MEMBOX_NODE_ADDR(box); ⑵ if (OsCheckBoxMem(boxInfo, node) != LOS_OK) { break; } ⑶ node->pstNext = boxInfo->stFreeList.pstNext; boxInfo->stFreeList.pstNext = node; ⑷ boxInfo->uwBlkCnt--; ret = LOS_OK; } while (0); MEMBOX_UNLOCK(intSave); return ret; }
接下來,我們再看看校驗函式OsCheckBoxMem()。⑴如果記憶體池的塊大小為0,返回校驗失敗。⑵處計算出要釋放的記憶體快節點相對第一個記憶體塊節點的偏移量offset。⑶如果偏移量除以記憶體塊數量餘數不為0,返回校驗失敗。⑷如果偏移量除以記憶體塊數量的商大於等於記憶體塊的數量,返回校驗失敗。⑸呼叫巨集OsMemBoxCheckMagic校驗魔術字。
STATIC INLINE UINT32 OsCheckBoxMem(const LOS_MEMBOX_INFO *boxInfo, const VOID *node) { UINT32 offset; ⑴ if (boxInfo->uwBlkSize == 0) { return LOS_NOK; } ⑵ offset = (UINT32)((UINTPTR)node - (UINTPTR)(boxInfo + 1)); ⑶ if ((offset % boxInfo->uwBlkSize) != 0) { return LOS_NOK; } ⑷ if ((offset / boxInfo->uwBlkSize) >= boxInfo->uwBlkNum) { return LOS_NOK; } ⑸ return OsMemBoxCheckMagic((LOS_MEMBOX_NODE *)node); }
小結
本文帶領大家一起剖析了鴻蒙輕核心的靜態記憶體模組的原始碼,包含靜態記憶體的結構體、靜態記憶體池初始化、靜態記憶體申請、釋放、清除內容等。後續也會陸續推出更多的分享文章,敬請期待,也歡迎大家分享學習、使用鴻蒙輕核心的心得,有任何問題、建議,都可以留言給我們:https://gitee.com/openharmony/kernel_liteos_m/issues 。為了更容易找到鴻蒙輕核心程式碼倉,建議訪問 https://gitee.com/openharmony/kernel_liteos_m ,關注Watch
、點贊Star
、並Fork
到自己賬戶下,謝謝。