大牛看鸿蒙源码:从进程/线程视角看内存

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

两个原因: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!\n");
        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架构资料、笔记、源码

 

posted @ 2020-09-26 15:19  架构分享  阅读(421)  评论(0编辑  收藏  举报