鸿蒙轻内核A核源码分析系列四(3) 虚拟内存 原创 精华

zhushangyuan_
发布于 2021-11-9 20:57
浏览
4收藏

鸿蒙轻内核A核源码分析系列四 虚拟内存

【本文正在参与优质创作者激励】

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, &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_SCANRB_MID_SCAN_END,循环遍历红黑树节点pstRbNode及其后续节点。⑷处如果当前遍历节点和映射区获取的地址区间有重叠则继续遍历下一个节点。⑸处如果地址区间长度满足要求,则返回虚拟地址,否则执行⑹更新地址区间的结束地址继续遍历。

当从映射区获取的地址区间为NULL时,执行⑺。红黑树的宏对RB_SCAN_SAFERB_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_ArchMmuMapMMU虚实映射系列来详细讲解。

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

总结

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

【本文正在参与优质创作者激励】

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2021-12-7 14:13:07修改
5
收藏 4
回复
举报
回复
    相关推荐