当前位置: 首页 > 科技观察

从零开始学Linux:操作系统——如何把页目录和页表当作普通物理页来操作?

时间:2023-03-17 16:08:11 科技观察

目录问题描述CPU接收到的是线性地址,不是物理地址,对页目录进行“自运算”一级查表:构造线性地址的前十位二级查表:构造线性地址的中间十位三级查表:构造线性地址的后十二位,将三个地址段组合起来,对页表进行“自运算”。在x86系统中,内存管理中的分页机制非常重要。在各种与Linux操作系统相关的书籍中,这部分内容也是大笔的一笔。如果你看过Linux内核相关的书籍,你一定对下面这张图很熟悉也很害怕:这就是Linux系统中页处理单元的多级页表查询方式。其中黄色背景部分:页面的上层目录索引和页面的中层目录索引是Linux系统自己扩展的,在原来的x86处理器中是不存在的,这也是为什么Linux中代码的相关部分比较复杂。在上一篇文章中,我们主要以图文并茂的方式讨论了x86中页目录和页表的“逆向构造”和“正向查找”两个过程。文章链接在这里:从零开始学习Linux15:【页目录与页表】——最全最接地气的理论+实例+图文详解!但是,有一个链接被故意忽略了。即:操作系统在构造页目录和页表时,如何寻址和操作?这部分也是内存管理中比较复杂的部分,就像医生给病人做手术一样,但病人不过是“医生本人”。在本文中,我们继续通过图片+实例来研究内核代码一般是如何进行这些“自操作”的。把这里的运行机制研究透了,再看Linux内核代码就不会头晕了。问题描述在上一篇文章中,我们举了这样一个例子:假设实际物理内存为1GB;硬盘上用户程序文件的长度为20MB;操作系统读取程序后,为所有地址构造页目录和页表;如下图所示:在页目录和页表的每个有效表项中,存储的地址都是每个实际物理页的前20位(因为一个物理页的长度固定为4KB,所以都是对齐的分配时,最后12位全部为0)。而页目录和页表“它们”本身都占据一个物理页的空间,所以它们都有自己的物理地址。当页目录和页表构造正确后,处理器面对一个线性地址,例如:0x4100_1800,页处理单元根据层次查找表将这个线性地址转换为物理地址:拆分线性地址:0x4100_1800=0100_0001_0000_0000___0001_1000_0000_0000;根据线性地址的前10位,在页目录中找到索引260,从而确定页表的物理地址为0x0800_4000(表项中的值为0x08004,低12位0必被添加);线性地址的中间10位,在0x0800_4000的页表中找到索引1,从而确定普通物理页的物理地址为0x0210_1000(表项中的值为0x02101,低12位)必须添加顺序0);根据线性地址commonpage的最后12位确定normalpage中的偏移量为2048,将这个偏移量加上normalpage的起始地址,得到最终的物理地址0x0210_1800。详细的讨论过程可以参考之前的文章:从零开始学Linux15:【页目录与页表】——最全最接地气的理论+实例+图解详解!。那么,问题来了:当打开页处理单元时,处理器面对的是一个线性地址,那么操作系统在构造页目录中的每一个表项时,是如何寻址这个表项的呢?具体如上图所示,当操作系统要在页目录的第256个表项中填写第一个页表的物理地址0x0800_0000时,那么CPU需要找到这个表项,这个表项的物理地址必须为.但是我们不能直接告诉CPU这个表项的物理地址,因为CPU接收的只是线性地址,它会通过分页单元的处理自动获取对应的物理地址。那么,这个线性地址的值应该是多少呢?继续举例说明,通俗易懂。假设页目录所在物理页的起始地址为0x0100_0000,那么第256个表项的物理地址为0x0100_0400。有的朋友可能会说:直接告诉处理器物理地址0x0100_0400,这不就可以了吗?这是错误的!处理器收到的是线性地址,不是物理地址,因为现在分页处理单元已经开启,而0x0100_0400是我们最后要得到的物理地址,而处理器只接受线性地址。虽然我们知道这是一个物理地址,但处理器并不知道!当我们给处理器一个地址后,处理器会一步一步的进行[段转换],然后[页转换],才得到它认为的物理地址。由于采用了“扁平化”的segment结构,这里忽略segment的处理过程,直接讨论page的处理过程。因此,我们应该用某种方法构造一个线性地址addr,让这个地址通过页处理单元,得到物理地址0x0100_0400:这个有点递归,有点像医生给它做手术他自己!现在,你应该明白你面临的问题了吧?目标是通过一定的方法构造一个线性地址addr,经过页处理单元转换后,得到物理地址0x0100_0400。重新梳理一下对页目录的操作思路:如果对一个普通物理页(以下简称:普通页)中某个地址的数据进行操作,需要经过三个查表操作:从一个表页表中的表项,找到的物理地址就是最后要操作的普通物理页。现在我们的问题是:我们需要使用页目录作为最终的操作对象。也就是说,从页表中找到的“普通页”的物理地址应该等于页目录的物理地址!作为一个软件开发者,递归的思维是随时可用的。我们构造一个线性地址addr,使其经过3次查表操作后可以指向页目录的物理地址。Level-1查表:构造线性地址的前10位,确定页表的物理地址Level-1查表:查找的对象是页目录。线性地址addr的前10位确定页目录中的索引。显然,在这个索引对应的表项中注册的地址必须指向页目录本身。常见的解决方法是:使用页目录的最后一个表项,让这个表项记录的地址指向页目录本身,如下图:也就是说,提前在页目录的最后一个表项中,填写页目录本身的物理地址,然后只要线性地址addr的前10位的值为1023,就可以得到这个表项。很容易得到addr的前10位应该是:0x3FF(二进制:1111_1111_11)。由于该表项存储的地址是页目录本身的起始地址(0x0100_0000,后12个0自动补上),这就相当于:下面进入二级搜索时,页目录将被视为“页表”来使用。如下图所示:这里红色虚线的“页表”其实就是页目录本身,只是一个影子。二级查表:构造线性地址的中间10位,确定“普通页”的物理地址。虽然一级查表的结果是页目录本身,但是处理器并不关心这个,它会将这张表当作页表来使用。现在,考虑线性地址addr的中间10位,它决定了页表中的索引号。显然,还需要继续让索引号对应的记录地址继续指向页目录本身。然后继续使用这个“页表”(实际上是页目录)的最后一个表项,也就是index=1023的表项,这个表项存储的物理地址即将是“普通页”的物理地址"从最终表查找中获得。由于这个条目预填了0x01000,加上12个尾随0后就是0x0100_0000,仍然指向页目录本身,完美!因此得到中间10位的结果:0x3FF(二进制:11_1111_1111)。如下图所示:最右边红色虚线中的“物理页面”是二次搜索的结果。它本质上还是页目录本身,只是会作为一个普通的物理页来使用。三级查找表:构造线性地址的后12位来确定“普通页”的页偏移现在,线性地址addr的前20位(这是我们的最终目标)已经构造完成,通过page表的前两层查表,成功定位到页目录本身!这是最后一步!我们知道,在线性地址到物理地址的转换过程中,最后12位表示页内的偏移量,直接从地址中取出的线性地址。也就是说:线性地址的最后12位偏移量和物理地址的值是一样的!那么,我们倒推一下:我们要操作的是页目录中的第256个表项,它的物理地址是0x0100_0400,这个物理地址相对于这个页目录起始位地址的偏移量是:0x400(0x0100_0400负0x0100_0000)。因此,线性地址addr中最后12位的值也应该是0x400。三个地址段的组合将以上三步得到的地址聚合在一起:0xFFFF_F400就是最终想要的线性地址!也就是说,只要我们告诉处理器线性地址0xFFFF_F400,它就会通过页处理单元。转换,最终找到页目录物理页中的第256个表项,也就是物理地址0x0100_0400。例如:mov[0xFFFF_4000],xxxx以上是操作系统在操作页目录本身时所采用的策略。具体到各个操作系统,可能会有细微的差别,但原理是相似的。比如本文开头的第一张图,Linux使用4级表来查找,中间的两张表可以省略。中间两张表怎么交叉,linux内核代码中的代码比较复杂,但是策略是一样的。寻址页表既然您了解了操作系统如何处理页目录,那么操作页表就不是什么大问题了。例如下图:目标:把最右边的普通物理页地址0x0200_0000放入页表0x0800_0000的第一项(只需要保存前20位),那么传递什么样的线性地址给处理设备?这个想法是完全一样的。一级查找表按照正常的分页查找流程,从页目录中的一个表项中查找出我们要操作的页表。页目录中的此项位于索引值256处,因此可以构造的线性地址的前10位为:0100_0000_00(0x100)。因此,一级查表得到的页表物理地址为0x0800_0000。二级查找表使用这个页表的最后一个条目(index=1023)预填充一个地址(0x08000),使其指向页表本身的起始物理地址。因此,可构造的线性地址的中间10位为:11_1111_1111(0x3FF)。由于这个表项存储的地址是0x0800_0000,它指向页表本身,但马上就被当作一个普通的物理页使用。这时候三级查找表已经找到了最后一个普通物理页(其实就是页表,作为普通物理页使用)。线性地址的后12位可以直接取你要操作的目标物理地址的后12位。我们的目标是操作页表中的第0个表项。该表项的物理地址为0x0800_0000,最后12位偏移量为0000_0000_0000。正确的线性地址可以通过以上三个地址段的组合得到:这里讨论的方法并不是处理页目录和页表的唯一方法。当处理逻辑比较复杂时,可能需要对页目录或页表中更多的表项进行一些特殊的预处理。如果你想挑战自己,可以看看Linux内核中的相关文档或代码!本系列介绍页目录和页表的知识点。文中如有错误或误导的地方,期待与大家共同探讨学习!写这篇文章不容易,深刻体会到那句话:写作是:在网络中思考——通过树状结构——用线性语言清晰表达。本文转载自微信公众号《IOT物联网小镇》【编辑推荐】鸿蒙官方战略合作共建-HarmonyOS技术社区Windows10系统,如何关闭输入法?Windows10关闭输入法操作步骤Windows11不好用?升级Windows11必须注意的细节Windows11正式版两周体验:内存占用高,莫名卡顿为什么国产手机那么火爆,苹果却依然卖不出去?工信部:基本解决App打开弹窗无法关闭问题