写点什么

鸿蒙轻内核源码分析:虚拟内存

  • 2021 年 11 月 12 日
  • 本文字数:13675 字

    阅读完需:约 45 分钟

摘要:本文以代码+文字的形式,介绍虚拟内存管理的结构体、相关宏定义,分析内核虚拟地址空间和用户进程虚拟地址空间如何初始化等内容。

 

本文分享自华为云社区《鸿蒙轻内核A核源码分析系列四(2) 虚拟内存》,作者: zhushy 。

 

本文中所涉及的源码,以 OpenHarmonyLiteOS-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_Tvaddr 指定虚拟地址,当为空时,从映射区申请虚拟地址;当不为空时,使用该虚拟地址。如果该虚拟地址已经被映射,会先相应的解除映射处理等。size_tlen 指定要申请的地区区间的长度。UINT32regionFlags 指定地区区间的标签。VM_OFFSET_Tpgoff 指定内存页偏移值。


我们具体看下代码,⑴处如果指定的虚拟地址为空,则调用函数 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, &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;}
复制代码


7、总结


本文分析虚拟内存管理的相关源代码,首先介绍虚拟内存管理的结构体、相关宏定义,接着会分析内核虚拟地址空间和用户进程虚拟地址空间如何初始化,然后分析虚拟内存区间常用操作包含查找、申请和释放等,最后分析动态内存堆的申请、释放接口的源代码,并简单介绍下内存区间预留接口源代码。后续也会陆续推出更多的分享文章,敬请期待,有任何问题、建议,都可以留言给我。谢谢。


点击关注,第一时间了解华为云新鲜技术~

发布于: 21 分钟前阅读数: 3
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
鸿蒙轻内核源码分析:虚拟内存