zoukankan      html  css  js  c++  java
  • 大牛看鸿蒙源码:从进程/线程视角看内存

    本章开始说内存,内存的管理是极其复杂的模块,涉及到非常多概念,光地址就有逻辑,线性,物理地址三个,网上文章很多,参差不齐,没有很好基础或实战经验的同学基本得懵掉,本篇最后也有这些概念介绍。系列篇打算用三篇来讲述鸿蒙内核的内存管理机制。由浅入深,层层递进。我们换个视角切入,将从进程和线程创建的视角看内存的运作机制。为何从进程和线程角度?

    两个原因:1.内存就是给他们使用的,只是分了内核空间和用户空间。用户空间的进程分配用到了虚拟内存,线程(task)需要分配栈空间 2.系列文章对进程和线程的管理和调度已经说完了,但是没有说内存,还有IPC(也是复杂的模块),有了前面的基础我们再来说鸿蒙的内存管理会轻松些。

    进程内存描述符LosVmSpace

    typedef struct ProcessCB {
        //..只留下相关部分
        LosVmSpace          *vmSpace;       /**< VMM space for processes */
    }LosProcessCB;
    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;

    被进程使用的内存叫进程内存描述符LosVmSpace(也叫虚拟内存空间),虚拟内存空间有多个虚拟存储区域(region),Linux内核中对这些虚拟存储区域的组织方式有两种,一种是采用双循环链表(regions),还有一种是采用树的结构。Linux内核从2.4.10开始,Linux内核对虚拟区的组织不再采用一般平衡二叉树,而是采用红黑树(regionRbTree),这是出于效率的考虑,就是增删改查更快了。node会加入到全局的g_vmSpaceList链表中,曾有人私信笔者LOS_DL_LIST里面只有两个指针数据去哪了?答案是:谁用它谁就是数据。 链表把所有进程拉进大循环,还记得鸿蒙内核进程池的大小吗?默认64个,另外就是堆栈空间等信息。这里大概说这么多,后续还会拆开细讲。

    从用户态的第一个进程初始化说起

    所有应用程序的爸爸都是"init"这个用户进程fork来的,看代码他是如何初始化的,这个函数之前也讲过,但没有说内存部分,这次专讲内存部分,涉及代码都已经加了注释。用户进程就是只能运行在用户空间,内核空间是通过系统调用访问的。

    LITE_OS_SEC_TEXT_INIT UINT32 OsUserInitProcess(VOID)
    {
        INT32 ret;
        UINT32 size;
        TSK_INIT_PARAM_S param = { 0 };
        VOID *stack = NULL;
        VOID *userText = NULL;
        CHAR *userInitTextStart = (CHAR *)&__user_init_entry;//*kfy 代码区开始位置
        CHAR *userInitBssStart = (CHAR *)&__user_init_bss;//*kyf 未初始化数据区(BSS)。在运行时改变其值
        CHAR *userInitEnd = (CHAR *)&__user_init_end;//*kyf 结束地址
        UINT32 initBssSize = userInitEnd - userInitBssStart;
        UINT32 initSize = userInitEnd - userInitTextStart;
    
        LosProcessCB *processCB = OS_PCB_FROM_PID(g_userInitProcess);
        ret = OsProcessCreateInit(processCB, OS_USER_MODE, "Init", OS_PROCESS_USERINIT_PRIORITY);//*kyf 初始化用户进程,它将是所有应用程序的父进程
        if (ret != LOS_OK) {
            return ret;
        }
    
        userText = LOS_PhysPagesAllocContiguous(initSize >> PAGE_SHIFT);//*kyf 分配连续的物理页
        if (userText == NULL) {
            ret = LOS_NOK;
            goto ERROR;
        }
    
        (VOID)memcpy_s(userText, initSize, (VOID *)&__user_init_load_addr, initSize);//*kyf 安全copy __user_init_load_addr -> userText
        ret = LOS_VaddrToPaddrMmap(processCB->vmSpace, (VADDR_T)(UINTPTR)userInitTextStart, LOS_PaddrQuery(userText),
                                   initSize, VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE |
                                   VM_MAP_REGION_FLAG_PERM_EXECUTE | VM_MAP_REGION_FLAG_PERM_USER);//*kyf 虚拟地址与物理地址的映射
        if (ret < 0) {
            goto ERROR;
        }
    
        (VOID)memset_s((VOID *)((UINTPTR)userText + userInitBssStart - userInitTextStart), initBssSize, 0, initBssSize);//*kyf 除了代码段,其余都清0
    
        stack = OsUserInitStackAlloc(g_userInitProcess, &size);//*kyf 初始化堆栈区
        if (stack == NULL) {
            PRINTK("user init process malloc user stack failed!
    ");
            ret = LOS_NOK;
            goto ERROR;
        }
    
        param.pfnTaskEntry = (TSK_ENTRY_FUNC)userInitTextStart;//*kyf 从代码区开始执行,也就是应用程序main 函数的位置
        param.userParam.userSP = (UINTPTR)stack + size;//*kyf 指向栈顶
        param.userParam.userMapBase = (UINTPTR)stack;//*kyf 栈底
        param.userParam.userMapSize = size;//*kyf 栈大小
        param.uwResved = OS_TASK_FLAG_PTHREAD_JOIN;//*kyf 可结合的(joinable)能够被其他线程收回其资源和杀死
        ret = OsUserInitProcessStart(g_userInitProcess, &param);//*kyf 创建一个任务,来运行main函数
        if (ret != LOS_OK) {
            (VOID)OsUnMMap(processCB->vmSpace, param.userParam.userMapBase, param.userParam.userMapSize);
            goto ERROR;
        }
    
        return LOS_OK;
    
    ERROR:
        (VOID)LOS_PhysPagesFreeContiguous(userText, initSize >> PAGE_SHIFT);
        OsDeInitPCB(processCB);
        return ret;
    }

    鸿蒙现有开源终端设备支持的128KB-128MB

    内存

    ,这三个值需要外部提供,先指定空间大小,内核才能还是管理。

        CHAR *userInitTextStart = (CHAR *)&__user_init_entry;//*kfy 代码区开始位置
        CHAR *userInitBssStart = (CHAR *)&__user_init_bss;//*kyf 未初始化数据区(BSS)。在运行时改变其值
        CHAR *userInitEnd = (CHAR *)&__user_init_end;//*kyf 结束地址

    代码区:应用程序源码码经过编译后,通过加载器加载到这里,第一条指令就是 main()

    BSS:英文

    全称

    叫Block Started by Symbol ,未初始化的全局变量和静态变量的内存空间,并且清0

    LOS_PhysPagesAllocContiguous: 从物理页分配连续的地址空间。

    LOS_VaddrToPaddrMmap:将虚拟内存映射成物理地址。 这两个函数先不展开,放在下篇说,需要了解物理地址页管理部分。

    同时通过g_userInitProcess创建了进程的第一个task,回调函数指向了代码区userInitTextStart,也就是像java等上层应用开发的main函数所在位置,等该任务被调度后,CPU的PC寄存器值将会被改成userInitTextStart,程序从这开始执行。明白了吗?

    再来,main任务的栈内存是怎么来的?

    STATIC VOID *OsUserInitStackAlloc(UINT32 processID, UINT32 *size)
    {
        LosVmMapRegion *region = NULL;
        LosProcessCB *processCB = OS_PCB_FROM_PID(processID);
        UINT32 stackSize = ALIGN(OS_USER_TASK_STACK_SIZE, PAGE_SIZE);//*kyf
    
        region = LOS_RegionAlloc(processCB->vmSpace, 0, stackSize,
                                 VM_MAP_REGION_FLAG_PERM_USER | VM_MAP_REGION_FLAG_PERM_READ |
                                 VM_MAP_REGION_FLAG_PERM_WRITE, 0);
        if (region == NULL) {
            return NULL;
        }
    
        LOS_SetRegionTypeAnon(region);
        region->regionFlags |= VM_MAP_REGION_FLAG_STACK;
    
        *size = stackSize;
    
        return (VOID *)(UINTPTR)region->range.base;
    }
    
    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;
    };

    VmMapRegion(线性区描述符),该结构体描述了 protectFlags(权限),LosVmMapRange(范围),线性区的类型(regionType)。映射类型(unTypeData):按文件映射,匿名映射,特殊设备映射,这是个联合体,具体下篇再展开讲。

    一些概念

    一、逻辑地址(有时也称虚拟地址)
      逻辑地址(Logical Address) 是指由程序产生的与段相关的偏移地址部分。例如在C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于当前进程数据段的地址,和绝对物理地址无关。
      只有在Intel处理器的实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,CPU不进行自动地址转换)。逻辑地址也就是在Intel 处理器的保护模式下,程序执行代码段限长内的偏移地址(假定代码段、数据段完全一样)。
      CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。
      应用程序仅需与逻辑地址打交道,而分段和分页机制仅系统编程涉及,应用程序虽然可以直接操作内存,但是也只能在操作系统分配的内存段中操作。

    二、线性地址
      线性地址(Linear Address)是逻辑地址到物理地址转换的中间层。程序代码经编译后会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
      若启用了分页机制,则线性地址会再此转换产生一个物理地址。若没有启用分页机制,则线性地址就是物理地址。
    三、物理地址
      物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终地址。若启用了分页机制,则线性地址会使用页目录和页表中的项转换为物理地址。若没有启用分页机制,则线性地址直接就是物理地址。
    四、虚拟内存
      虚拟内存(Virtual Memory)是指计算机呈现出比实际拥有的内存大得多的内存量。因此它允许程序员编写并运行比实际系统拥有的内存大得多的程序。这使得许多大型项目也能够在具有有限内存资源的系统上实现。

    转发+关注,私信我就可以获取获取更多java架构资料、笔记、源码

  • 相关阅读:
    CSS3自适应布局单位 —— vw,vh
    JS 设计模式四 -- 模块模式
    JS 设计模式三 -- 策略模式
    JS 设计模式
    JS 设计模式二 -- 单例模式
    JS 设计模式一 -- 原型模式
    JS 灵活使用 console 调试
    JS 优化条件语句的5个技巧
    JS 函数节流与防抖
    前端性能优化
  • 原文地址:https://www.cnblogs.com/ming569/p/13735154.html
Copyright © 2011-2022 走看看