摘要:本文以代碼+文字的形式,介紹虛擬內存管理的結構體、相關宏定義,分析內核虛擬地址空間和用戶進程虛擬地址空間如何初始化等內容。
本文分享自華為雲社區《鴻蒙輕內核A核源碼分析系列四(2) 虛擬內存》,作者: zhushy 。
本文中所涉及的源碼,以OpenHarmony LiteOS-A
內核為例,均可以在開源站點https://gitee.com/openharmony/kernel_liteos_a 獲取。如果涉及開發板,則默認以hispark_taurus
為例。
我們首先了解了虛擬內存管理的結構體、相關宏定義,接着會分析內核虛擬地址空間和用戶進程虛擬地址空間如何初始化,然后分析虛擬內存區間常用操作包含查找、申請和釋放等,最后分析動態內存堆的申請、釋放接口的源代碼,並簡單介紹下內存區間預留接口源代碼。
1、 虛擬內存管理相關的結構體
在文件kernel/base/include/los_vm_map.h中定義了進程地址空間結構體LosVmSpace,進程地址區間結構體LosVmMapRegion和進程地址區間范圍結構體LosVmMapRange。每個用戶態進程會創建自己的進程空間,內核態會創建2個進程空間,分別g_kVmSpace和g_vMallocSpace。從進程空間申請的虛擬內存塊使用進程區間LosVmMapRegion來表示。每個進程空間維護一個紅黑樹來鏈接各個進程區間。
1.1 虛擬內存地址空間結構體LosVmSpace
typedef struct VmSpace { LOS_DL_LIST node; /**< 地址空間雙向鏈表 */ LosRbTree regionRbTree; /**< 地址區間的紅黑樹根節點 */ LosMux regionMux; /**< 地址區間的紅黑樹的互斥鎖 */ VADDR_T base; /**< 地址空間開始地址 */ UINT32 size; /**< 地址空間大小 */ VADDR_T heapBase; /**< 地址空間的堆開始地址heapBase */ VADDR_T heapNow; /**< 地址空間的堆開始地址heapNow */ LosVmMapRegion *heap; /**< 地址空間的地址區間 */ VADDR_T mapBase; /**< 地址空間的映射區開始地址 */ UINT32 mapSize; /**< 地址空間的映射區大小 */ LosArchMmu archMmu; /**< 地址空間的MMU結構體 */ #ifdef LOSCFG_DRIVERS_TZDRIVER VADDR_T codeStart; /**< 用戶進程代碼區開始地址 */ VADDR_T codeEnd; /**< 用戶進程代碼區結束地址 */ #endif } LosVmSpace;
1.2 虛擬內存地址區間LosVmMapRegion
typedef struct VmMapRange { VADDR_T base; /**< 虛擬內存地址區間開始地址 */ UINT32 size; /**< 虛擬內存地址區間大小 */ } LosVmMapRange; ...... struct VmMapRegion; typedef struct VmMapRegion LosVmMapRegion; ...... struct VmMapRegion { LosRbNode rbNode; /**< 地址區間紅黑樹節點 */ LosVmSpace *space; /**< 地址區間所在的地址空間 */ LOS_DL_LIST node; /**< 地址區間雙向鏈表 */ LosVmMapRange range; /**< 地址區間地址范圍 */ VM_OFFSET_T pgOff; /**< 地址區間頁偏移 */ UINT32 regionFlags; /**< 地址區間標記: cow, user_wired */ UINT32 shmid; /**< 共享地址區間編號 */ UINT8 forkFlags; /**< 地址區間fork標記: COPY, ZERO, */ UINT8 regionType; /**< 地址區間類型: ANON, FILE, DEV */ union { struct VmRegionFile { unsigned int fileMagic; struct file *file; const LosVmFileOps *vmFOps; } rf; struct VmRegionAnon { LOS_DL_LIST node; /**< 地址區間類型的雙向鏈表 */ } ra; struct VmRegionDev { LOS_DL_LIST node; /**< 地址區間類型的雙向鏈表 */ const LosVmFileOps *vmFOps; } rd; } unTypeData; };
2、 虛擬內存相關的宏定義
文件kernel/base/include/los_vm_common.h和kernel/base/include/los_vm_zone.h定義了虛擬內存相關的宏。對於32位系統,虛擬進程空間大小為4GiB,OpenHarmony鴻蒙輕內核當前支持32位系統。⑴和⑵定義了用戶進程虛擬地址空間的開始地址和大小,⑶是用戶虛擬進程空間的結束地址,接着定義的是用戶虛擬進程空間的堆區、映射區的開始地址和大小。
/* user address space, defaults to below kernel space with a 16MB guard gap on either side */ #ifndef USER_ASPACE_BASE ⑴ #define USER_ASPACE_BASE ((vaddr_t)0x01000000UL) #endif #ifndef USER_ASPACE_SIZE ⑵ #define USER_ASPACE_SIZE ((vaddr_t)KERNEL_ASPACE_BASE - USER_ASPACE_BASE - 0x01000000UL) #endif ⑶ #define USER_ASPACE_TOP_MAX ((vaddr_t)(USER_ASPACE_BASE + USER_ASPACE_SIZE)) #define USER_HEAP_BASE ((vaddr_t)(USER_ASPACE_TOP_MAX >> 2)) #define USER_MAP_BASE ((vaddr_t)(USER_ASPACE_TOP_MAX >> 1)) #define USER_MAP_SIZE ((vaddr_t)(USER_ASPACE_SIZE >> 3))
內核虛擬進程空間的宏定義如下,⑴處定義內核進程地址空間開始地址和大小,⑵處定義內核非緩存虛擬地址空間開始地址和大小,⑶處定義虛擬動態分配地址空間開始地址和大小,⑷處定義外設開始地址和大小,⑸處定義外設緩存區開始地址和大小,⑹處定義外設非緩存區開始地址和大小。
#ifdef LOSCFG_KERNEL_MMU #ifdef LOSCFG_TEE_ENABLE #define KERNEL_VADDR_BASE 0x41000000 #else #define KERNEL_VADDR_BASE 0x40000000 #endif #else #define KERNEL_VADDR_BASE DDR_MEM_ADDR #endif #define KERNEL_VADDR_SIZE DDR_MEM_SIZE #define SYS_MEM_BASE DDR_MEM_ADDR #define SYS_MEM_END (SYS_MEM_BASE + SYS_MEM_SIZE_DEFAULT) #define _U32_C(X) X##U #define U32_C(X) _U32_C(X) #define KERNEL_VMM_BASE U32_C(KERNEL_VADDR_BASE) #define KERNEL_VMM_SIZE U32_C(KERNEL_VADDR_SIZE) ⑴ #define KERNEL_ASPACE_BASE KERNEL_VMM_BASE #define KERNEL_ASPACE_SIZE KERNEL_VMM_SIZE /* Uncached vmm aspace */ ⑵ #define UNCACHED_VMM_BASE (KERNEL_VMM_BASE + KERNEL_VMM_SIZE) #define UNCACHED_VMM_SIZE DDR_MEM_SIZE ⑶ #define VMALLOC_START (UNCACHED_VMM_BASE + UNCACHED_VMM_SIZE) #define VMALLOC_SIZE 0x08000000 #ifdef LOSCFG_KERNEL_MMU ⑷ #define PERIPH_DEVICE_BASE (VMALLOC_START + VMALLOC_SIZE) #define PERIPH_DEVICE_SIZE U32_C(PERIPH_PMM_SIZE) ⑸ #define PERIPH_CACHED_BASE (PERIPH_DEVICE_BASE + PERIPH_DEVICE_SIZE) #define PERIPH_CACHED_SIZE U32_C(PERIPH_PMM_SIZE) ⑹ #define PERIPH_UNCACHED_BASE (PERIPH_CACHED_BASE + PERIPH_CACHED_SIZE) #define PERIPH_UNCACHED_SIZE U32_C(PERIPH_PMM_SIZE) #else #define PERIPH_DEVICE_BASE PERIPH_PMM_BASE #define PERIPH_DEVICE_SIZE U32_C(PERIPH_PMM_SIZE) #define PERIPH_CACHED_BASE PERIPH_PMM_BASE #define PERIPH_CACHED_SIZE U32_C(PERIPH_PMM_SIZE) #define PERIPH_UNCACHED_BASE PERIPH_PMM_BASE #define PERIPH_UNCACHED_SIZE U32_C(PERIPH_PMM_SIZE) #endif
虛擬地址空間分布示意圖如下:
3、進程地址空間初始化
虛擬進程空間分用戶虛擬進程空間和內核虛擬進程空間,每個用戶進程都會創建屬於自己的進程空間。內核會初始化2個進程空間。下文詳細介紹。
3.1 內核虛擬地址空間初始化
3.1.1 函數OsKSpaceInit
函數OsKSpaceInit()初始化內核進程虛擬地址空間,⑴處的函數初始化虛擬空間鏈表互斥鎖g_vmSpaceListMux,在操作內核進程空間時需要持有該互斥鎖。⑵處開始的函數2個函數OsKernVmSpaceInit和OsVMallocSpaceInit分別初始化內核進程虛擬空間g_kVmSpace和內核動態分配進程空間g_vMallocSpace。傳入的第2個參數由函數OsGFirstTableGet()獲取,即g_firstPageTable,這是內核的2個進程空間使用的一級頁表基地址,大小為0x4000字節,后文在設置轉化表基地址MMU virtTtb時會使用。下文會詳細分析這2個函數。
VOID OsKSpaceInit(VOID) { ⑴ OsVmMapInit(); ⑵ OsKernVmSpaceInit(&g_kVmSpace, OsGFirstTableGet()); OsVMallocSpaceInit(&g_vMallocSpace, OsGFirstTableGet()); }
3.1.2 函數OsKernVmSpaceInit
函數OsKernVmSpaceInit()初始化內核進程虛擬地址空間,⑴處設置地址空間的開始地址和大小,⑵處設置地址空間映射區的開始地址和大小,對於內核虛擬地址空間g_kVmSpace,這2個開始地址和大小是一樣的。⑶處調用通用的地址空間初始化函數,后文分析此函數。
BOOL OsKernVmSpaceInit(LosVmSpace *vmSpace, VADDR_T *virtTtb) { ⑴ vmSpace->base = KERNEL_ASPACE_BASE; vmSpace->size = KERNEL_ASPACE_SIZE; ⑵ vmSpace->mapBase = KERNEL_VMM_BASE; vmSpace->mapSize = KERNEL_VMM_SIZE; #ifdef LOSCFG_DRIVERS_TZDRIVER vmSpace->codeStart = 0; vmSpace->codeEnd = 0; #endif ⑶ return OsVmSpaceInitCommon(vmSpace, virtTtb); }
3.1.3 函數OsVMallocSpaceInit
函數OsVMallocSpaceInit()初始化內核堆虛擬空間,設置的虛擬地址空間和映射區地址空間的開始地址和大小也是一樣的,代碼和函數OsKernVmSpaceInit()類似,不再贅述。
BOOL OsVMallocSpaceInit(LosVmSpace *vmSpace, VADDR_T *virtTtb) { vmSpace->base = VMALLOC_START; vmSpace->size = VMALLOC_SIZE; vmSpace->mapBase = VMALLOC_START; vmSpace->mapSize = VMALLOC_SIZE; #ifdef LOSCFG_DRIVERS_TZDRIVER vmSpace->codeStart = 0; vmSpace->codeEnd = 0; #endif return OsVmSpaceInitCommon(vmSpace, virtTtb); }
3.2 用戶進程虛擬地址空間初始化
3.2.1 函數OsCreateUserVmSpace
在創建進程時,會調用函數OsCreateUserVmSpace()創建用戶進程的虛擬地址空間。⑴為虛擬地址空間結構體申請內存。⑵申請一個內存頁,並調用memset_s()初始化為0,這個內存頁虛擬地址會作為頁表轉換基地址TTB(translation table base,ttb),虛實映射的頁表會保存在這個內存區域。在虛實映射章節,會講述為什么申請4KiB大小內存。⑶處調用函數OsUserVmSpaceInit初始化用戶進程虛擬地址空間。⑷處獲取虛擬地址對應的物理頁結構體地址。如果初始化失敗,則釋放申請的內存。⑸處把物理頁加入虛擬空間中的MMU的頁表鏈表中,這個鏈表維護該進程空間映射的內存頁。
LosVmSpace *OsCreateUserVmSpace(VOID) { BOOL retVal = FALSE; ⑴ LosVmSpace *space = LOS_MemAlloc(m_aucSysMem0, sizeof(LosVmSpace)); if (space == NULL) { return NULL; } ⑵ VADDR_T *ttb = LOS_PhysPagesAllocContiguous(1); if (ttb == NULL) { (VOID)LOS_MemFree(m_aucSysMem0, space); return NULL; } (VOID)memset_s(ttb, PAGE_SIZE, 0, PAGE_SIZE); ⑶ retVal = OsUserVmSpaceInit(space, ttb); ⑷ LosVmPage *vmPage = OsVmVaddrToPage(ttb); if ((retVal == FALSE) || (vmPage == NULL)) { (VOID)LOS_MemFree(m_aucSysMem0, space); LOS_PhysPagesFreeContiguous(ttb, 1); return NULL; } ⑸ LOS_ListAdd(&space->archMmu.ptList, &(vmPage->node)); return space; }
3.2.2 函數OsUserVmSpaceInit
函數OsUserVmSpaceInit初始化用戶進程虛擬地址空間,⑴處設置虛擬地址空間的開始地址和大小。⑵處設置虛擬空間的映射區的開始地址和大小,開始地址在虛擬空間開始地址的1/2處,大小為用戶虛擬空間大小的1/8。⑶處設置虛擬空間的堆區,開始地址為虛擬空間開始地址的1/4處。
BOOL OsUserVmSpaceInit(LosVmSpace *vmSpace, VADDR_T *virtTtb) { ⑴ vmSpace->base = USER_ASPACE_BASE; vmSpace->size = USER_ASPACE_SIZE; ⑵ vmSpace->mapBase = USER_MAP_BASE; vmSpace->mapSize = USER_MAP_SIZE; ⑶ vmSpace->heapBase = USER_HEAP_BASE; vmSpace->heapNow = USER_HEAP_BASE; vmSpace->heap = NULL; #ifdef LOSCFG_DRIVERS_TZDRIVER vmSpace->codeStart = 0; vmSpace->codeEnd = 0; #endif return OsVmSpaceInitCommon(vmSpace, virtTtb); }
3.3 虛擬地址空間初始化的通用函數
3.3.1 函數OsVmSpaceInitCommon
函數OsVmSpaceInitCommon用於進程虛擬地址空間的通用部分的初始化,⑴處初始化地址空間的紅黑樹根節點。⑵處初始化地址空間的地址區間操作互斥鎖。⑶處把新創建的地址空間掛在虛擬地址空間雙向鏈表g_vmSpaceList上。⑷處繼續調用函數OsArchMmuInit()完成地址空間MMU部分的初始化。
STATIC BOOL OsVmSpaceInitCommon(LosVmSpace *vmSpace, VADDR_T *virtTtb) { ⑴ LOS_RbInitTree(&vmSpace->regionRbTree, OsRegionRbCmpKeyFn, OsRegionRbFreeFn, OsRegionRbGetKeyFn); ⑵ status_t retval = LOS_MuxInit(&vmSpace->regionMux, NULL); if (retval != LOS_OK) { VM_ERR("Create mutex for vm space failed, status: %d", retval); return FALSE; } (VOID)LOS_MuxAcquire(&g_vmSpaceListMux); ⑶ LOS_ListAdd(&g_vmSpaceList, &vmSpace->node); (VOID)LOS_MuxRelease(&g_vmSpaceListMux); ⑷ return OsArchMmuInit(&vmSpace->archMmu, virtTtb); }
3.3.2 函數OsArchMmuInit
函數OsArchMmuInit()用於初始化虛擬地址空間的MMU,MMU在后續系列會詳細分析,此處快速了解一下即可。⑴處獲取地址空間編號,如果獲取失敗則返回FALSE。⑵初始化MMU互斥鎖,如果初始化失敗則返回FALSE。⑶處初始化內存頁雙向鏈表。⑷處設置MMU的TTB虛擬地址。⑸處設置MMU的TTB物理地址,TTB虛擬地址基於內核虛擬地址空間開始地址的偏移(UINTPTR)virtTtb - KERNEL_ASPACE_BASE加上物理地址就等於TTB物理地址。
BOOL OsArchMmuInit(LosArchMmu *archMmu, VADDR_T *virtTtb) { #ifdef LOSCFG_KERNEL_VM ⑴ if (OsAllocAsid(&archMmu->asid) != LOS_OK) { VM_ERR("alloc arch mmu asid failed"); return FALSE; } #endif ⑵ status_t retval = LOS_MuxInit(&archMmu->mtx, NULL); if (retval != LOS_OK) { VM_ERR("Create mutex for arch mmu failed, status: %d", retval); return FALSE; } ⑶ LOS_ListInit(&archMmu->ptList); ⑷ archMmu->virtTtb = virtTtb; ⑸ archMmu->physTtb = (VADDR_T)(UINTPTR)virtTtb - KERNEL_ASPACE_BASE + SYS_MEM_BASE; return TRUE; }
4、虛擬地址區間常用操作
虛擬地址區間操作分為查找、申請、釋放等操作。
4.1 函數LOS_RegionFind
⑴處的函數LOS_RegionFind用於在進程虛擬地址空間內查找並返回指定虛擬地址對應的虛擬地址區間,兩個傳入參數分別是虛擬地址空間和虛擬內存地址。該函數有個兄弟函數LOS_RegionRangeFind(),見⑶處代碼,可以用於在進程空間內查找並返回指定地址范圍對應的虛擬地址區間,三個傳入參數分別指定指定進程空間、虛擬內存開始地址和地址長度(長度單位字節)。這2個函數都調用函數OsFindRegion()實現地址區間的查找,⑵處的第3個參數為1的原因是地址區間是左閉右開區間,區間的結束地址會減1。下文會分析該函數的代碼。
⑴ LosVmMapRegion *LOS_RegionFind(LosVmSpace *vmSpace, VADDR_T addr) { LosVmMapRegion *region = NULL; (VOID)LOS_MuxAcquire(&vmSpace->regionMux); ⑵ region = OsFindRegion(&vmSpace->regionRbTree, addr, 1); (VOID)LOS_MuxRelease(&vmSpace->regionMux); return region; } ⑶ LosVmMapRegion *LOS_RegionRangeFind(LosVmSpace *vmSpace, VADDR_T addr, size_t len) { LosVmMapRegion *region = NULL; (VOID)LOS_MuxAcquire(&vmSpace->regionMux); region = OsFindRegion(&vmSpace->regionRbTree, addr, len); (VOID)LOS_MuxRelease(&vmSpace->regionMux); return region; }
4.2 函數LOS_RegionAlloc
函數LOS_RegionAlloc用於從地址空間中申請空閑的虛擬地址區間。參數較多,LosVmSpace *vmSpace指定虛擬地址空間,VADDR_T vaddr指定虛擬地址,當為空時,從映射區申請虛擬地址;當不為空時,使用該虛擬地址。如果該虛擬地址已經被映射,會先相應的解除映射處理等。size_t len指定要申請的地區區間的長度。UINT32 regionFlags指定地區區間的標簽。VM_OFFSET_T pgoff指定內存頁偏移值。
我們具體看下代碼,⑴處如果指定的虛擬地址為空,則調用函數OsAllocRange()申請內存。⑵如果指定的虛擬地址不為空,則調用函數OsAllocSpecificRange申請虛擬內存,下文會詳細分析這2個申請函數。⑶處創建虛擬內存地址區間,然后指定地址區間的地址空間為當前空間vmSpace。⑷處把創建的地址區間插入地址空間的紅黑樹中。
LosVmMapRegion *LOS_RegionAlloc(LosVmSpace *vmSpace, VADDR_T vaddr, size_t len, UINT32 regionFlags, VM_OFFSET_T pgoff) { VADDR_T rstVaddr; LosVmMapRegion *newRegion = NULL; BOOL isInsertSucceed = FALSE; /** * If addr is NULL, then the kernel chooses the address at which to create the mapping; * this is the most portable method of creating a new mapping. If addr is not NULL, * then the kernel takes it as where to place the mapping; */ (VOID)LOS_MuxAcquire(&vmSpace->regionMux); if (vaddr == 0) { ⑴ rstVaddr = OsAllocRange(vmSpace, len); } else { /* if it is already mmapped here, we unmmap it */ ⑵ rstVaddr = OsAllocSpecificRange(vmSpace, vaddr, len, regionFlags); if (rstVaddr == 0) { VM_ERR("alloc specific range va: %#x, len: %#x failed", vaddr, len); goto OUT; } } if (rstVaddr == 0) { goto OUT; } ⑶ newRegion = OsCreateRegion(rstVaddr, len, regionFlags, pgoff); if (newRegion == NULL) { goto OUT; } newRegion->space = vmSpace; ⑷ isInsertSucceed = OsInsertRegion(&vmSpace->regionRbTree, newRegion); if (isInsertSucceed == FALSE) { (VOID)LOS_MemFree(m_aucSysMem0, newRegion); newRegion = NULL; } OUT: (VOID)LOS_MuxRelease(&vmSpace->regionMux); return newRegion; }
4.3 函數LOS_RegionFree
函數LOS_RegionFree用於釋放地區區間到地址空間中。⑴進行參數校驗,參數不能為空。⑵處如果開啟了虛擬文件系統宏,並且地址區間是有效的文件類型,則調用函數OsFilePagesRemove。⑶處如果開啟了共享內存,並且地址區間是共享的,則調用函數OsShmRegionFree釋放共享內存區間,分析共享內存部分時再詳細看該函數的代碼。⑷如果地址區間是設備類型的,則調用函數OsDevPagesRemove解除映射,否則執行⑸。這些函數都涉及虛實映射,會在虛實映射章節分析這些函數。⑹處把地址區間從紅黑樹上移除,並釋放地址區間結構體占用的內存。
STATUS_T LOS_RegionFree(LosVmSpace *space, LosVmMapRegion *region) { ⑴ if ((space == NULL) || (region == NULL)) { VM_ERR("args error, aspace %p, region %p", space, region); return LOS_ERRNO_VM_INVALID_ARGS; } (VOID)LOS_MuxAcquire(&space->regionMux); #ifdef LOSCFG_FS_VFS ⑵ if (LOS_IsRegionFileValid(region)) { OsFilePagesRemove(space, region); } else #endif #ifdef LOSCFG_KERNEL_SHM ⑶ if (OsIsShmRegion(region)) { OsShmRegionFree(space, region); } else if (LOS_IsRegionTypeDev(region)) { #else ⑷ if (LOS_IsRegionTypeDev(region)) { #endif OsDevPagesRemove(&space->archMmu, region->range.base, region->range.size >> PAGE_SHIFT); } else { ⑸ OsAnonPagesRemove(&space->archMmu, region->range.base, region->range.size >> PAGE_SHIFT); } /* remove it from space */ ⑹ LOS_RbDelNode(&space->regionRbTree, ®ion->rbNode); /* free it */ LOS_MemFree(m_aucSysMem0, region); (VOID)LOS_MuxRelease(&space->regionMux); return LOS_OK; }
4.4 虛擬內存內部實現函數
4.4.1 函數OsAllocRange
函數OsAllocRange用於從虛擬地址空間中申請指定長度的內存,返回值為申請到的虛擬地址。⑴處從進程空間中獲取映射區開始地址對應的地址區間。當獲取的地址區間不為NULL時,執行⑵,獲取地址區間的紅黑樹節點,並獲取該地址區間的結束地址。⑶處使用紅黑樹的宏對RB_MID_SCAN和RB_MID_SCAN_END,循環遍歷紅黑樹節點pstRbNode及其后續節點。⑷處如果當前遍歷節點和映射區獲取的地址區間有重疊則繼續遍歷下一個節點。⑸處如果地址區間長度滿足要求,則返回虛擬地址,否則執行⑹更新地址區間的結束地址繼續遍歷。
當從映射區獲取的地址區間為NULL時,執行⑺。紅黑樹的宏對RB_SCAN_SAFE和RB_SCAN_SAFE_END會從第一個樹節點循環遍歷。循環體內的內容和上文重復,不再贅述。⑻如果映射區沒有申請到合適的虛擬地址,則判斷下在映射區后的地址區間是否滿足條件。如果依舊申請不到合適的虛擬地址,返回0。
VADDR_T OsAllocRange(LosVmSpace *vmSpace, size_t len) { LosVmMapRegion *curRegion = NULL; LosRbNode *pstRbNode = NULL; LosRbNode *pstRbNodeTmp = NULL; LosRbTree *regionRbTree = &vmSpace->regionRbTree; VADDR_T curEnd = vmSpace->mapBase; VADDR_T nextStart; ⑴ curRegion = LOS_RegionFind(vmSpace, vmSpace->mapBase); if (curRegion != NULL) { ⑵ pstRbNode = &curRegion->rbNode; curEnd = curRegion->range.base + curRegion->range.size; ⑶ RB_MID_SCAN(regionRbTree, pstRbNode) curRegion = (LosVmMapRegion *)pstRbNode; nextStart = curRegion->range.base; ⑷ if (nextStart < curEnd) { continue; } ⑸ if ((nextStart - curEnd) >= len) { return curEnd; } else { ⑹ curEnd = curRegion->range.base + curRegion->range.size; } RB_MID_SCAN_END(regionRbTree, pstRbNode) } else { /* rbtree scan is sorted, from small to big */ ⑺ RB_SCAN_SAFE(regionRbTree, pstRbNode, pstRbNodeTmp) curRegion = (LosVmMapRegion *)pstRbNode; nextStart = curRegion->range.base; if (nextStart < curEnd) { continue; } if ((nextStart - curEnd) >= len) { return curEnd; } else { curEnd = curRegion->range.base + curRegion->range.size; } RB_SCAN_SAFE_END(regionRbTree, pstRbNode, pstRbNodeTmp) } ⑻ nextStart = vmSpace->mapBase + vmSpace->mapSize; if ((nextStart >= curEnd) && ((nextStart - curEnd) >= len)) { return curEnd; } return 0; }
4.4.2 函數OsAllocSpecificRange
函數OsAllocSpecificRange用於從虛擬地址空間中申請指定長度的內存,如果指定的虛擬地址已經被映射,則取消映射,返回值為申請到的虛擬地址。⑴處驗證虛擬內存塊是否在虛擬地址空間范圍內。⑵處判斷虛擬地址是否已經屬於某個地址區間,如果不屬於任何地址區間,則執行⑸返回該虛擬地址;如果屬於某個地址區間,則繼續執行⑶,如果地址區間標簽包含VM_MAP_REGION_FLAG_FIXED_NOREPLACE,不允許替換,則返回0;如果標簽包含VM_MAP_REGION_FLAG_FIXED,則調用LOS_UnMMap取消映射。如果不包含上述標簽,則執行⑷,重新申請地址區間。
VADDR_T OsAllocSpecificRange(LosVmSpace *vmSpace, VADDR_T vaddr, size_t len, UINT32 regionFlags) { STATUS_T status; ⑴ if (LOS_IsRangeInSpace(vmSpace, vaddr, len) == FALSE) { return 0; } ⑵ if ((LOS_RegionFind(vmSpace, vaddr) != NULL) || (LOS_RegionFind(vmSpace, vaddr + len - 1) != NULL) || (LOS_RegionRangeFind(vmSpace, vaddr, len - 1) != NULL)) { ⑶ if ((regionFlags & VM_MAP_REGION_FLAG_FIXED_NOREPLACE) != 0) { return 0; } else if ((regionFlags & VM_MAP_REGION_FLAG_FIXED) != 0) { status = LOS_UnMMap(vaddr, len); if (status != LOS_OK) { VM_ERR("unmmap specific range va: %#x, len: %#x failed, status: %d", vaddr, len, status); return 0; } } else { ⑷ return OsAllocRange(vmSpace, len); } } ⑸ return vaddr; }
4.4.3 函數OsCreateRegion
函數OsCreateRegion用於根據虛擬地址、內存大小、地址區間標簽等信息創建地址區間。⑴處為地址區間結構體申請內存,⑵處根據參數設置地址區間屬性值。代碼比較簡單,自行閱讀即可。
LosVmMapRegion *OsCreateRegion(VADDR_T vaddr, size_t len, UINT32 regionFlags, unsigned long offset) { ⑴ LosVmMapRegion *region = LOS_MemAlloc(m_aucSysMem0, sizeof(LosVmMapRegion)); if (region == NULL) { VM_ERR("memory allocate for LosVmMapRegion failed"); return region; } ⑵ region->range.base = vaddr; region->range.size = len; region->pgOff = offset; region->regionFlags = regionFlags; region->regionType = VM_MAP_REGION_TYPE_NONE; region->forkFlags = 0; region->shmid = -1; return region; }
4.4.4 函數OsInsertRegion
函數OsInsertRegion用於把紅黑樹節點插入紅黑樹。⑴處調用函數LOS_RbAddNode插入紅黑樹節點,LosVmMapRegion結構體的第一個成員是LosRbNode類型,二者可以強轉。⑵處如果插入節點失敗,則打印地址空間信息。代碼比較簡單。
BOOL OsInsertRegion(LosRbTree *regionRbTree, LosVmMapRegion *region) { ⑴ if (LOS_RbAddNode(regionRbTree, (LosRbNode *)region) == FALSE) { VM_ERR("insert region failed, base: %#x, size: %#x", region->range.base, region->range.size); ⑵ OsDumpAspace(region->space); return FALSE; } return TRUE; }
4.4.5 函數OsFindRegion
函數OsFindRegion實現根據虛擬內存地址查找地址區間。⑴處設置地址區間范圍的開始地址和大小。⑵處調用函數LOS_RbGetNode()從紅黑樹上獲取紅黑樹節點pstRbNode,獲取成功時會繼續執行⑶從紅黑樹節點轉換為需要的地址區間。后續會有專門的系列講解紅黑樹,屆時再分析函數LOS_RbGetNode()。
LosVmMapRegion *OsFindRegion(LosRbTree *regionRbTree, VADDR_T vaddr, size_t len) { LosVmMapRegion *regionRst = NULL; LosRbNode *pstRbNode = NULL; LosVmMapRange rangeKey; ⑴ rangeKey.base = vaddr; rangeKey.size = len; ⑵ if (LOS_RbGetNode(regionRbTree, (VOID *)&rangeKey, &pstRbNode)) { ⑶ regionRst = (LosVmMapRegion *)LOS_DL_LIST_ENTRY(pstRbNode, LosVmMapRegion, rbNode); } return regionRst; }
5、VMalloc常用操作
內核動態分配虛擬地址空間操作分為申請和釋放2個操作。
5.1 函數LOS_VMalloc
函數LOS_VMalloc用於從VMalloc動態分配內存堆虛擬地址空間中申請內存,參數為需要申請的字節數。⑴處把申請的內存大小進行頁對齊,並由字節數計算頁數sizeCount。⑵處聲明一個內存頁雙向鏈表。⑶處申請指定數量的物理內存頁並掛載到雙向鏈表pageList上。⑷處從動態內存分配堆進程空間g_vMallocSpace中申請虛擬內存地址區間。此時成功申請了虛擬內存和物理內存,而且頁數也是一樣的,下面執行⑸循環遍歷物理頁雙向鏈表上的每一個內存頁進行虛實映射。⑹處獲取物理內存頁的物理內存地址,然后把物理內存頁的引用計數自增加1。⑺處進行虛實映射,然后把虛擬內存地址增加一個內存頁的大小,繼續循環遍歷。⑻處返回申請到的虛擬地址區間的內存開始地址。虛實映射函數LOS_ArchMmuMap在MMU虛實映射系列來詳細講解。
VOID *LOS_VMalloc(size_t size) { LosVmSpace *space = &g_vMallocSpace; LosVmMapRegion *region = NULL; size_t sizeCount; size_t count; LosVmPage *vmPage = NULL; VADDR_T va; PADDR_T pa; STATUS_T ret; ⑴ size = LOS_Align(size, PAGE_SIZE); if ((size == 0) || (size > space->size)) { return NULL; } sizeCount = size >> PAGE_SHIFT; ⑵ LOS_DL_LIST_HEAD(pageList); (VOID)LOS_MuxAcquire(&space->regionMux); ⑶ count = LOS_PhysPagesAlloc(sizeCount, &pageList); if (count < sizeCount) { VM_ERR("failed to allocate enough pages (ask %zu, got %zu)", sizeCount, count); goto ERROR; } /* allocate a region and put it in the aspace list */ ⑷ region = LOS_RegionAlloc(space, 0, size, VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE, 0); if (region == NULL) { VM_ERR("alloc region failed, size = %x", size); goto ERROR; } va = region->range.base; ⑸ while ((vmPage = LOS_ListRemoveHeadType(&pageList, LosVmPage, node))) { ⑹ pa = vmPage->physAddr; LOS_AtomicInc(&vmPage->refCounts); ⑺ ret = LOS_ArchMmuMap(&space->archMmu, va, pa, 1, region->regionFlags); if (ret != 1) { VM_ERR("LOS_ArchMmuMap failed!, err;%d", ret); } va += PAGE_SIZE; } (VOID)LOS_MuxRelease(&space->regionMux); ⑻ return (VOID *)(UINTPTR)region->range.base; ERROR: (VOID)LOS_PhysPagesFree(&pageList); (VOID)LOS_MuxRelease(&space->regionMux); return NULL; }
5.2 函數LOS_VFree
函數LOS_VFree用於釋放從VMalloc動態內存堆虛擬地址空間中申請的虛擬內存,傳入參數為虛擬地址。 ⑴處根據虛擬地址獲取虛擬地址區間,然后執行⑵釋放地址區間,其中函數LOS_RegionFree在前文已經詳細講述。
VOID LOS_VFree(const VOID *addr) { LosVmSpace *space = &g_vMallocSpace; LosVmMapRegion *region = NULL; STATUS_T ret; if (addr == NULL) { VM_ERR("addr is NULL!"); return; } (VOID)LOS_MuxAcquire(&space->regionMux); ⑴ region = LOS_RegionFind(space, (VADDR_T)(UINTPTR)addr); if (region == NULL) { VM_ERR("find region failed"); goto DONE; } ⑵ ret = LOS_RegionFree(space, region); if (ret) { VM_ERR("free region failed, ret = %d", ret); } DONE: (VOID)LOS_MuxRelease(&space->regionMux); }
6 其他
6.1 函數LOS_VmSpaceReserve
函數LOS_VmSpaceReserve用於在在進程空間中預留一塊內存空間。⑴處先做參數校驗。⑵處先判斷虛擬地址和大小在指定的虛擬地址空間內。⑶處查詢指定的虛擬地址的映射標簽。⑷處加上標簽VM_MAP_REGION_FLAG_FIXED申請一段地址區間。
STATUS_T LOS_VmSpaceReserve(LosVmSpace *space, size_t size, VADDR_T vaddr) { UINT32 regionFlags = 0; ⑴ if ((space == NULL) || (size == 0) || (!IS_PAGE_ALIGNED(vaddr) || !IS_PAGE_ALIGNED(size))) { return LOS_ERRNO_VM_INVALID_ARGS; } ⑵ if (!LOS_IsRangeInSpace(space, vaddr, size)) { return LOS_ERRNO_VM_OUT_OF_RANGE; } /* lookup how it's already mapped */ ⑶ (VOID)LOS_ArchMmuQuery(&space->archMmu, vaddr, NULL, ®ionFlags); /* build a new region structure */ ⑷ LosVmMapRegion *region = LOS_RegionAlloc(space, vaddr, size, regionFlags | VM_MAP_REGION_FLAG_FIXED, 0); return region ? LOS_OK : LOS_ERRNO_VM_NO_MEMORY; }
總結
本文分析虛擬內存管理的相關源代碼,首先介紹虛擬內存管理的結構體、相關宏定義,接着會分析內核虛擬地址空間和用戶進程虛擬地址空間如何初始化,然后分析虛擬內存區間常用操作包含查找、申請和釋放等,最后分析動態內存堆的申請、釋放接口的源代碼,並簡單介紹下內存區間預留接口源代碼。后續也會陸續推出更多的分享文章,敬請期待,有任何問題、建議,都可以留言給我。謝謝。