大牛看鸿蒙源码:从进程/线程视角看内存
本章开始说内存,内存的管理是极其复杂的模块,涉及到非常多概念,光地址就有逻辑,线性,物理地址三个,网上文章很多,参差不齐,没有很好基础或实战经验的同学基本得懵掉,本篇最后也有这些概念介绍。系列篇打算用三篇来讲述鸿蒙内核的内存管理机制。由浅入深,层层递进。我们换个视角切入,将从进程和线程创建的视角看内存的运作机制。为何从进程和线程角度?
两个原因: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, ¶m);//*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架构资料、笔记、源码