电子产业一站式赋能平台

PCB联盟网

搜索
查看: 1405|回复: 0
收起左侧

鸿蒙内核源码分析(内存分配篇):内存的分配方式有哪些

[复制链接]

2607

主题

2607

帖子

7472

积分

高级会员

Rank: 5Rank: 5

积分
7472
发表于 2020-11-20 11:56:50 | 显示全部楼层 |阅读模式
鸿蒙内核源码分析(内存分配篇):内存的分配方式有哪些,   
鸿蒙内核有多少代码
内存部分占了整个kernel代码量近30%,代码多实现复杂,而且内存部分还分了两个文件夹mem,vm大书特书,为什么要分两个文件夹?应该是鸿蒙内核开发者想从目录的名称上区分内存的层级概念,vm是内存模块的更底层实现,mem是提供给上层使用对vm层的调用。


mem层 mem层介绍可以参考 LiteOS > 开发指南> 内核开发指南> 内存> 概述 看,有更详细的描述,这里结合代码说。 Huawei LiteOS的内存管理分为静态内存管理和动态内存管理,提供内存初始化、分配、释放等功能。


动态内存:在动态内存池中分配用户指定大小的内存块。

  • 优点:按需分配。
  • 缺点:内存池中可能出现碎片。
      



静态内存:在静态内存池中分配用户初始化时预设(固定)大小的内存块。

  • 优点:分配和释放效率高,静态内存池中无碎片。
  • 缺点:只能申请到初始化预设大小的内存块,不能按需申请。
      
动态内存管理,即在内存资源充足的情况下,从系统配置的一块比较大的连续内存(内存池),根据用户需求,分配任意大小的内存块。当用户不需要该内存块时,又可以释放回系统供下一次使用。与静态内存相比,动态内存管理的好处是按需分配,缺点是内存池中容易出现碎片。LiteOS动态内存支持DLINK和BEST LITTLE两种标准算法。


动态内存接口函数 动态内存管理模块为用户提供下面几种功能。
功能分类 接口名 描述
内存初始化 LOS_MemInit 初始化一块指定的动态内存池,大小为size。
申请动态内存 LOS_MemAlloc 从指定动态内存池中申请size长度的内存。
释放动态内存 LOS_MemFree 释放已申请的内存。
重新申请内存 LOS_MemRealloc 按size大小重新分配内存块,并保留原内存块内容。
内存对齐分配 LOS_MemAllocAlign 从指定动态内存池中申请长度为size且地址按boundary字节对齐的内存。
分析内存池状态 LOS_MemStatisticsGet 获取指定内存池的统计信息。
查看内存池中最大可用空闲块 LOS_MemGetMaxFreeBlkSize 获取指定内存池的最大可用空闲块。


这里LOS_MemAlloc被调用的情况,太长还有很多没截出来。
OsMemAllocWithCheck 采用内存池是嵌入式内存管理的一种常用做法,目的是减少申请和释放内存的开销,简单说就是先申请一大块内存,需要时从空闲链表中split,怎么切割鸿蒙就看最佳适应算法(best fit),用完了回收,node进入空闲链表,并从小到大排序,如果相邻两块都是可用内存块就合并。直接看LOS_MemAlloc内主要函数OsMemAllocWithCheck代码


      
  • STATIC INLINE VOID *OsMemAllocWithCheck(VOID *pool, UINT32 size, UINT32 intSave)
      
      
  • {
      
      
  •     LosMemDynNode *allocNode = NULL;
      
      
  •     UINT32 allocSize;
      
      
  •     LosMemPoolInfo *poolInfo = (LosMemPoolInfo *)pool;
      
      
  •     const VOID *firstNode = (const VOID *)((UINT8 *)OS_MEM_HEAD_ADDR(pool) + OS_DLNK_HEAD_SIZE);
      
      
  •     INT32 ret;
      
      

  •   
      
  •     if (OsMemAllocCheck(pool, intSave) == LOS_NOK) {
      
      
  •         return NULL;
      
      
  •     }
      
      

  •   
      
  •     allocSize = OS_MEM_ALIGN(size + OS_MEM_NODE_HEAD_SIZE, OS_MEM_ALIGN_SIZE);
      
      
  •     if (allocSize == 0) {
      
      
  •         return NULL;
      
      
  •     }
      
      
  • retry:
      
      

  •   
      
  •     allocNode = OsMemFindSuitableFreeBlock(pool, allocSize);//从内存池中找到合适的内存块
      
      
  •     if (allocNode == NULL) {
      
      
  •         if (poolInfo->flag & MEM_POOL_EXPAND_ENABLE) {
      
      
  •             ret = OsMemPoolExpand(pool, allocSize, intSave);//木有找到就扩展内存池
      
      
  •             if (ret == 0) {
      
      
  •                 goto retry;
      
      
  •             }
      
      
  •         }
      
      
  •         return NULL;
      
      
  •     }
      
      
  •     if ((allocSize + OS_MEM_NODE_HEAD_SIZE + OS_MEM_ALIGN_SIZE) <= allocNode->selfNode.sizeAndFlag) {
      
      
  •         OsMemSplitNode(pool, allocNode, allocSize);//找到了就劈开node
      
      
  •     }
      
      
  •     OsMemListDelete(&allocNode->selfNode.freeNodeInfo, firstNode);//从空闲双链表中删除该节点
      
      
  •     return (allocNode + 1);
      
      
  • }

复制代码
很显然,最佳适应算法(best fit)去带来很多极小块内存碎片的问题。


vm层 vm目录:是虚拟内存的代码实现,包括物理内存的段页式管理,内存虚拟地址<->物理地址映射,缺页中断处理,分配大块内存的伙伴算法,LRU置换算法,以及针对用户态开发,提供的一套内存系统调用接口等等,这部分官方没有提供任何文档,代码注释也很少,全靠硬摸。


先说三种虚拟空间 空间(space)这个概念很重要,还记得进程描述符(LosProcessCB)里的LosVmSpace  *vmSpace吗?它是进程使用内存的方式,空间就是边界,进程只能在划定的空间里运行,任何指令都不能越界运行。

在鸿蒙内核源码分析(内存分配篇)中已讲明虚拟内存是MMU带出来的概念,为解决物理内存满足不了多进程对内存的需要。虚拟内存可以远大于物理内存。虚拟空间是进程层面的概念,每个进程都有一个,给进程独享整个物理内存的假象。对鸿蒙来说操作系统和驱动程序运行在内核空间(kernel space),应用程序运行在用户空间(user space), 在运行期间需动态分配的向堆空间(heap space)申请内存。具体看代码会更清晰些。 从空间的初始化调用关系上可以看出只有这三种空间,所不同的是 内核虚拟空间,堆虚拟空间只有一个,而每一个用户进程都有属于自己的用户虚拟空间。看看他们初始化代码:

  • //内核虚拟空间初始化
      
  • 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);
      
  • }
      
  • //动态分配空间初始化
      
  • 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);
      
  • }
      
  • //用户虚拟空间初始化
      
  • 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);
      
  • }

复制代码

它们唯一的区别是虚拟地址的开始位置和大小不一样,但是所有用户进程的虚拟地址都是一样的,注意用户进程是一样的,细品。

  • STATIC BOOL OsVmSpaceInitCommon(LosVmSpace *vmSpace, VADDR_T *virtTtb)
      
  • {
      
  •     LOS_RbInitTree(&vmSpace->regionRbTree, OsRegionRbCmpKeyFn, OsRegionRbFreeFn, OsRegionRbGetKeyFn);//初始化虚拟存储区域-以红黑树组织方式
      

  •   
  •     LOS_ListInit(&vmSpace->regions);//初始化虚拟存储区域-以双循环链表组织方式
      
  •     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);//对空间mmu初始化
      
  • }
      

  •   
  • //通过虚拟地址获取所属空间地址
      
  • LosVmSpace *LOS_SpaceGet(VADDR_T vaddr)
      
  • {
      
  •     if (LOS_IsKernelAddress(vaddr)) {
      
  •         return LOS_GetKVmSpace();
      
  •     } else if (LOS_IsUserAddress(vaddr)) {
      
  •         return OsCurrProcessGet()->vmSpace;//当前进程的虚拟空间
      
  •     } else if (LOS_IsVmallocAddress(vaddr)) {
      
  •         return LOS_GetVmallocSpace();
      
  •     } else {
      
  •         return NULL;
      
  •     }
      
  • }
      


复制代码

这些空间都挂在 g_vmSpaceList 双循环链表上,LOS_SpaceGet可以通过虚拟地址反查是属于哪种空间。每一个空间都有一张页表和物理内存页表形成映射关系,虚拟内存和物理内存都是页对页的映射,两边每页都是4K,也必须是一样的!否则无法完成映射。具体如何映射的将在鸿蒙内核源码分析(内存映射篇)中说明,在调度算法切换进程时就需要切换至该进程自己的虚拟空间,即MMU上下文。
物理内存初始化 物理内存部分见代码: los_vm_phys.c,到了物理内存就没有什么进程,空间的概念了,只有页的概念!一页4K 物理内存的管理和分配都是围绕着页展开的,鸿蒙对物理内存使用了段页式管理,看代码吧,关键处都加了注释。

  • /* Physical memory area array */
      
  • STATIC struct VmPhysArea g_physArea[] = {
      
  •     {
      
  •         .start = SYS_MEM_BASE, //整个物理内存基地址
      
  •         .size = SYS_MEM_SIZE_DEFAULT,//整个物理内存总大小
      
  •     },
      
  • };
      
  • //* page初始化
      
  • VOID OsVmPageStartup(VOID)
      
  • {
      
  •     struct VmPhysSeg *seg = NULL;
      
  •     LosVmPage *page = NULL;
      
  •     paddr_t pa;
      
  •     UINT32 nPage;
      
  •     INT32 segID;
      

  •   
  •     OsVmPhysAreaSizeAdjust(ROUNDUP((g_vmBootMemBase - KERNEL_ASPACE_BASE), PAGE_SIZE));//校正 g_physArea size
      

  •   
  •     nPage = OsVmPhysPageNumGet();//得到 g_physArea 总页数
      
  •     g_vmPageArraySize = nPage * sizeof(LosVmPage);//页表总大小
      
  •     g_vmPageArray = (LosVmPage *)OsVmBootMemAlloc(g_vmPageArraySize);//申请页表存放区域
      

  •   
  •     OsVmPhysAreaSizeAdjust(ROUNDUP(g_vmPageArraySize, PAGE_SIZE));// g_physArea 变小
      

  •   
  •     OsVmPhysSegAdd();// 段页绑定
      
  •     OsVmPhysInit();// 加入空闲链表和设置置换算法,LRU(最近最久未使用)算法
      

  •   
  •     for (segID = 0; segID < g_vmPhysSegNum; segID++) {
      
  •         seg = &g_vmPhysSeg[segID];
      
  •         nPage = seg->size >> PAGE_SHIFT;
      
  •         for (page = seg->pageBase, pa = seg->start; page <= seg->pageBase + nPage;
      
  •              page++, pa += PAGE_SIZE) {
      
  •             OsVmPageInit(page, pa, segID);//page初始化
      
  •         }
      
  •         OsVmPageOrderListInit(seg->pageBase, nPage);// 页面分配的排序
      
  •     }
      
  • }
      
  • UINT32 OsVmPhysPageNumGet(VOID)
      
  • {
      
  •     UINT32 nPages = 0;
      
  •     INT32 i;
      

  •   
  •     for (i = 0; i < (sizeof(g_physArea) / sizeof(g_physArea[0])); i++) {
      
  •         nPages += g_physArea.size >> PAGE_SHIFT;//右移12位,相当于除以4K,得出总页数
      
  •     }
      

  •   
  •     return nPages;
      
  • }
      
  • VOID OsVmPhysSegAdd(VOID)
      
  • {
      
  •     INT32 i, ret;
      

  •   
  •     LOS_ASSERT(g_vmPhysSegNum <= VM_PHYS_SEG_MAX);
      
  •         
      
  •     for (i = 0; i < (sizeof(g_physArea) / sizeof(g_physArea[0])); i++) {
      
  •         ret = OsVmPhysSegCreate(g_physArea.start, g_physArea.size);//一个区对应一个段
      
  •         if (ret != 0) {
      
  •             VM_ERR(“create phys seg failed“);
      
  •         }
      
  •     }
      
  • }
      

  •   
  • STATIC VOID OsVmPageInit(LosVmPage *page, paddr_t pa, UINT8 segID)
      
  • {
      
  •     LOS_ListInit(&page->node);//初始化链表节点
      
  •     page->flags = FILE_PAGE_FREE;//默认空闲
      
  •     LOS_AtomicSet(&page->refCounts, 0);//0次引用
      
  •     page->physAddr = pa;//物理地址
      
  •     page->segID = segID;//所属段
      
  •     page->order = VM_LIST_ORDER_MAX;//伙伴算法默认级数
      
  • }
      


复制代码

代码中可以看出初始化对物理内存做了几个动作: 1.对整个物理内存进行了分页,每页框4K,存放在大页表数组中 g_vmPageArray 2.段页绑定,根据g_physArea数组的大小来创建段,因数组里只有一条数据,所以只有一个段 3.初始化了回收双链表和置换算法,采用了LRU置换算法。 4.对每一页框进行了初始化,每个页框可用于分配,指定了物理地址,注意这是物理内存的页。 5.对伙伴算法初始化。
什么是伙伴算法? 简单的说就是把所有的空闲页面分为10个块组,每组中块的大小是2的幂次方个页面,例如,第0组中块的大小都为2的0次方 (1个页面),第1组中块的大小为都为2的1次方(2个页面),第9组中块的大小都为2的9次方(512个页面)。也就是说,每一组中块的大小是相同的,且这同样大小的块形成一个链表,能看懂下面这张图的就看懂了伙伴算法,一个方块代表一个物理页框。
物理内存是怎么被分配的? 物理内存是以页为单位被分配的,详细看 LOS_PhysPagesAllocContiguous,看下哪些地方调用了它。 1.初始化进程块会用到  2.扩展内存池会用到,3,用户进程空间初始化会用到 4,内核动态分配的时候会到,这个上面已经讲过了。5.动态加载可执行程序会用到,LOS_PhysPagesAllocContiguous 调用的主要函数是:

[quote]
  

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

  复制代码

[/quote] 总结下:看到这里大家脑子里应该浮现出一幅图,内核抽象出无数个虚拟空间页表,但实际只有一个物理内存页表,每个虚拟空间都要映射到了物理内存页表上,他们是 1:N的关系,如何保证不会错呢?缺页了怎么处理?如何置换页面?怎么才能保证效率?请查看《鸿蒙内核源码分析:虚拟地址与物理地址之间是如何映射的》



本文来源:图解鸿蒙源码逐行注释分析
回复

使用道具 举报

发表回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则


联系客服 关注微信 下载APP 返回顶部 返回列表