17 | 划分土地(中):如何实现内存页面初始化?
下载APP
关闭
渠道合作
推荐作者
17 | 划分土地(中):如何实现内存页面初始化?
2021-06-16 LMOS 来自北京
《操作系统实战45讲》
课程介绍
讲述:陈晨
时长14:21大小13.11M
你好,我是 LMOS。
上节课,我们确定了用分页方式管理内存,并且一起动手设计了表示内存页、内存区相关的内存管理数据结构。不过,虽然内存管理相关的数据结构已经定义好了,但是我们还没有在内存中建立对应的实例变量。
我们都知道,在代码中实际操作的数据结构必须在内存中有相应的变量,这节课我们就去建立对应的实例变量,并初始化它们。
初始化
前面的课里,我们在 hal 层初始化中,初始化了从二级引导器中获取的内存布局信息,也就是那个 e820map_t 数组,并把这个数组转换成了 phymmarge_t 结构数组,还对它做了排序。
但是,我们 Cosmos 物理内存管理器剩下的部分还没有完成初始化,下面我们就去实现它。
Cosmos 的物理内存管理器,我们依然要放在 Cosmos 的 hal 层。
因为物理内存还和硬件平台相关,所以我们要在 cosmos/hal/x86/ 目录下建立一个 memmgrinit.c 文件,在这个文件中写入一个 Cosmos 物理内存管理器初始化的大总管——init_memmgr 函数,并在 init_halmm 函数中调用它,代码如下所示。
根据前面我们对内存管理相关数据结构的设计,你应该不难想到,在 init_memmgr 函数中应该要完成内存页结构 msadsc_t 和内存区结构 memarea_t 的初始化,下面就分别搞定这两件事。
内存页结构初始化
内存页结构的初始化,其实就是初始化 msadsc_t 结构对应的变量。因为一个 msadsc_t 结构体变量代表一个物理内存页,而物理内存由多个页组成,所以最终会形成一个 msadsc_t 结构体数组。
这会让我们的工作变得简单,我们只需要找一个内存地址,作为 msadsc_t 结构体数组的开始地址,当然这个内存地址必须是可用的,而且之后内存空间足以存放 msadsc_t 结构体数组。
然后,我们要扫描 phymmarge_t 结构体数组中的信息,只要它的类型是可用内存,就建立一个 msadsc_t 结构体,并把其中的开始地址作为第一个页面地址。
接着,要给这个开始地址加上 0x1000,如此循环,直到其结束地址。
当这个 phymmarge_t 结构体的地址区间,它对应的所有 msadsc_t 结构体都建立完成之后,就开始下一个 phymmarge_t 结构体。依次类推,最后,我们就能建好所有可用物理内存页面对应的 msadsc_t 结构体。
下面,我们去 cosmos/hal/x86/ 目录下建立一个 msadsc.c 文件。在这里写下完成这些功能的代码,如下所示。
上面的代码量很少,逻辑也很简单,再配合注释,相信你看得懂。其中的 ret_msadsc_vadrandsz 函数也是遍历 phymmarge_t 结构数组,计算出有多大的可用内存空间,可以分成多少个页面,需要多少个 msadsc_t 结构。
内存区结构初始化
前面我们将整个物理地址空间在逻辑上分成了三个区,分别是:硬件区、内核区、用户区,这就要求我们要在内存中建立三个 memarea_t 结构体的实例变量。
就像建立 msadsc_t 结构数组一样,我们只需要在内存中找个空闲空间,存放这三个 memarea_t 结构体就行。相比建立 msadsc_t 结构数组这更为简单,因为 memarea_t 结构体是顶层结构,并不依赖其它数据结构,只是对其本身进行初始化就好了。
但是由于它自身包含了其它数据结构,在初始化它时,要对其中的其它数据结构进行初始化,所以要小心一些。
下面我们去 cosmos/hal/x86/ 目录下建立一个 memarea.c 文件,写下完成这些功能的代码,如下所示。
由于这些数据结构很大,所以代码有点长,但是重要的代码我都做了详细注释。
在 init_memarea_core 函数的开始,我们调用了 memarea_t_init 函数,对 MEMAREA_MAX 个 memarea_t 结构进行了基本的初始化。
然后,在 memarea_t_init 函数中又调用了 memdivmer_t_init 函数,而在 memdivmer_t_init 函数中又调用了 bafhlst_t_init 函数,这保证了那些被包含的数据结构得到了初始化。
最后,我们给三个区分别设置了类型和地址空间。
处理初始内存占用问题
我们初始化了内存页和内存区对应的数据结构,已经可以组织好内存页面了。现在看似已经万事俱备了,其实这有个重大的问题,你知道是什么吗?我给你分析一下。
目前我们的内存中已经有很多数据了,有 Cosmos 内核本身的执行文件,有字体文件,有 MMU 页表,有打包的内核映像文件,还有刚刚建立的内存页和内存区的数据结构,这些数据都要占用实际的物理内存。
再回头看看我们建立内存页结构 msadsc_t,所有的都是空闲状态,而它们每一个都表示一个实际的物理内存页。
假如在这种情况下,对调用内存分配接口进行内存分配,它按既定的分配算法查找空闲的 msadsc_t 结构,那它一定会找到内核占用的内存页所对应的 msadsc_t 结构,并把这个内存页分配出去,然后得到这个页面的程序对其进行改写。这样内核数据就会被覆盖,这种情况是我们绝对不能允许的。
所以,我们要把这些已经占用的内存页面所对应的 msadsc_t 结构标记出来,标记成已分配,这样内存分配算法就不会找到它们了。
要解决这个问题,我们只要给出被占用内存的起始地址和结束地址,然后从起始地址开始查找对应的 msadsc_t 结构,再把它标记为已经分配,最后直到查找到结束地址为止。
下面我们在 msadsc.c 文件中来实现这个方案,代码如下。
这三个函数逻辑很简单,由 init_search_krloccupymm 函数入口,search_krloccupymsadsc_core 函数驱动,由 search_segment_occupymsadsc 函数完成实际的工作。
由于初始化阶段各种数据占用的开始、结束地址和大小,这些信息都保存在 machbstart_t 类型的 kmachbsp 变量中,所以函数与 machbstart_t 类型的指针为参数。
其实 phymmarge_t、msadsc_t、memarea_t 这些结构的实例变量和 MMU 页表,它们所占用的内存空间已经涵盖在了内核自身占用的内存空间。
好了,这个问题我们已经完美解决,只要在初始化内存页结构和内存区结构之后调用 init_search_krloccupymm 函数即可。
合并内存页到内存区
我们做了这么多前期工作,依然没有让内存页和内存区联系起来,即让 msadsc_t 结构挂载到内存区对应的数组中。只有这样,我们才能提高内存管理器的分配速度。
让我们来着手干这件事情,这件事情有点复杂,但是我给你梳理以后就会清晰很多。整体上可以分成两步。
1.确定内存页属于哪个区,即标定一系列 msadsc_t 结构是属于哪个 memarea_t 结构的。
2.把特定的内存页合并,然后挂载到特定的内存区下的 memdivmer_t 结构中的 dm_mdmlielst 数组中。
我们先来做第一件事,这件事比较简单,我们只要遍历每个 memarea_t 结构,遍历过程中根据特定的 memarea_t 结构,然后去扫描整个 msadsc_t 结构数组,最后依次对比 msadsc_t 的物理地址,看它是否落在 memarea_t 结构的地址区间中。
如果是,就把这个 memarea_t 结构的类型值写入 msadsc_t 结构中,这样就一个一个打上了标签,遍历 memarea_t 结构结束之后,每个 msadsc_t 结构就只归属于某一个 memarea_t 结构了。
我们在 memarea.c 文件中写几个函数,来实现前面这个步骤,代码如下所示。
我们一下子写了三个函数,它们的作用且听我一一道来。从 init_merlove_mem 函数开始,但是它并不实际干活,作为入口函数,它调用的 merlove_mem_core 函数才是真正干活的。
这个 merlove_mem_core 函数有两个遍历内存区,第一次遍历是为了完成上述第一步:确定内存页属于哪个区。
当确定内存页属于哪个区之后,就来到了第二次遍历 memarea_t 结构,合并其中的 msadsc_t 结构,并把它们挂载到其中的 memdivmer_t 结构下的 dm_mdmlielst 数组中。
这个操作就稍微有点复杂了。第一,它要保证其中所有的 msadsc_t 结构挂载到 dm_mdmlielst 数组中合适的 bafhlst_t 结构中。
第二,它要保证多个 msadsc_t 结构有最大的连续性。
举个例子,比如一个内存区中有 12 个页面,其中 10 个页面是连续的地址为 0~0x9000,还有两个页面其中一个地址为 0xb000,另一个地址为 0xe000。
这样的情况下,需要多个页面保持最大的连续性,还有在 m_mdmlielst 数组中找到合适的 bafhlst_t 结构。
那么:0~0x7000 这 8 个页面就要挂载到 m_mdmlielst 数组中第 3 个 bafhlst_t 结构中;0x8000~0x9000 这 2 个页面要挂载到 m_mdmlielst 数组中第 1 个 bafhlst_t 结构中,而 0xb000 和 0xe000 这 2 个页面都要挂载到 m_mdmlielst 数组中第 0 个 bafhlst_t 结构中。
从上述代码可以看出,遍历每个内存区,然后针对其中每一个内存区进行 msadsc_t 结构的合并操作,完成这个操作的是 merlove_mem_onmemarea,我们这就去写好这个函数,代码如下所示。
上述代码中,整体上分为两步。
第一步,通过 merlove_scan_continumsadsc 函数,返回最多且地址连续的 msadsc_t 结构体的开始、结束地址、一共多少个 msadsc_t 结构体,下一轮开始的 msadsc_t 结构体的索引号。
第二步,根据第一步获取的信息调用 merlove_continumsadsc_mareabafh 函数,把第一步返回那一组连续的 msadsc_t 结构体,挂载到合适的 m_mdmlielst 数组中的 bafhlst_t 结构中。详细的逻辑已经在注释中说明。
好,内存页已经按照规定的方式组织起来了,这表示物理内存管理器的初始化工作已经进入尾声。
初始化汇总
别急!先别急着写内存分配相关的代码。到目前为止,我们一起写了这么多的内存初始化相关的代码,但是我们没有调用它们。
根据前面内存管理数据结构的关系,很显然,它们的调用次序很重要,谁先谁后都有严格的规定,这关乎内存管理初始化的成败。所以,现在我们就在先前的 init_memmgr 函数中去调用它们,代码如下所示。
上述代码中,init_msadsc、init_memarea 函数是可以交换次序的,它们俩互不影响,但它们俩必须最先开始调用,而后面的函数要依赖它们生成的数据结构。
但是 init_search_krloccupymm 函数必须要在 init_merlove_mem 函数之前被调用,因为 init_merlove_mem 函数在合并页面时,必须先知道哪些页面被占用了。
等一等,init_memmgrob 是什么函数,这个我们还没写呢。下面我们就来现实它。
不知道你发现没有,我们的 phymmarge_t 结构体的地址和数量、msadsc_t 结构体的地址和数据、memarea_t 结构体的地址和数量都保存在了 kmachbsp 变量中,这个变量其实不是用来管理内存的,而且它里面放的是物理地址。
但内核使用的是虚拟地址,每次都要转换极不方便,所以我们要设计一个专用的数据结构,用于内存管理。我们来定义一下这个结构,代码如下。
这些代码非常容易理解,我们就不再讨论了,无非是将内存管理核心数据结构的地址和数量放在其中,并计算了一些统计信息,这没有任何难度,相信你会轻松理解。
重点回顾
今天课程的重点工作是初始化我们设计的内存管理数据结构,在内存中建立它们的实例变量,我来为你梳理一下重点。
首先,我们从初始化 msadsc_t 结构开始,在内存中建立 msadsc_t 结构的实例变量,每个物理内存页面一个 msadsc_t 结构的实例变量。
然后是初始化 memarea_t 结构,在 msadsc_t 结构的实例变量之后,每个内存区一个 memarea_t 结构实例变量。
接着标记哪些 msadsc_t 结构对应的物理内存被内核占用了,这些被标记 msadsc_t 结构是不能纳入内存管理结构中去的。
最后,把所有的空闲 msadsc_t 结构按最大地址连续的形式组织起来,挂载到 memarea_t 结构下的 memdivmer_t 结构中,对应的 dm_mdmlielst 数组中。
不知道你是否想过,随着物理内存不断增加,msadsc_t 结构实例变量本身占用的内存空间就会增加,那你有办法降低 msadsc_t 结构实例变量占用的内存空间吗?期待你的实现。
思考题
请问在 4GB 的物理内存的情况下,msadsc_t 结构实例变量本身占用多大的内存空间?
欢迎你在留言区跟我交流互动,也希望你能把这节课分享给你的同事、朋友。
好,我是 LMOS,我们下节课见!
分享给需要的人,Ta购买本课程,你将得20元
生成海报并分享
赞 11
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
16 | 划分土地(上):如何划分与组织内存?
下一篇
18 | 划分土地(下):如何实现内存页的分配与释放?
精选留言(27)
- neohope2021-06-17一、稍微整理了一下流程: init_hal->init_halmm->init_memmgr //每个页对应一个msadsc_t 结构体,循环填充msadsc_t 结构体数组 ->init_msadsc //初始化三类memarea_t,硬件区、内核区、用户区 ->init_memarea //对已使用的页打上标记,包括:BIOS中断表、内核栈、内核、内核映像 ->init_search_krloccupymm(&kmachbsp); //将页面按地址范围,分配给内存区 //然后按顺序依次查找最长连续的页面,根据连续页面的长度, //将这些页面的msadsc_t挂载到memdivmer_t 结构下的bafhlst_t数组dm_mdmlielst中 ->init_merlove_mem(); //物理地址转为虚拟地址,便于以后使用 ->init_memmgrob(); 二、对于最后的问题,用了虚拟机进行测试,但无论内存大小,总有56K内存没能找到【有知道的小伙伴,麻烦帮忙解答一下】: 1、4G内存情况如下: 理论内存:0x1 0000 0000 = 4,194,304K 可用内存:0xfff8fc00 = 4,193,855K 预留区域:0x52400 = 329K 硬件使用:0x10000 = 64K 没能找到:0xE000 = 56K msadsc_t结构体大小为40,使用内存总计为: 4,193,855K/4K*40=41,938,520=接近40M 2、2G内存情况如下 理论内存:0x8000 0000 =2,097,152K 可用内存:0x7ff8fc00 = 2,096,703K 预留区域:0x52400 = 329K 硬件使用:0x10000 = 64K 没能找到:0xE000 = 56K msadsc_t结构体大小为40,使用内存总计为: 2,096,703K/4K*40=20,967,030=接近20M 3、1G内存情况如下 理论内存:0x4000 0000= 1,048,576K 可用内存:0x3ff8fc00 = 1,048,127K 预留区域:0x52400 = 329K 硬件使用:0x10000 = 64K 没能找到:0xE000 = 56K msadsc_t结构体大小为40,使用内存总计为: 1,048,127K/4K*40=10,481,270=接近10M 三、如果想节约msadsc_t内存的话,感觉有几种方案: 1、最简单的方法,就是大内存时采用更大的分页,但应用在申请内存时,同样会有更多内存浪费 2、也可以用更复杂的页面管理机制,比如相同属性的连续页面不要用多个单独msadsc_t表示,而用一个msadsc_t表示并标明其范围,并通过skiplist等数据结构加速查询。但无论是申请内存还是归还内存时,性能会有所下降,感觉得不偿失。 3、页面分组情况较少的时候,可以通过每个组建立一个链表记录哪些页面属于某个链表,而msadsc_t中只记录地址等少量信息,不适合复杂系统。展开
作者回复: 大神 6666
共 5 条评论21 - 黄光华2021-09-02老师,我觉得这个类、变量的命名可读性,还是非常非常重要的 这个项目是您手写的,每一个命名什么意思,你了然于胸 但是我们都是新手,很多变量命名真的太简略了,有很多命名就算琢磨也不一定能知道是什么意思。 真心建议命名可读性还是要增强一下哈~~
作者回复: 好的
19 - 朱炜敏2021-11-04老师,初始化这些数据结构的过程跟行为,让我想到了自己在电脑上装上4G内存条后,上电后发现系统反馈的可用内存一般只有3.8G左右。 是不是类似的,消失的几百兆内存里,存放的就是这些页管理的数据结构?
作者回复: 还有内核 的其它数据 但32位下 只能使用2.8GB
2 - pedro2021-06-16胡乱一猜~ msadsc_t 占用内存 = 4GB/4KB(页大小) * sizeof(msadsc_t )
作者回复: 是的
共 3 条评论2 - 小李飞刀2022-06-25大佬的这个变量命名是真的把我看懵了。。。
作者回复: 哈哈
1 - 浮生尽歇2021-11-14怎么调试内核呢?
作者回复: KPrint
1 - 杨军2021-09-19这一讲的内容、概念很多,尤其是内存页合并到内存区的代码不好理解,建议大家把16讲的内存页面组织结构图放在手边,对照代码多看几次慢慢就有感觉了,加油,过了内存管理这道坎,就看见胜利的曙光了
作者回复: 哈哈
1 - Geek_a5edac2021-07-24代码的命名,不太好懂,能说下命名规则么,一些看很久才知道是哪些缩写
作者回复: 不需要搞懂的,不要陷入名词概念之中
1 - lhgdy2022-12-13 来自北京大佬,这个地方为什么要价格 & 符号,怎么对参数取地址? phyadrflgs_t *tmp = (phyadrflgs_t *)(&phyadr);
- 2022-12-06 来自湖北求助一下可能是编译或者是链接方面的问题。不知道为什么全局变量被定义到了很远的位置。比如kernel.bin的大小只有56512Byte。地址从0x2000000开始。所以我设定mb_nextwtpadr的初始值为0x200e000。但是全局变量的地址却被定义在了0x220e000。这样导致了用mb_nextwtpadr向后申请内存的最终会覆盖掉全局变量的数据。想知道gcc或者ld的什么参数可以改变全局变量所在的位置啊?
作者回复: 看看链接器脚本 data 和 bss 的位置
共 2 条评论 - 卖薪沽酒2022-06-21//根据地址连续的msadsc_t结构的数量查找合适bafhlst_t结构 bafhlst_t *find_continumsa_inbafhlst(memarea_t *mareap, uint_t fmnr) { bafhlst_t *retbafhp = NULL; uint_t in = 0; if (NULL == mareap || 0 == fmnr) // 检查判断 { return NULL; } if (MA_TYPE_PROC == mareap->ma_type) //TODO 如果是用户区, 直接返回第一个?, 这里不太懂 { return &mareap->ma_mdmdata.dm_onemsalst; } if (MA_TYPE_SHAR == mareap->ma_type) // 如果是共享区, 直接返回null { return NULL; } 这里的用户区为啥直接返回的是第一个, 不用做类似内核区的判断展开
作者回复: 因为用户区总是一次分配一个页面
共 2 条评论 - 卖薪沽酒2022-06-21// mbsp->mb_krlinitstack & (~(0xfffUL)) 取得是高 52位,低12位全部之置为0, // TODO mbsp->mb_krlinitstack 作为结束地址的 retschmnr = search_segment_occupymsadsc(msadstat, msanr, mbsp->mb_krlinitstack & (~(0xfffUL)), mbsp->mb_krlinitstack);=========================================================这里的结束地址为啥不是 mb_krlinitstack + 栈的大小的, 这里不太懂,展开
作者回复: 栈是向下伸长的
- 艾恩凝2022-04-13打卡,函数一步步的分析到位,函数搞清逻辑,花了三个下午,再花一个晚上的时间整理一下,参数命名是真的影响阅读,只能联系上下文了
作者回复: 哈哈
- ifelse2022-02-14大家都能看懂代码?
作者回复: 代码难度有点点大
- 秋宇雨2022-01-28u1s1,代码命名真心看晕了
编辑回复: 攻略:关注下这些代码要实现的功能是啥。还是死磕不出来的展开说说,大家一起讨论解决。
- 任国宁2022-01-26hi,有一个疑问,在启动初始化的时候mmu设置的每页2MB大小,到内核怎么变成4KB了,而且看源码里copy 页表数据的代码也是2MB
作者回复: 启动初始化用2MB页是为了简化开发
- PAWCOOK2021-11-29请问BIOS中断表占用的内存有必要处理吗?我们不是已经设置好了自己的中断门描述符吗?而且,只处理BIOS中断表占用的空间有什么用呢,BIOS中断服务程序不也占用了内存吗
作者回复: 要处理 因为后期要有用的
- PAWCOOK2021-11-29kmachbsp 变量的地址是虚拟地址表示的(因为它是长模式下的全局变量),而其中保存的信息却是物理地址。请问这样理解是对的吗?
作者回复: 嗯嗯 有的是物理地址 而有些不是
- 琥珀·2021-11-24扫描 phymmarge_t 结构体数组中的信息,只要它的类型是可用内存,就建立一个 msadsc_t 结构体,并把其中的开始地址作为第一个页面地址。 这里已经把其中的开始地址作为第一个页面地址,那msadsc_t这个结构体存放在哪里呢?
作者回复: 放在内核后面,初始化时指定了
- Geek_2600412021-10-29老师请问,合并内存页到内存区这里,uint_t merlove_setallmarflgs_onmemarea(memarea_t *mareap, msadsc_t *mstat, uint_t msanr)这个函数里,在最后for循环时,为什么要使用页信息结构(msadsc_t)里分配的物理地址去和某个区段逻辑地址去比较啊,这样第一个区(硬件区)还可以正常分配,第二个区(内核区)就直接会把所有页都分配到内核区了吧,第三个区就完全分配不到一个页了
作者回复: 代码是这样的逻辑 第三个区分配不到 就会到第二个区。。。