鴻蒙輕核心原始碼分析:虛擬記憶體

華為雲開發者社群發表於2021-11-12
摘要:本文以程式碼+文字的形式,介紹虛擬記憶體管理的結構體、相關巨集定義,分析核心虛擬地址空間和使用者程式虛擬地址空間如何初始化等內容。

本文分享自華為雲社群《鴻蒙輕核心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;
#endifreturn 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)) {
#elseif (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, &region->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, &regionFlags);

    /* 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;
}

總結

本文分析虛擬記憶體管理的相關原始碼,首先介紹虛擬記憶體管理的結構體、相關巨集定義,接著會分析核心虛擬地址空間和使用者程式虛擬地址空間如何初始化,然後分析虛擬記憶體區間常用操作包含查詢、申請和釋放等,最後分析動態記憶體堆的申請、釋放介面的原始碼,並簡單介紹下記憶體區間預留介面原始碼。後續也會陸續推出更多的分享文章,敬請期待,有任何問題、建議,都可以留言給我。謝謝。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章