HarmonyOS内存模块分析

内存分配函数分析

Posted by Weizhou on October 12, 2020

前言

当前HarmonyOS开源的源码适用于小型IOT设备,从开源仓代码分析,内核使用的是LiteOS,但与现有的 开源LiteOS有着较大的不同。其中与开源版本LiteOS最大的不同之处当数内存管理模块。HarmonyOS支持Arm Cortex-A7 并且可以通过使能MMU对虚拟内存进行管理。该系列文章将对HarmonyOS内存模块进行详细的代码剖析讲解, 该篇文章的分析内容主要侧重与内存模块中分配过程所涉及的函数,后续将会对内存初始化,ELF加载等过程进行 详细的解读。

正文

该文分析的内容类比与Linux,从kmalloc和vmalloc入手,分析内核中内存分析的流程。 Linux中kmalloc可类比于HarmonyOS中的LOS_KernelMalloc,vmalloc可类比于HarmonyOS中的LOS_VMalloc函数。 本文将从两个函数入手,分析页面的分配管理方法以及涉及到的相关结构体。

LOS_KernelMalloc

函数原型: VOID *LOS_KernelMalloc(UINT32 size)
  LOS_KernelMalloc可类比于Linux内核的kmalloc申请连续的物理地址内存空间,但对比kmalloc的接受传参,LOS_KernelMalloc函数缺少了flags入参,该入参在Linux中表示分配出的页内存空间的不同用途。如GFP_KERNEL表示分配出的内核内存页,用于内核进程;GFP_USER表示分配出的页内存将用于用户态进程。下文将从LOS_KernelMalloc入手进行逐层分析HMOS的内存分配策略和实现方式。
  内核主要使用LOS_KernelMalloc接口申请内核态内存空间。LOS_KernelMalloc的分配策略大体与Linux的策略相似:分配大块内存时(超过4K)调用LOS_PhysPagesAllocContiguous函数以页为单位分配出一个连续的内存空间;否则调用LOS_MemAlloc函数(与开源LiteOS函数接口相同),该函数的内存分配策略为bestfit,从内存池中找到合适的大小进行分配。

函数源码:

VOID *LOS_KernelMalloc(UINT32 size)
{
    VOID *ptr = NULL;

    if (OsMemLargeAlloc(size)) {
        ptr = LOS_PhysPagesAllocContiguous(ROUNDUP(size, PAGE_SIZE) >> PAGE_SHIFT);
    } else {
        ptr = LOS_MemAlloc(OS_SYS_MEM_ADDR, size);
    }

    return ptr;
}

接下来主要分析页面分配的流程,LOS_KernelMalloc获取page的关键调用路径:
LOS_KernelMalloc->LOS_PhysPagesAllocContiguous-> OsVmPhysPagesGet->OsVmPhysPagesAlloc接下来将对涉及的函数进行逐个分析。

LOS_PhysPagesAllocContiguous

VOID *LOS_PhysPagesAllocContiguous(size_t nPages)
{
    LosVmPage *page = NULL;

    if (nPages == 0) {
        return NULL;
    }

    page = OsVmPhysPagesGet(nPages);
    if (page == NULL) {
        return NULL;
    }

    return OsVmPageToVaddr(page);
}

  该函数主要干了两件事情,首先调用OsVmPhysPagesGet函数获取指定数量的页内存,再通过调用OsVmPageToVaddr函数将页内存转换成虚拟地址。

OsVmPhysPagesGet

STATIC LosVmPage *OsVmPhysPagesGet(size_t nPages)
{
    UINT32 intSave;
    struct VmPhysSeg *seg = NULL;
    LosVmPage *page = NULL;
    UINT32 segID;

    if (nPages == 0) {
        return NULL;
    }

    for (segID = 0; segID < g_vmPhysSegNum; segID++) {
        seg = &g_vmPhysSeg[segID];
        LOS_SpinLockSave(&seg->freeListLock, &intSave);
        page = OsVmPhysPagesAlloc(seg, nPages);
        if (page != NULL) {
            /* the first page of continuous physical addresses holds refCounts */
            LOS_AtomicSet(&page->refCounts, 0);
            page->nPages = nPages;
            LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
            return page;
        }
        LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
    }
    return NULL;
}

  在HMOS的内存管理机制中引入了VmPhysSeg结构体,该结构体可以理解为是Linux中简易的Zone,VmPhysSeg的结构体如所示:

typedef struct VmPhysSeg {
    PADDR_T start;            /* The start of physical memory area */
    size_t size;              /* The size of physical memory area */
    LosVmPage *pageBase;      /* The first page address of this area */

    SPIN_LOCK_S freeListLock; /* The buddy list spinlock */
    struct VmFreeList freeList[VM_LIST_ORDER_MAX];  /* The free pages in the buddy list */

    SPIN_LOCK_S lruLock;
    size_t lruSize[VM_NR_LRU_LISTS];
    LOS_DL_LIST lruList[VM_NR_LRU_LISTS];
} LosVmPhysSeg;

  Linux中zone的作用主要是区分DMA内存区和普通内存区,HMOS中的Seg结构体有相似的功能,但结构体中成员变量数量相对于Linux的Zone结构体减少了很多。这里不做具体的区别分析,只做HMOS内存管理的分析。HMOS的seg结构体更类似于段页式内存管理中对段的描述。支持将系统所有可用的内存地址空间分割成多个段,再将段分为不同数量的页挂载到freeList列表中。
  start成员变量表示该seg结构体管理的物理内存起始地址;size表示该区域的大小;pageBase指向该块内存区域起始的页内存结构体;freeList是结构体数组,使用伙伴算法管理内存页。VM_LIST_ORDER_MAX最大值为9,基于伙伴内存的算法可判断出freeList[0]中应该存放的是2^0数量大小的连续也空间,最大2^8表示支持最大256个连续页空间的内存,即管理256*4k大小的空间。
  从页分配算法来说,OsVmPhysPagesGet函数遍历了所有的seg,然后调用OsVmPhysPagesAlloc函数从能够满足分配npage数量大小的连续页内存的seg结构体中获取到page结构体指针。

OsVmPhysPagesAlloc

LosVmPage *OsVmPhysPagesAlloc(struct VmPhysSeg *seg, size_t nPages)
{
    struct VmFreeList *list = NULL;
    LosVmPage *page = NULL;
    UINT32 order;
    UINT32 newOrder;

    if ((seg == NULL) || (nPages == 0)) {
        return NULL;
    }

    order = OsVmPagesToOrder(nPages);
    if (order < VM_LIST_ORDER_MAX) {
        for (newOrder = order; newOrder < VM_LIST_ORDER_MAX; newOrder++) {
            list = &seg->freeList[newOrder];
            if (LOS_ListEmpty(&list->node)) {
                continue;
            }
            page = LOS_DL_LIST_ENTRY(LOS_DL_LIST_FIRST(&list->node), LosVmPage, node);
            goto DONE;
        }
    }
    return NULL;
DONE:
    OsVmPhysFreeListDelUnsafe(page);
    OsVmPhysPagesSpiltUnsafe(page, order, newOrder);
    return page;
}

  首先通过OsVmPagesToOrder函数计算出nPages对应所需最小的order值,然后以order值为初始值向上便利当前seg结构体中freeList[order]上是否有空闲的连续order大小的连续page内存空间。最后通过OsVmPhysFreeListDelUnsafe函数将该page从该seg的freeList[order]数组中删除;再通过OsVmPagesSplitUnsafe函数将当前order的连续page内存页做适当的split操作,并将未使用的page挂在到对应order大小的freeList[order]数组上,这样就完成了一次page的分配过程。

LOS_VMalloc

  LOS_VMalloc可类比于Linux的vmalloc函数,用于申请连续的虚拟内存地址空间。但物理页面是不连续的。一般情况下,只有硬件设备才需要物理地址连续的内存,因为硬件设备往往存在于MMU之外,根本不了解虚拟地址;但为了性能上的考虑,内核中一般使用 kmalloc(),而只有在需要获得大块内存时才使用vmalloc(),例如当模块被动态加载到内核当中时,就把模块装载到由vmalloc()分配 的内存上。
  Kmalloc与Vmalloc的却别:kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续;kmalloc分配的物理地址与虚拟地址只有一个PAGE—OFFSET偏移,不需要为地址段修改页表,Vmalloc类函数地址完全虚拟,每次分配都需要对页表进行设置,效率相较于Kmalloc低。
  在详细解析Vmalloc之前,首先要介绍space(虚拟内存空间)和region(线性区描述符)的概念。一个虚拟内存空间(space)中存在多个先行区描述符(region),Linux内核中对这些虚拟存储区域的组织方式有两种,一种是采用双循环链表(regions),还有一种是采用红黑树的结构。两个结构体的定义如下:

VmSpace

typedef struct VmSpace {
    LOS_DL_LIST         node;           /**< vm space dl list */
    LOS_DL_LIST         regions;        /**< region dl list */
    LosRbTree           regionRbTree;   /**< region red-black tree root */
    LosMux              regionMux;      /**< region list mutex lock */
    VADDR_T             base;           /**< vm space base addr */
    UINT32              size;           /**< vm space size */
    VADDR_T             heapBase;       /**< vm space heap base address */
    VADDR_T             heapNow;        /**< vm space heap base now */
    LosVmMapRegion      *heap;          /**< heap region */
    VADDR_T             mapBase;        /**< vm space mapping area base */
    UINT32              mapSize;        /**< vm space mapping area size */
    LosArchMmu          archMmu;        /**< vm mapping physical memory */
#ifdef LOSCFG_DRIVERS_TZDRIVER
    VADDR_T             codeStart;      /**< user process code area start */
    VADDR_T             codeEnd;        /**< user process code area end */
#endif
} LosVmSpace;

  结构体中成员变量node用于连接多个虚拟内存空间;regions即为连接space中regions的双向链表,regionRbTree为采用红黑树结构表示的space中的regions,regionRbTree与regions使用两种方式表实一个相同的regions集合
  HMOS一共定义了三类虚拟内存空间:g_kVmSpace,g_vMallocSpace和各进程的VmSpace。在创建每一个进程的时候都会创建ProcessCB->vmSpace结构体记录虚拟内存空间;g_kVmSpace和g_vMallocSpace是在系统初始化时调用OsKSpaceInit(VOID)函数完成内核相关内存初始化。

LosVmMapRegion

struct VmMapRegion {
    LosRbNode           rbNode;         /**< region red-black tree node */
    LosVmSpace          *space;
    LOS_DL_LIST         node;           /**< region dl list */
    LosVmMapRange       range;          /**< region address range */
    VM_OFFSET_T         pgOff;          /**< region page offset to file */
    UINT32              regionFlags;   /**< region flags: cow, user_wired */
    UINT32              shmid;          /**< shmid about shared region */
    UINT8               protectFlags;   /**< vm region protect flags: PROT_READ, PROT_WRITE, */
    UINT8               forkFlags;      /**< vm space fork flags: COPY, ZERO, */
    UINT8               regionType;     /**< vm region type: ANON, FILE, DEV */
    union {
        struct VmRegionFile {
            unsigned int fileMagic;
            struct file *file;
            const LosVmFileOps *vmFOps;
        } rf;
        struct VmRegionAnon {
            LOS_DL_LIST  node;          /**< region LosVmPage list */
        } ra;
        struct VmRegionDev {
            LOS_DL_LIST  node;          /**< region LosVmPage list */
            const LosVmFileOps *vmFOps;
        } rd;
    } unTypeData;
};

HMOS中的VmMapRegion与Linux中的vm_area_struct结构体相似,称为线性区描述符,它标识了一个线性地址区间,每一个线性区由多个page组成。
函数原型: VOID *LOS_VMalloc(size_t size)
函数源码:

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

  首先调用LOS_PhysPagesAlloc函数分配出sizeCount数量的页面内存,并将其加入到pageList列表中;然后调用LOS_RegionAlloc函数分配/创建一个region结构体;最后在while循环中从pageList中逐个将page取下来并通过LOS_ArchMmuMap函数将list上的全部页面的物理地址(pa)映射到虚拟地址(va)上,每次循环映射一个页面地址空间,va以PAGE_SIZE大小进行增长。下面会对上述涉及到的函数进行逐步解析。

LOS_PhysPagesAlloc

函数源码:

size_t LOS_PhysPagesAlloc(size_t nPages, LOS_DL_LIST *list)
{
    LosVmPage *page = NULL;
    size_t count = 0;

    if ((list == NULL) || (nPages == 0)) {
        return 0;
    }

    while (nPages--) {
        page = OsVmPhysPagesGet(ONE_PAGE);
        if (page == NULL) {
            break;
        }
        LOS_ListTailInsert(list, &page->node);
        count++;
    }

    return count;
}

  nPages决定while循环次数,在每次循环中都会通过调用OsVmPhysPagesGet函数获取到一个页面内存,该函数在上文中已有分析。在获取到一个单独页之后将其添加到list列表中,从而实现了在list列表上添加了nPages个物理地址不连续的页面。

LosVmMapRegion

函数源码:

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);
        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;
}

  首先判断vaddr是否为0,为0表示没有指定的虚拟内存地址,调用OsAllocRange函数初始化虚拟地址。在成功获取到正确的rstVaddr地址后调用OsCreateRegion创建LosVmMapRegion结构体,并通过OsInsertRegion函数将创建的region结构体作为树节点添加到LosVmSpace结构体的RegionRbTree红黑树上。
  LOS_RegionAlloc函数传参中由LOS_VMalloc函数传入的vmSpace指针指向g_vMallocSpace全局变量,表示LOS_VMalloc函数获取到的虚拟内存空间全部映射到g_vMallocSpace的space空间中。

LOS_ArchMmuMap

函数源码:

status_t LOS_ArchMmuMap(LosArchMmu *archMmu, VADDR_T vaddr, PADDR_T paddr, size_t count, UINT32 flags)
{
    PTE_T l1Entry;
    UINT32 saveCounts = 0;
    INT32 mapped = 0;
    INT32 checkRst;

    checkRst = OsMapParamCheck(flags, vaddr, paddr);
    if (checkRst < 0) {
        return checkRst;
    }

    /* see what kind of mapping we can use */
    while (count > 0) {
        if (MMU_DESCRIPTOR_IS_L1_SIZE_ALIGNED(vaddr) &&
            MMU_DESCRIPTOR_IS_L1_SIZE_ALIGNED(paddr) &&
            count >= MMU_DESCRIPTOR_L2_NUMBERS_PER_L1) {
            /* compute the arch flags for L1 sections cache, r ,w ,x, domain and type */
            saveCounts = OsMapSection(archMmu, flags, &vaddr, &paddr, &count);
        } else {
            /* have to use a L2 mapping, we only allocate 4KB for L1, support 0 ~ 1GB */
            l1Entry = OsGetPte1(archMmu->virtTtb, vaddr);
            if (OsIsPte1Invalid(l1Entry)) {
                OsMapL1PTE(archMmu, &l1Entry, vaddr, flags);
                saveCounts = OsMapL2PageContinous(l1Entry, flags, &vaddr, &paddr, &count);
            } else if (OsIsPte1PageTable(l1Entry)) {
                saveCounts = OsMapL2PageContinous(l1Entry, flags, &vaddr, &paddr, &count);
            } else {
                LOS_Panic("%s %d, unimplemented tt_entry %x\n", __FUNCTION__, __LINE__, l1Entry);
            }
        }
        mapped += saveCounts;
    }

    return mapped;
}

  这段代码涉及ARM Cortex-A MMU底层工作逻辑,所以首先需要了解经典的ARM MMU工作结构,再进一步对源码进行解读,MMU工作原理示意图如下:
1.png
  HMOS使用的是MMU二级映射,第一级为PGD使用4k空间存放第二级PTE的地址。一级的PGD地址是1M对齐,二级PTE为4K地址对齐。在一级表中有两种的地址有两种形式:一种是直接映射1MB的地址空间(称为section模式),另一种是映射到对应二级页表的起始地址(称为table模式)。
  回过头来看LOS_ArchMmuMap函数源码逻辑,在while循环中有两个分支,在进入第一个if分支前做了如下三个判断:vaddr是否为0x100000地址对齐,paddr是否为0x100000地址对齐,以及count是否大于256。count表实4k页面的数量,在PGD中每一个PGD最多能够映射256个页面,所以第一个分支的含义是,如果映射的起始地址是1M对齐,且映射的内存大小超过1M,则执行第一个分支,进行section模式的直接地址映射。
  在第一个分支中调用了函数OsMapSection()函数,源码逻辑如下:

STATIC UINT32 OsMapSection(const LosArchMmu *archMmu, UINT32 flags, VADDR_T *vaddr,
                           PADDR_T *paddr, UINT32 *count)
{
    UINT32 mmuFlags = 0;

    mmuFlags |= OsCvtSecFlagsToAttrs(flags);
    OsSavePte1(OsGetPte1Ptr(archMmu->virtTtb, *vaddr),
        OsTruncPte1(*paddr) | mmuFlags | MMU_DESCRIPTOR_L1_TYPE_SECTION);
    *count -= MMU_DESCRIPTOR_L2_NUMBERS_PER_L1;
    *vaddr += MMU_DESCRIPTOR_L1_SMALL_SIZE;
    *paddr += MMU_DESCRIPTOR_L1_SMALL_SIZE;

    return MMU_DESCRIPTOR_L2_NUMBERS_PER_L1;
}

其中最关键的一步是OsSavePte1函数。该函数首先通过OsGetPtePtr函数获取到第一级4k内存空间中va对应的table ptr地址。获取到地址后将paddr赋值到该地址对应的列表内存中,即完成了1M section的地址映射过程,即这里使用的是第一种的直接映射1MB内存空间的方式。
  但是在LOS_VMalloc函数中第一个分支是不会执行到的,LOS_ArchMmuMap函数在LOS_VMalloc中被调用时传参count值始终为1,所以在该场景下始终执行的时else分支。源码如下所示:

} else {
    /* have to use a L2 mapping, we only allocate 4KB for L1, support 0 ~ 1GB */
    l1Entry = OsGetPte1(archMmu->virtTtb, vaddr);
    if (OsIsPte1Invalid(l1Entry)) {
        OsMapL1PTE(archMmu, &l1Entry, vaddr, flags);
        saveCounts = OsMapL2PageContinous(l1Entry, flags, &vaddr, &paddr, &count);
    } else if (OsIsPte1PageTable(l1Entry)) {
        saveCounts = OsMapL2PageContinous(l1Entry, flags, &vaddr, &paddr, &count);
    } else {
        LOS_Panic("%s %d, unimplemented tt_entry %x\n", __FUNCTION__, __LINE__, l1Entry);
    }
}

该分支中首先调用OsGetPte1()函数获得L1中的页表项,然后进行了两个判断:首先判断该页表项中的内容是否是合法的,然后判断该页表项中的内容是否是page模式(上文提及到与page模式相对的是section模式)。判断页表项是否合法可理解为该页表项是否已被初始化,若没有初始化,则首先调用OsMapL1PTE函数对页表项内容进行初始化:

STATIC VOID OsMapL1PTE(LosArchMmu *archMmu, PTE_T *pte1Ptr, vaddr_t vaddr, UINT32 flags)
{
    paddr_t pte2Base = 0;

    if (OsGetL2Table(archMmu, OsGetPte1Index(vaddr), &pte2Base) != LOS_OK) {
        LOS_Panic("%s %d, failed to allocate pagetable\n", __FUNCTION__, __LINE__);
    }

    *pte1Ptr = pte2Base | MMU_DESCRIPTOR_L1_TYPE_PAGE_TABLE;
    if (flags & VM_MAP_REGION_FLAG_NS) {
        *pte1Ptr |= MMU_DESCRIPTOR_L1_PAGETABLE_NON_SECURE;
    }
    *pte1Ptr &= MMU_DESCRIPTOR_L1_SMALL_DOMAIN_MASK;
    *pte1Ptr |= MMU_DESCRIPTOR_L1_SMALL_DOMAIN_CLIENT; // use client AP
    OsSavePte1(OsGetPte1Ptr(archMmu->virtTtb, vaddr), *pte1Ptr);
}

  总体的流程是获取二级页表的base地址pte2Base,然后基于flags变量设置pte2Base值,最后将设置好的页表项值填入到pte1Ptr指向的内存。
  在完成页表项内容初始化后调用OsMapL2PageContinous()函数完成二级页表页表项的配置:

STATIC UINT32 OsMapL2PageContinous(PTE_T pte1, UINT32 flags, VADDR_T *vaddr, PADDR_T *paddr, UINT32 *count)
{
    PTE_T *pte2BasePtr = NULL;
    UINT32 archFlags;
    UINT32 saveCounts;

    pte2BasePtr = OsGetPte2BasePtr(pte1);
    if (pte2BasePtr == NULL) {
        LOS_Panic("%s %d, pte1 %#x error\n", __FUNCTION__, __LINE__, pte1);
    }

    /* compute the arch flags for L2 4K pages */
    archFlags = OsCvtPte2FlagsToAttrs(flags);
    saveCounts = OsSavePte2Continuous(pte2BasePtr, OsGetPte2Index(*vaddr), *paddr | archFlags, *count);
    *paddr += (saveCounts << MMU_DESCRIPTOR_L2_SMALL_SHIFT);
    *vaddr += (saveCounts << MMU_DESCRIPTOR_L2_SMALL_SHIFT);
    *count -= saveCounts;
    return saveCounts;
}

首先调用OsGetPte2BasePtr()函数获取pte2Base基址指针,其次调用OsCvtPte2FlagsToAttrs()函数对页面属性进行配置,最后调用OsSavePte2Continuous()函数进行二级页表项的填充:

STATIC INLINE UINT32 OsSavePte2Continuous(PTE_T *pte2BasePtr, UINT32 index, PTE_T pte2, UINT32 count)
{
    UINT32 saveCounts = 0;
    if (count == 0) {
        return 0;
    }

    DMB;
    do {
        pte2BasePtr[index++] = pte2;
        count--;
        pte2 += MMU_DESCRIPTOR_L2_SMALL_SIZE;
        saveCounts++;
    } while ((count != 0) && (index != MMU_DESCRIPTOR_L2_NUMBERS_PER_L1));
    DSB;

    return saveCounts;
}

至此LOS_ArchMmuMap()函数完成了对页面的映射过程,同样LOS_VMalloc也完成了内存的申请。