热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

鸿蒙内核源码分析(内存管理篇)|鸿蒙虚拟内存全景图是怎样的|给HarmonyOS源码逐行加上中文注释|v10.03

鸿,蒙,内核,源,码,分析,内存,管理,篇,鸿,蒙,虚拟内存,全景,图,是,怎样,的,给,harmonyos,源,码,逐行,加上,中文

鸿蒙内核源码注释中文版 【 Gitee仓|CSDN仓|Github仓|Coding仓 】给 HarmonyOS 源码逐行加上中文注解,详细阐述设计细节, 助你快速精读 HarmonyOS 内核源码, 掌握整个鸿蒙内核运行机制,四大码仓和wiki每日同步更新.

鸿蒙源码分析系列篇 【 CSDN| OSCHINA| WIKI 】从 HarmonyOS 架构层视角整理成文, 并首创用生活场景讲故事的方式试图去解构内核,一窥究竟。


本文分析虚拟内存模块源码 详见:../kernel/base/vm 本篇源码超级多,很烧脑,但笔者关键处都加了注释。废话不多说,开始吧。

目录

初始化整个内存

鸿蒙虚拟内存全景图

内核空间是怎么初始化的?

Page是如何初始化的?

进程是如何申请内存的?

task是如何申请内存的?

初始化整个内存

从main()跟踪可看内存部分初始化是在OsSysMemInit()中完成的。

UINT32 OsSysMemInit(VOID) {     STATUS_T ret;     OsKSpaceInit();//内核空间初始化     ret = OsKHeapInit(OS_KHEAP_BLOCK_SIZE);// 内核动态内存初始化 512K        if (ret != LOS_OK) {         VM_ERR("OsKHeapInit fail");         return LOS_NOK;     }     OsVmPageStartup();// page初始化     OsInitMappingStartUp();// 映射初始化     ret = ShmInit();// 共享内存初始化     if (ret <0) {         VM_ERR("ShmInit fail");           return LOS_NOK;     }     return LOS_OK; } 

鸿蒙虚拟内存整体布局图

// HarmonyOS 内核空间包含以下各段: extern CHAR __int_stack_start; // 运行系统函数栈的开始地址 extern CHAR __rodata_start; // ROM开始地址 只读 extern CHAR __rodata_end; // ROM结束地址 extern CHAR __bss_start; // bss开始地址 extern CHAR __bss_end; // bss结束地址 extern CHAR __text_start; // 代码区开始地址 extern CHAR __text_end; // 代码区结束地址 extern CHAR __ram_data_start; // RAM开始地址 可读可写 extern CHAR __ram_data_end; // RAM结束地址 extern UINT32 __heap_start; // 堆区开始地址 extern UINT32 __heap_end; // 堆区结束地址 

内存一开始一张白纸,这些extern就是给它画大界线的,从哪到哪是属于什么段。这些值大小取决实际项目内存条的大小,不同的内存条,地址肯定会不一样,所以必须由外部提供,鸿蒙内核采用了Linux的段管理方式。结合上图对比以下的解释自行理解下位置。 

 BSS段 (bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。该段用于存储未初始化的全局变量或者是默认初始化为0的全局变量,它不占用程序文件的大小,但是占用程序运行时的内存空间。

data段 该段用于存储初始化的全局变量,初始化为0的全局变量出于编译优化的策略还是被保存在BSS段。

细心的读者可能发现了,鸿蒙内核几乎所有的全局变量都没有赋初始化值或NULL,这些变量经过编译后是放在了BSS段的,运行时占用内存空间,如此编译出来的ELF包就变小了。

.rodata段,该段也叫常量区,用于存放常量数据,ro就是Read Only之意。

text段 是用于存放程序代码的,编译时确定,只读。更进一步讲是存放处理器的机器指令,当各个源文件单独编译之后生成目标文件,经连接器链接各个目标文件并解决各个源文件之间函数的引用,与此同时,还得将所有目标文件中的.text段合在一起。

stack栈段,是由系统负责申请释放,用于存储参数变量及局部变量以及函数的执行。

heap段 它由用户申请和释放,申请时至少分配虚存,当真正存储数据时才分配相应的实存,释放时也并非立即释放实存,而是可能被重复利用。

内核空间是怎么初始化的?

LosMux g_vmSpaceListMux;//虚拟空间互斥锁,一般和g_vmSpaceList配套使用 LOS_DL_LIST_HEAD(g_vmSpaceList);//g_vmSpaceList把所有虚拟空间挂在一起, LosVmSpace g_kVmSpace;    //内核空间地址 LosVmSpace g_vMallocSpace;//虚拟分配空间地址 //鸿蒙内核空间有两个(内核进程空间和内核动态分配空间),共用一张L1页表 VOID OsKSpaceInit(VOID) {     OsVmMapInit();// 初始化互斥量     OsKernVmSpaceInit(&g_kVmSpace, OsGFirstTableGet());// 初始化内核虚拟空间,OsGFirstTableGet 为L1表基地址     OsVMallocSpaceInit(&g_vMallocSpace, OsGFirstTableGet());// 初始化动态分配区虚拟空间,OsGFirstTableGet 为L1表基地址 }//g_kVmSpace g_vMallocSpace 共用一个L1页表 //初始化内核堆空间 STATUS_T OsKHeapInit(size_t size) {     STATUS_T ret;     VOID *ptr = NULL;     /*      * roundup to MB aligned in order to set kernel attributes. kernel text/code/data attributes      * should page mapping, remaining region should section mapping. so the boundary should be      * MB aligned.      */      //向上舍入到MB对齐是为了设置内核属性。内核文本/代码/数据属性应该是页映射,其余区域应该是段映射,所以边界应该对齐。     UINTPTR end = ROUNDUP(g_vmBootMemBase + size, MB);//用M是因为采用section mapping 鸿蒙内核源码分析(内存映射篇)有阐述     size = end - g_vmBootMemBase;     //ROUNDUP(0x00000200+512,1024) = 1024  ROUNDUP(0x00000201+512,1024) = 2048 此处需细品!      ptr = OsVmBootMemAlloc(size);//因刚开机,使用引导分配器分配     if (!ptr) {         PRINT_ERR("vmm_kheap_init boot_alloc_mem failed! %d\n", size);         return -1;     }     m_aucSysMem0 = m_aucSysMem1 = ptr;//内存池基地址,取名auc还用0和1来标识有何深意,一直没整明白, 哪位大神能告诉下?     ret = LOS_MemInit(m_aucSysMem0, size);//初始化内存池     if (ret != LOS_OK) {         PRINT_ERR("vmm_kheap_init LOS_MemInit failed!\n");         g_vmBootMemBase -= size;//分配失败时需归还size, g_vmBootMemBase是很野蛮粗暴的         return ret;     }     LOS_MemExpandEnable(OS_SYS_MEM_ADDR);//地址可扩展     return LOS_OK; } 

内核空间用了三个全局变量,其中一个是互斥LosMux,IPC部分会详细讲,这里先不展开。 比较有意思的是LOS_DL_LIST_HEAD,看内核源码过程中经常会为这样的代码点头称赞,会心一笑。点赞!

#define LOS_DL_LIST_HEAD(list) LOS_DL_LIST list = { &(list), &(list) } 

Page是如何初始化的?

page是映射的最小单位,是物理地址<--->虚拟地址映射的数据结构的基础

// 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 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);// 页面分配的排序     } } 

进程是如何申请内存的?

进程的主体是来自进程池,进程池是统一分配的,怎么创建进程池的去翻系列篇里的文章,所以创建一个进程的时候只需要分配虚拟内存LosVmSpace,这里要分内核模式和用户模式下的申请。

//初始化进程的 用户空间 或 内核空间 //初始化PCB块 STATIC UINT32 OsInitPCB(LosProcessCB *processCB, UINT32 mode, UINT16 priority, UINT16 policy, const CHAR *name) {     UINT32 count;     LosVmSpace *space = NULL;     LosVmPage *vmPage = NULL;     status_t status;     BOOL retVal = FALSE;     processCB->processMode = mode;//用户态进程还是内核态进程     processCB->processStatus = OS_PROCESS_STATUS_INIT;//进程初始状态     processCB->parentProcessID = OS_INVALID_VALUE;//爸爸进程,外面指定     processCB->threadGroupID = OS_INVALID_VALUE;//所属线程组     processCB->priority = priority;//优先级     processCB->policy = policy;//调度算法 LOS_SCHED_RR     processCB->umask = OS_PROCESS_DEFAULT_UMASK;//掩码     processCB->timerID = (timer_t)(UINTPTR)MAX_INVALID_TIMER_VID;     LOS_ListInit(&processCB->threadSiblingList);//初始化任务/线程链表     LOS_ListInit(&processCB->childrenList);        //初始化孩子链表     LOS_ListInit(&processCB->exitChildList);    //初始化记录哪些孩子退出了的链表         LOS_ListInit(&(processCB->waitList));        //初始化等待链表     for (count = 0; count threadPriQueueList[count]);       }     if (OsProcessIsUserMode(processCB)) {// 是否为用户态进程         space = LOS_MemAlloc(m_aucSysMem0, sizeof(LosVmSpace));         if (space == NULL) {             PRINT_ERR("%s %d, alloc space failed\n", __FUNCTION__, __LINE__);             return LOS_ENOMEM;         }         VADDR_T *ttb = LOS_PhysPagesAllocContiguous(1);//分配一个物理页用于存储L1页表 4G虚拟内存分成 (4096*1M)         if (ttb == NULL) {//这里直接获取物理页ttb             PRINT_ERR("%s %d, alloc ttb or space failed\n", __FUNCTION__, __LINE__);             (VOID)LOS_MemFree(m_aucSysMem0, space);             return LOS_ENOMEM;         }         (VOID)memset_s(ttb, PAGE_SIZE, 0, PAGE_SIZE);         retVal = OsUserVmSpaceInit(space, ttb);//初始化虚拟空间和本进程 mmu         vmPage = OsVmVaddrToPage(ttb);//通过虚拟地址拿到page         if ((retVal == FALSE) || (vmPage == NULL)) {//异常处理             PRINT_ERR("create space failed! ret: %d, vmPage: %#x\n", retVal, vmPage);             processCB->processStatus = OS_PROCESS_FLAG_UNUSED;//进程未使用,干净             (VOID)LOS_MemFree(m_aucSysMem0, space);//释放虚拟空间             LOS_PhysPagesFreeContiguous(ttb, 1);//释放物理页,4K             return LOS_EAGAIN;         }         processCB->vmSpace = space;//设为进程虚拟空间         LOS_ListAdd(&processCB->vmSpace->archMmu.ptList, &(vmPage->node));//将空间映射页表挂在 空间的mmu L1页表, L1为表头     } else {         processCB->vmSpace = LOS_GetKVmSpace();//内核共用一个虚拟空间,内核进程 常驻内存     } #ifdef LOSCFG_SECURITY_VID     status = VidMapListInit(processCB);     if (status != LOS_OK) {         PRINT_ERR("VidMapListInit failed!\n");         return LOS_ENOMEM;     } #endif #ifdef LOSCFG_SECURITY_CAPABILITY     OsInitCapability(processCB); #endif     if (OsSetProcessName(processCB, name) != LOS_OK) {         return LOS_ENOMEM;     }     return LOS_OK; } LosVmSpace *LOS_GetKVmSpace(VOID) { return &g_kVmSpace; } 

从代码可以看出,内核空间固定只有一个g_kVmSpace,而每个用户进程的虚拟内存空间都是独立的。请细品! 

task是如何申请内存的?

task的主体是来自进程池,task池是统一分配的,怎么创建task池的去翻系列篇里的文章。这里task只需要申请stack空间,还是直接上看源码吧,用OsUserInitProcess函数看应用程序的main() 是如何被内核创建任务和运行的。

//所有的用户进程都是使用同一个用户代码段描述符和用户数据段描述符,它们是__USER_CS和__USER_DS,也就是每个进程处于用户态时,它们的CS寄存器和DS寄存器中的值是相同的。当任何进程或者中断异常进入内核后,都是使用相同的内核代码段描述符和内核数据段描述符,它们是__KERNEL_CS和__KERNEL_DS。这里要明确记得,内核数据段实际上就是内核态堆栈段。 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;//代码区开始位置 ,所有进程     CHAR *userInitBssStart = (CHAR *)&__user_init_bss;// 未初始化数据区(BSS)。在运行时改变其值     CHAR *userInitEnd = (CHAR *)&__user_init_end;// 结束地址     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);// 初始化用户进程,它将是所有应用程序的父进程     if (ret != LOS_OK) {         return ret;     }     userText = LOS_PhysPagesAllocContiguous(initSize >> PAGE_SHIFT);// 分配连续的物理页     if (userText == NULL) {         ret = LOS_NOK;         goto ERROR;     }     (VOID)memcpy_s(userText, initSize, (VOID *)&__user_init_load_addr, initSize);// 安全copy 经加载器load的结果 __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);// 虚拟地址与物理地址的映射     if (ret <0) {         goto ERROR;     }     (VOID)memset_s((VOID *)((UINTPTR)userText + userInitBssStart - userInitTextStart), initBssSize, 0, initBssSize);// 除了代码段,其余都清0     stack = OsUserInitStackAlloc(g_userInitProcess, &size);// 初始化堆栈区     if (stack == NULL) {         PRINTK("user init process malloc user stack failed!\n");         ret = LOS_NOK;         goto ERROR;     }     param.pfnTaskEntry = (TSK_ENTRY_FUNC)userInitTextStart;// 从代码区开始执行,也就是应用程序main 函数的位置     param.userParam.userSP = (UINTPTR)stack + size;// 指向栈底     param.userParam.userMapBase = (UINTPTR)stack;// 栈顶     param.userParam.userMapSize = size;// 栈大小     param.uwResved = OS_TASK_FLAG_PTHREAD_JOIN;// 可结合的(joinable)能够被其他线程收回其资源和杀死     ret = OsUserInitProcessStart(g_userInitProcess, ¶m);// 创建一个任务,来运行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);//删除PCB块     return ret; } 

所有的用户进程都是通过init进程 fork来的, 可以看到创建进程的同时创建了一个task, 入口函数就是代码区的第一条指令,也就是应用程序 main函数。这里再说下stack的大小,不同空间下的task栈空间是不一样的,鸿蒙内核中有三种栈空间size,如下

#define LOSCFG_BASE_CORE_TSK_IDLE_STACK_SIZE SIZE(0x800)//内核进程,运行在内核空间2K #define OS_USER_TASK_SYSCALL_SATCK_SIZE 0x3000 //用户进程,通过系统调用创建的task运行在内核空间的 12K #define OS_USER_TASK_STACK_SIZE 0x100000//用户进程运行在用户空间的1M 


系列篇文章 进入 >>鸿蒙系统源码分析(总目录) 【 CSDN|OSCHINA| WIKI 】查看

注释中文版 进入 >>鸿蒙内核源码注释中文版 【 Gitee仓|CSDN仓|Github仓|Coding仓 】阅读

内容仅代表个人观点,会反复修正,出精品注解,写精品文章,一律原创,转载需注明出处,不修改内容,不乱插广告,错漏之处欢迎指正笔者第一时间加以完善


推荐阅读
  • PHP预处理常量详解:如何定义与使用常量 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 【问题】在Android开发中,当为EditText添加TextWatcher并实现onTextChanged方法时,会遇到一个问题:即使只对EditText进行一次修改(例如使用删除键删除一个字符),该方法也会被频繁触发。这不仅影响性能,还可能导致逻辑错误。本文将探讨这一问题的原因,并提供有效的解决方案,包括使用Handler或计时器来限制方法的调用频率,以及通过自定义TextWatcher来优化事件处理,从而提高应用的稳定性和用户体验。 ... [详细]
  • 深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案
    深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案 ... [详细]
  • 本文详细解析了使用C++实现的键盘输入记录程序的源代码,该程序在Windows应用程序开发中具有很高的实用价值。键盘记录功能不仅在远程控制软件中广泛应用,还为开发者提供了强大的调试和监控工具。通过具体实例,本文深入探讨了C++键盘记录程序的设计与实现,适合需要相关技术的开发者参考。 ... [详细]
  • 在对WordPress Duplicator插件0.4.4版本的安全评估中,发现其存在跨站脚本(XSS)攻击漏洞。此漏洞可能被利用进行恶意操作,建议用户及时更新至最新版本以确保系统安全。测试方法仅限于安全研究和教学目的,使用时需自行承担风险。漏洞编号:HTB23162。 ... [详细]
  • 本文全面解析了 Python 中字符串处理的常用操作与技巧。首先介绍了如何通过 `s.strip()`, `s.lstrip()` 和 `s.rstrip()` 方法去除字符串中的空格和特殊符号。接着,详细讲解了字符串复制的方法,包括使用 `sStr1 = sStr2` 进行简单的赋值复制。此外,还探讨了字符串连接、分割、替换等高级操作,并提供了丰富的示例代码,帮助读者深入理解和掌握这些实用技巧。 ... [详细]
  • 为了确保iOS应用能够安全地访问网站数据,本文介绍了如何在Nginx服务器上轻松配置CertBot以实现SSL证书的自动化管理。通过这一过程,可以确保应用始终使用HTTPS协议,从而提升数据传输的安全性和可靠性。文章详细阐述了配置步骤和常见问题的解决方法,帮助读者快速上手并成功部署SSL证书。 ... [详细]
  • 在《ChartData类详解》一文中,我们将深入探讨 MPAndroidChart 中的 ChartData 类。本文将详细介绍如何设置图表颜色(Setting Colors)以及如何格式化数据值(Formatting Data Values),通过 ValueFormatter 的使用来提升图表的可读性和美观度。此外,我们还将介绍一些高级配置选项,帮助开发者更好地定制和优化图表展示效果。 ... [详细]
  • 深入解析Android 4.4中的Fence机制及其应用
    在Android 4.4中,Fence机制是处理缓冲区交换和同步问题的关键技术。该机制广泛应用于生产者-消费者模式中,确保了不同组件之间高效、安全的数据传输。通过深入解析Fence机制的工作原理和应用场景,本文探讨了其在系统性能优化和资源管理中的重要作用。 ... [详细]
  • 在Cisco IOS XR系统中,存在提供服务的服务器和使用这些服务的客户端。本文深入探讨了进程与线程状态转换机制,分析了其在系统性能优化中的关键作用,并提出了改进措施,以提高系统的响应速度和资源利用率。通过详细研究状态转换的各个环节,本文为开发人员和系统管理员提供了实用的指导,旨在提升整体系统效率和稳定性。 ... [详细]
  • 本文深入解析了WCF Binding模型中的绑定元素,详细介绍了信道、信道管理器、信道监听器和信道工厂的概念与作用。从对象创建的角度来看,信道管理器负责信道的生成。具体而言,客户端的信道通过信道工厂进行实例化,而服务端则通过信道监听器来接收请求。文章还探讨了这些组件之间的交互机制及其在WCF通信中的重要性。 ... [详细]
  • 使用 ListView 浏览安卓系统中的回收站文件 ... [详细]
  • Python 伦理黑客技术:深入探讨后门攻击(第三部分)
    在《Python 伦理黑客技术:深入探讨后门攻击(第三部分)》中,作者详细分析了后门攻击中的Socket问题。由于TCP协议基于流,难以确定消息批次的结束点,这给后门攻击的实现带来了挑战。为了解决这一问题,文章提出了一系列有效的技术方案,包括使用特定的分隔符和长度前缀,以确保数据包的准确传输和解析。这些方法不仅提高了攻击的隐蔽性和可靠性,还为安全研究人员提供了宝贵的参考。 ... [详细]
  • V8不仅是一款著名的八缸发动机,广泛应用于道奇Charger、宾利Continental GT和BossHoss摩托车中。自2008年以来,作为Chromium项目的一部分,V8 JavaScript引擎在性能优化和技术创新方面取得了显著进展。该引擎通过先进的编译技术和高效的垃圾回收机制,显著提升了JavaScript的执行效率,为现代Web应用提供了强大的支持。持续的优化和创新使得V8在处理复杂计算和大规模数据时表现更加出色,成为众多开发者和企业的首选。 ... [详细]
author-avatar
mobiledu2502853463
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有