一、实验目的

分页内存管理是内存管理的基本方法之一。本实验的目的在于全面理解分页式内存管理的基本方法以及访问页表,完成地址转换等的方法。

二、实验过程&错误

内容(一):设计不同的方式引发页错误,观察并记录对应的现象

存储器保护:
操作系统的一个主要任务是将程序彼此隔离。为了实现这个目标,操作系统利用硬件功能来确保一个进程的存储区域不能被其他进程访问。根据硬件和操作系统的实现,存在各种方法。
比方说,一些ARMCortex-M处理器(用于嵌入式系统)具有存储器保护单元(MPU),其允许您定义具有不同访问权限的存储器区域的小数量(例如,不访问、只读、读写)。在每个内存访问上,MPU可确保地址在具有正确访问权限的区域中,否则会引发异常。通过改变每个进程交换机上的区域和访问权限,操作系统可以确保每个进程仅访问自己的内存,并因此将进程彼此隔离。
在x86上,硬件支持两种不同的内存保护方法:分段和分页。
分段:
分段技术已经在1978年引入,最初是为了增加可寻址内存的数量。当时的情况是CPU只使用16位地址,这将可寻址内存量限制在64 KiB。为了使这64 KiB更容易访问,引入了额外的段寄存器,每个寄存器都包含一个偏移地址。CPU将此偏移量自动添加到每个内存访问中,以便可以访问多达1MiB的内存。
段寄存器由CPU自动选择,这取决于内存访问的类型:为了获取指令,使用代码段CS,对于堆栈操作(Push/POP),使用堆栈段SS。其他指令使用数据段DS或额外段es。后来又增加了两个段寄存器FS和GS,可以自由使用。
在分段的第一个版本中,段寄存器直接包含偏移量,不执行访问控制。后来,随着受保护模式的引入,这种情况发生了改变。当CPU在这种模式下运行时,段描述符将索引包含到本地或全局描述符表中,该表除了包含偏移地址之外,还包含段大小和访问权限。通过为每个将内存访问限制到进程自身内存区域的进程加载单独的全局/本地描述符表,OS可以隔离进程。
通过修改实际访问之前的内存地址,分段已经采用了一种现在几乎在任何地方都使用的技术:虚拟内存。
虚拟内存:
虚拟内存背后的想法是将内存地址从底层物理存储设备中提取出来。首先执行转换步骤,而不是直接访问存储设备。对于分段,翻译步骤是添加活动段的偏移地址。假设一个程序在一个偏移量为0x1111000的段中访问内存地址0x1234000:真正访问的地址是0x2345000。
为了区分这两种地址类型,翻译前的地址称为虚拟地址,翻译后的地址称为物理地址。这两种地址之间的一个重要区别是物理地址是唯一的,并且总是引用相同的、不同的内存位置。另一方面,虚拟地址依赖于翻译功能。完全有可能两个不同的虚拟地址引用相同的物理地址。同样,相同的虚拟地址可以在使用不同的转换函数时引用不同的物理地址。
此属性有用的一个示例是并行运行同一程序两次:

这里同样的程序运行两次,但具有不同的翻译功能。第一个实例的段偏移量为100,因此它的虚拟地址0-150被转换为物理地址100-250。第二实例具有偏移300,其将其虚拟地址0-150转换为物理地址300-450。这允许两个程序运行相同的代码,并使用相同的虚拟地址而不相互干扰。
另一个优点是,即使它们使用完全不同的虚拟地址,也可以将程序放置在任意的物理存储器位置。因此,OS可以利用全部可用存储器而无需重新编译程序。
分裂:
虚拟地址和物理地址的区别–使得分段功能非常强大。然而,它存在着分裂的问题。例如,假设我们希望运行上面看到的程序的第三份副本:

没有方法将程序的第三实例映射到虚拟存储器而不重叠,即使有足够的空闲存储器可用。问题是,我们需要连续的内存,不能使用小的空闲chunks。打击这种分段的一种方式是暂停执行,将存储器的已用部分更靠近地移动,更新转换,然后恢复执行:

现在有足够的连续空间来启动我们的程序的第三个实例。这种碎片整理过程的缺点是需要复制大量的存储器,这降低了性能。还需要在存储器变得过于分散之前定期进行。这使得性能不可预测,因为程序在随机时间暂停并且可能变得无响应。碎片问题是大多数系统不再使用分段的原因之一。事实上,在x86上的64位模式下,分段甚至不支持。而不是使用寻呼,这完全避免了分裂问题。
页:
这样做的目的是将虚拟内存和物理内存空间划分为小的、固定大小的块。虚拟内存空间的块称为页,物理地址空间的块称为帧。每个页面都可以单独映射到一个帧,这样就可以在不连续的物理帧之间分割更大的内存区域。如果我们重温一下碎片内存空间的示例,但这次使用分页而不是分段,则可以看出这一点的优势:

在这个例子中,我们有50个字节的页面大小,这意味着我们的每个内存区域被分割成三个页面。每个页面分别映射到一个帧,因此一个连续的虚拟内存区域可以映射到非连续的物理帧。这允许我们在不执行任何碎片整理的情况下启动程序的第三个实例。
内部碎片:
与分段相比,分页使用了大量的小的、固定大小的内存区域,而不是几个大的、可变大小的区域。因为每个帧都有相同的大小,所以没有任何帧太小而不能使用,因此不会出现碎片。
或者似乎没有碎裂发生。还有一些隐藏的碎片化,所谓的内部碎片化.内部碎片的发生是因为并非每个内存区域都是页面大小的确切倍数。在上面的例子中,设想一个大小为101的程序:它仍然需要三页大小为50的页面,因此它将比所需的多占用49个字节。为了区分这两种类型的碎片,在使用分段时发生的这种碎片类型称为外部碎片。
内部分割是不幸的,但通常比分割时发生的外部碎片要好。它仍然浪费内存,但不需要碎片整理,并且可以预测碎片的数量(平均每个内存区域半页)。
页表:
我们看到,每个潜在的数百万页是单独映射到一个框架。此映射信息需要存储在某个地方。分段对每个活动内存区域使用单个段选择器寄存器,这是不可能分页的,因为有比寄存器更多的页面。相反,分页使用称为页表的表结构来存储映射信息。
对于我们上面的例子,页面表将是这样的:

我们看到每个程序实例都有自己的页面表。指向当前活动表的指针存储在专用CPU寄存器中。在x86上,这个寄存器称为CR3。在运行每个程序实例之前,操作系统的工作是用指向正确页表的指针加载此寄存器。
在每次访问内存时,CPU从寄存器中读取表指针,并查找表中被访问页的映射帧。这完全是在硬件上完成的,并且对运行中的程序完全透明。为了加快翻译过程,许多CPU体系结构都有一个特殊的缓存来记住最后一次翻译的结果。
根据体系结构的不同,页表条目还可以在标志字段中存储访问权限等属性。在上面的示例中,“r/w”标志使页面既可读又可写。

多级页表:
我们刚才看到的简单页表在较大的地址空间中有一个问题:它们浪费内存。例如,假设一个程序使用四个虚拟页面0、1_000_000、1_000_050和1_000_100(我们使用_作为数千个分隔符):

它只需要4个物理帧,但页表有一百万个条目。我们不能省略空条目,因为CPU将不再能够直接跳转到翻译过程中的正确条目(例如,不再保证第四页面使用第四条目)。
为了减少浪费的内存,我们可以使用两级页表。这个想法是我们针对不同的地址区域使用不同的页表。名为“Level2”页表的附加表包含地址区域和(级别1)页表之间的映射。
这一点最好用一个例子来解释。让我们定义每个级别1的页表负责10_000的一个区域。则上述示例映射将存在以下表格:

第0页落入第一个10_000字节区域,因此它使用第2级页表的第一个条目。此条目指向“Level1”页面表T1,它指定第0页指向第0帧。
页面1_000_000、1_000_050和1_000_100都落入第100个10000字节区域,因此它们使用第2级页表的第100条目的。该入口指向不同级别1的页表T2,其将三个页面映射到帧100、150和200。请注意,级别1表中的页地址不包括区域偏移,因此第1页_000_050的条目仅为50。
在2级表格中,我们仍然有100个空条目,但比以前的百万个空条目少很多。这种节省的原因是,我们不需要为10_000和1_000_000之间的未映射存储区域创建1级页面表。
二级页面表的原理可以扩展到三个、四个或更多级别。然后,页表寄存器指向最高级别的表,指向下一个较低级别的表,指向下一个较低的级别,依此类推。然后,“Level1”页面表指向映射的帧。一般原理称为多级或分层页表。
现在我们知道分页和多级页表是如何工作的,我们可以查看在x86_64架构中如何实现分页(我们假设CPU以64位模式运行)。
x86_64架构上的分页:
x86_64体系结构使用4级页表,页面大小为4 KiB。每个页面表独立于该级别,其固定大小为512项。每个条目的大小为8个字节,因此每个表的大小为512*8B=4KiB,因此正好适合于一个页面。级别的页表索引直接来自虚拟地址:

我们看到每个表索引由9位组成,这是有意义的,因为每个表都有29=512个条目。最低的12位是4KiB页面中的偏移量(212字节=4 KiB)。位48到64被丢弃,这意味着x86_64实际上不是64位,因为它只支持48位地址。有计划通过5级页面表将地址大小扩展到57位,但还没有支持此功能的处理器。
即使丢弃了48到64位,也不能将其设置为任意值。相反,这个范围内的所有位都必须是位47的副本,这样才能保持地址的唯一性,并允许像5级页面表这样的未来扩展。这被称为符号扩展,因为它非常类似于两个补语中的符号扩展。当地址没有正确地进行签名扩展时,CPU会抛出一个异常。
实例:
让我们通过一个示例来详细了解翻译过程是如何工作的:

当前活动级别4页表的物理地址存储在CR3寄存器中,该表是4级页表的根。然后,每个页面表条目指向下一个级别表的物理框架。然后,第1级表的条目指向映射的框架。注意,页面表中的所有地址都是物理的,而不是虚拟的,因为否则CPU也需要转换这些地址(这可能导致无休止的递归)。
上面的页面表层次结构映射两页(蓝色)。从页面表索引中可以推断出这两个页面的虚拟地址是0x803FE7F000和0x803FE00000。让我们看看当程序试图从地址0x803FE7F5CE读取时会发生什么。首先,我们将地址转换为二进制,并确定该地址的页表索引和页偏移量:

使用这些索引,我们现在可以遍历页表层次结构来确定地址的映射帧:
·我们首先从CR3寄存器中读取第4级表的地址。
·级别4索引为1,因此我们查看该表的索引1的条目,它告诉我们,级别3表存储在地址16KiB。
·我们从该地址加载三级表,并查看索引为0的条目,这将我们指向24KiB的2级表。
·二级索引为511,因此我们查看该页的最后一个条目,以找出一级表的地址。
·通过与级别1表的索引127的条目,我们最终发现页面被映射到帧12KiB或0x3000(十六进制)。
·最后一步是将页偏移添加到帧地址,以获得物理地址0x3000+0x5ce=0x35ce。

一级表中页面的权限为r,表示只读…硬件强制执行这些权限,如果我们试图写入该页面,则会抛出异常。上级页面中的权限限制了下级可能的权限,因此,如果我们将三级条目设置为只读,即使较低级别指定读/写权限,使用此条目的页面也不能写。
重要的是要注意,即使此示例只使用每个表的单个实例,但每个地址空间中通常有每个级别的多个实例。最多有:

·一个四级表,
·512个3级表(因为4级表有512个条目),
·512*512个二级表(因为512个三级表各有512个条目),以及
·为512*512*512的一级表(每个二级表512条目)..

页表格式:
x86_64体系结构上的页表基本上是由512个条目组成的数组。在Rust语法中:

#[repr(align(4096))]pub struct PageTable {entries: [PageTableEntry; 512],
}

如repr属性所示,页表需要对齐页,即在4 KiB边界上对齐。此要求保证页面表始终填充完整的页面,并允许进行优化,从而使条目非常紧凑。每个条目大8个字节(64位),格式如下:

我们看到只有12-51位被用来存储物理帧地址,其余的位被用作标志,或者可以被操作系统自由使用。这是可能的,因为我们总是指向一个4096字节对齐的地址,或者指向对页的页表,或者指向映射的帧的开始。这意味着比特0-11始终为零,因此没有理由存储这些位,因为硬件可以在使用地址之前将它们设置为零。比特52-63也是如此,因为x86_64体系结构只支持52位物理地址(类似于它只支持48位虚拟地址)。
让我们仔细看看可用的标志:
·当前标志将映射的页面与未映射的页面区分开来。它可以用于在主内存满时将页面临时交换到磁盘。当随后访问该页时,会出现一个称为页错误的特殊异常,操作系统可以对此作出反应,从磁盘重新加载丢失的页,然后继续执行该程序。
·可写标志和不执行标志分别控制页的内容是可写的还是包含可执行指令的。
·当对页进行读或写时,CPU会自动设置已访问的和错误的标志。操作系统可以利用这些信息,例如,决定自上次保存到磁盘以来哪些页面要交换,或者页面内容是否被修改。
·写入缓存和禁用缓存标志允许单独控制每个页面的缓存。
·用户访问标志使页面对用户空间代码可用,否则只有在CPU处于内核模式时才能访问它。此特性可用于在运行用户空间程序时保持内核映射,从而使系统调用更快。但是,谱漏洞仍然允许用户空间程序读取这些页面。
·全局标志向硬件发出信号,表明一个页面在所有地址空间中都可用,因此不需要从地址空间交换机上的转换缓存(参见下面关于TLB的部分)中删除。此标志通常与清除的用户可访问标志一起使用,以将内核代码映射到所有地址空间。
·巨大标志允许通过让第2级或第3级页表的条目直接指向映射的框架来创建较大大小的页面。在此位设置下,页面大小将增加因子512,对于第2级条目,将增加到2MiB=512x4KiB,对于第3级条目,页面大小甚至会增加到1 GiB=512x2 MiB。使用较大页面的优点是需要更少的翻译缓存行和更少的页表。
x86_64机箱提供页表及其条目的类型,因此我们不需要自己创建这些结构。
查找缓冲区TLB:
由于每个转换都需要4个内存访问,所以4级页表使得虚拟地址的转换非常昂贵。为了提高性能,x8664体系结构在所谓的翻译lookaside缓冲区(TLB)中缓存最后几个翻译。这允许在仍缓存转换时跳过翻译。
与其他CPU缓存不同,TLB不是完全透明的,并且在页表的内容更改时不会更新或删除翻译。这意味着每当内核修改页表时,都必须手动更新TLB。为此,有一个名为invlpg(“invalidatepage”)的特殊CPU指令从TLB中删除指定页面的翻译,以便在下次访问时从页表再次加载。也可以通过重新加载CR3寄存器来完全刷新TLB,CR3寄存器模拟地址空间交换机。x8664机箱为tlb模块中的两个变体提供Rust功能。
记住在每个页表修改上刷新TLB非常重要,因为否则CPU可能会继续使用旧的翻译,这可能会导致非常难以调试的非确定性错误。

成就:
有一点我们还没有提到:我们的内核已经在分页上运行了。我们在“A Minimal Rust Kernel”文章中添加的引导加载器已经建立了一个4级的分页层次结构,它将内核的每一页映射到物理框架。引导加载程序这样做是因为在x86_64上,在64位模式下分页是强制性的。
这意味着我们在内核中使用的每个内存地址都是一个虚拟地址。访问地址0xb 8000的VGA缓冲区只有效,因为引导加载器标识映射了该内存页,这意味着它将虚拟页0xb 8000映射到物理帧0xb 8000。
分页使内核已经相对安全,因为每个超出界限的内存访问都会导致页面错误异常,而不是写入随机物理内存。引导加载程序甚至为每个页面设置正确的访问权限,这意味着只有包含代码的页是可执行的,只有数据页是可写的。
步骤1:页面错误:
让我们尝试通过访问内核之外的一些内存来导致页面错误。首先,我们创建一个页面错误处理程序并在IDT中注册它,这样我们就可以看到一个页面故障异常,而不是一个普通的双重错误:
我们在我们的interrupts文件中加入如下代码:

CR2寄存器由CPU在页面错误上自动设置,并包含导致页面错误的访问虚拟地址。我们使用x86_64机箱的Cr2::Read函数来读取和打印它。PageFaultErrorCode类型提供了有关导致页错误的内存访问类型的更多信息,例如,它是由读操作还是写操作引起的。因此,我们也把它打印出来。如果不解决页面故障,我们就不能继续执行,所以我们在末尾输入hlt_循环。现在,我们可以尝试访问内核之外的一些内存:
我们将main文件修改如下:

现象1-1:当我们运行它时,我们看到我们的页面错误处理程序被调用:

CR2寄存器确实包含0xdelbeaf,这是我们试图访问的地址。错误代码通过引发的_by_WITH告诉我们,该错误是在执行写操作时发生的。它通过未设置的比特来告诉我们更多的信息。例如,保护_违章标志没有设置,这意味着由于目标页不存在,所以发生了页面错误。
我们看到当前的指令指针是0x2031b2,所以我们知道这个地址指向一个代码页。代码页被引导加载程序映射为只读,因此读取此地址是可行的,但是写入会导致页面错误。通过将0x2031b2指针更改为0x2031b2,您可以尝试这样做:
我们在main文件进行如下修改

现象1-2:通过注释最后一行,我们看到读取访问工作,但写入访问导致页面错误:

我们看到打印了“ReadWork”消息,这表明Read操作没有导致任何错误。但是,不是“写工作”消息,而是出现了页面错误。这一次,除了所导致的_by_WITE标志之外,还设置了保护_违章标志,这表明页面已经存在,但不允许对其进行操作。在这种情况下,不允许写入页,因为代码页被映射为只读。
步骤2:访问页表
让我们看一下定义内核映射方式的页面表:
我们将我们的main文件进行如下修改:

x86_64的Cr3:read函数从CR3寄存器返回当前活动的4级页表。它返回PhysFrame和Cr3Flags类型的元组。我们只对框架感兴趣,所以忽略了元组的第二个元素…
现象2-1:当我们运行它时,我们看到以下输出:

因此,当前活动级别4页表存储在物理内存中的地址0x1000,如PhysAddr包装器类型所示。现在的问题是:我们如何从内核访问这个表?
当分页处于活动状态时,直接访问物理内存是不可能的,因为程序很容易绕过内存保护而访问其他程序的内存。因此,访问表的唯一方法是通过映射到地址0x1000的物理帧的虚拟页面。为页表框架创建映射的问题是一个普遍的问题,因为内核需要定期访问页表,例如在为新线程分配堆栈时。

分页实现:
上文介绍了分页的概念。它通过与分段的比较来激发分页,解释了分页和页表的工作原理,然后介绍了x86_64的4级页表设计。我们发现引导加载程序已经为内核设置了一个页面表层次结构,这意味着我们的内核已经在虚拟地址上运行。这提高了安全性,因为非法内存访问会导致页面错误异常,而不是修改任意物理内存。
上文有这样一个问题:我们无法从内核访问页面表,因为它们存储在物理内存中,并且我们的内核已经运行在虚拟地址上。现在继续,并探讨了使我们的内核可以访问页面表框架的不同方法。我们将讨论每种方法的优缺点,然后为我们的内核决定一种方法。
要实现该方法,我们需要引导加载程序的支持,因此我们将首先配置它。之后,我们将实现一个遍历页面表层次结构的函数,以便将虚拟地址转换为物理地址。最后,我们将学习如何在页面表中创建新的映射,以及如何找到用于创建新页表的未使用内存框架。
依赖性更新:
我们需要版本为0.7.5或更高版本的x86_64依赖项。更新我们Cargo.toml:中的依赖关系,即将cargo.toml文件进行如下修改:

访问页表:
从内核访问页面表并不像看起来那么容易。为了理解这个问题,让我们再看看前面文章的4级页面层次结构:
这里的重要内容是每个页面条目存储下一个表的物理地址。这避免了对这些地址进行翻译的需要,这对于性能来说是不好的,并且很容易导致循环的翻译循环。
我们的问题是,我们无法从内核直接访问物理地址,因为内核还在虚拟地址的顶部运行。例如,当我们访问地址4KiB时,我们访问虚拟地址4KiB,而不是存储第4级页表的物理地址4KiB。当我们想要访问物理地址4KiB时,我们只能通过映射到它的一些虚拟地址来这样做。
因此,为了访问页表框架,我们需要将一些虚拟页映射到它们。创建这些映射的方式不同,所有这些映射都允许我们访问任意的页表框架。
恒等映射:
简单的解决方案是标识映射所有页面表:

在本例中,我们看到了各种身份映射的页表框架。这样,页表的物理地址也是有效的虚拟地址,以便我们可以容易地访问从CR3寄存器开始的所有级别的页表。
然而,它使虚拟地址空间混乱,并且使得难以找到更大尺寸的连续存储区域。例如,假设我们希望在上述图形中创建大小为1000KiB的虚拟内存区域,例如用于对文件进行内存映射。我们无法在28KiB启动该区域,因为它将与1004KiB上已映射的页面冲突。因此,我们必须进一步研究,直到找到足够大的未映射区域,例如1008KiB。这与分段一样是一个碎片问题。
同样,这使得创建新的页表变得更加困难,因为我们需要找到物理帧,这些物理帧的相应页没有在使用中。例如,让我们假设我们为我们的内存映射文件保留了从1008KiB开始的虚拟1000KiB内存区域。现在,我们无法使用任何具有1000KiB和2008KiB之间物理地址的帧,因为我们无法对其进行恒等映射。
固定偏移距页表:
为避免使虚拟地址空间混乱的问题,我们可以使用单独的内存区域进行页表映射。因此,我们不使用标识映射页表框架,而是将它们映射到虚拟地址空间中的固定偏移处。例如,偏移可以是10TiB:

通过使用范围10TiB中的虚拟内存。(10TiB+物理内存大小)专用于页表映射,避免了身份映射的冲突问题。如果虚拟地址空间远大于物理存储器大小,则仅保留虚拟地址空间的这样大的区域是可能的。这不是x86_64上的问题,因为48位地址空间是256TiB。
无论何时创建新的页表,这种方法仍然存在我们需要创建新映射的缺点。此外,它不允许访问其他地址空间的页表,这在创建新进程时将是有用的。
映射完整的物理内存:
我们可以通过映射完整的物理内存而不是仅页表框架来解决这些问题:

这种方法允许内核访问任意物理内存,包括其他地址空间的页表帧。保留的虚拟内存范围具有与以前相同的大小,不同之处在于它不再包含未映射的页。
这种方法的缺点是需要额外的页表来存储物理内存的映射。这些页表需要存储在某个地方,因此它们消耗掉了一部分物理内存,这在内存较少的设备上可能是一个问题。
但是,在x86_64上,我们可以使用大小为2MiB的巨大页面进行映射,而不是使用默认的4KiB页面。这样,映射32 GIB的物理内存只需要132 Kib页表,因为只需要一个三级表和32个二级表。巨大的页面还具有更高的缓存效率,因为它们在翻译旁白缓冲区(TLB)中使用的条目较少。
临时映射:
对于物理内存非常少的设备,我们只能在需要访问页面表帧时临时映射它们。为了能够创建临时映射,我们只需要一个标识映射1级别表:

此图形中的第1级表控制虚拟地址空间的前2个MIB。这是因为它可以从CR3寄存器开始,并在第4级、第3级和第2级页表中的第0项之后到达。索引8的条目将地址32 Kib上的虚拟页面映射到地址32 Kib的物理框架,从而映射级别1表本身。这张图用32 Kib的水平箭头显示了这个标识映射。
通过写入标识映射级别1表,我们的内核可以创建多达511个临时映射(512减去标识映射所需的条目)。在上面的示例中,内核创建了两个临时映射:
·通过将第1级表的第0项映射到地址为24 Kib的框架,它创建了一个将0 Kib的虚拟页面临时映射到2级页面表的物理框架的临时映射,该映射由虚线箭头指示。
·通过将1级表的第9项映射到地址4 Kib的帧,它创建了36 Kib处的虚拟页面到4级页面表的物理框架的临时映射,该映射由虚线箭头指示。
现在内核可以通过写入页0 Kib来访问第2级页表,并通过写入第36页Kib来访问第4级页表。
访问具有临时映射的任意页面表框架的过程是:
·在标识映射的级别1表中搜索一个空闲条目。
·将该条目映射到我们要访问的页表的物理框架。
·通过映射到条目的虚拟页面访问目标框架。
·将条目设置为未使用,从而再次删除临时映射。
这种方法重用相同的512个虚拟页面来创建映射,因此只需要4KiB的物理内存。缺点是它有点麻烦,特别是因为一个新的映射可能需要修改多个表级别,这意味着我们需要多次重复上面的过程。
递归页表:
另一个有趣的方法,即不需要附加的页表,是递归地映射页表。此方法背后的思想是将第4级页表的某些条目映射到四级表本身。通过这样做,我们可以有效地保留虚拟地址空间的一部分,并将所有当前和未来的页表帧映射到该空间。举例如下:

这个帖子开头的示例唯一的不同之处是在第4级表中索引511处的附加条目,该条目映射到物理框架4 Kib,这是4级表本身的框架。
通过让CPU在转换时跟随这个条目,它不会到达第3级表,而是再次到达相同的第4级表。这与调用自身的递归函数类似,因此此表称为递归页表。重要的是,CPU假定第4级表中的每个条目指向第3级表,因此它现在将第4级表视为第3级表。这是因为所有级别的表在x86_64上都有完全相同的布局。
通过在开始实际转换之前跟踪一次或多次递归条目,可以有效地缩短CPU遍历的级别数。例如,如果我们跟随递归条目一次,然后继续到第3级表,CPU认为第3级表是第2级表。更进一步,它将第2级表视为第1级表,将第1级表视为映射的框架。这意味着我们现在可以读写1级页表,因为CPU认为它是映射的帧。下图说明了5个翻译步骤:

类似地,我们可以在开始转换之前两次跟随递归条目,以将遍历的级别的数量减少到两个:

让我们一步一步地看一遍:首先,CPU遵循第4级表上的递归条目,并认为它到达了第3级表。然后,它再次跟踪递归条目,并认为它达到了第2级表。但在现实中,它仍然在第4级表上。当CPU现在跟随一个不同的条目时,它降落在第3级表上,但认为它已经在第1级表上了。因此,当下一个入口点位于第2级表时,CPU认为它指向映射的帧,这允许我们读取和写入第2级表。
访问第3级和第4级的表以同样的方式工作。对于访问第3级表,我们跟踪递归条目三次,告诉CPU以为它已经在第1级表上了。然后,我们跟踪另一个条目,并到达一个3级表,CPU将其视为映射的框架。对于访问第4级表本身,我们只需遵循递归条目4次,直到CPU将第4级表本身作为映射的帧(在下面的图形中以蓝色表示)。

递归分页是一种有趣的技术,它展示了页面表中的单个映射是多么强大。它相对容易实现,只需要最少的设置(只是一个递归条目),所以对于分页的第一次实验来说,这是一个很好的选择。
然而,它也有一些缺点:
·它占用了大量的虚拟内存(512 GiB)。在大的48位地址空间中,这不是一个大问题,但它可能导致次优缓存行为。
·它只允许轻松访问当前活动的地址空间。通过更改递归条目仍然可以访问其他地址空间,但切换回时需要临时映射。我们在(过时的)重新映射内核帖子中描述了如何做到这一点。
·它严重依赖于x86的页表格式,并且可能无法在其他架构上工作。
步骤3:引导加载程序支持
所有这些方法都需要对其设置进行页表修改。例如,需要创建物理存储器的映射,或者需要递归地映射表4表的条目。问题在于,我们无法创建这些必需的映射,而不存在访问页表的现有方法。
这意味着我们需要引导加载程序的帮助,这将创建内核运行的页表。引导加载程序可以访问页表,因此它可以创建我们需要的任何映射。在当前实施中,bootloader机箱支持上述两种方法,通过cargo特征进行控制:
·map_physical_memory功能将整个物理内存映射到虚拟地址空间中。因此,内核可以访问所有物理内存,并且可以遵循映射完整的物理内存方法。
·使用recursive_page_table功能,bootloader递归地映射四级页面表的条目。这允许内核访问递归页表部分中描述的页表。
我们为内核选择第一种方法,因为它是简单的、独立的、更强大的(它还允许访问非页表框架)。要启用所需的引导加载程序支持,我们将map_physical_memory功能添加到bootloader依赖项,在toml文件中加入如下代码:

启用此功能后,引导加载程序将完整的物理内存映射到一些未使用的虚拟地址范围。为了将虚拟地址范围传送到内核,引导加载程序会传递引导信息结构。
步骤4:引导信息
bootloader机箱定义了bootinfo结构,包含它传递给内核的所有信息。struct仍处于早期阶段,因此在更新到未来的semver-不兼容的bootloader版本时,预期会出现一些损坏。如果启用了map_physical_memory功能,则它当前具有两个字段memory_map和physical_memory_offset:
·memory_map字段包含可用物理内存的概述。这说明我们的内核有多少物理内存可用在系统中,哪些内存区域是为设备(如VGA硬件)保留的。内存映射可以从BIOS或UEFI固件中查询,但在启动过程中仅非常早。因此,引导加载程序必须提供它,因为内核无法稍后检索它。我们稍后将需要在此帖子中的内存映射。
·physical_memory_offset告诉我们物理内存映射的虚拟起始地址。通过将此偏移量添加到物理地址,我们获得相应的虚拟地址。这允许我们从内核访问任意物理内存。
bootloader将bootinfo结构传递到我们的内核,以“static bootinfo”参数的形式指向我们的_start函数。我们还没有在函数中声明这个参数,因此让我们添加它,我们在main中加入如下代码:

在此之前停止此参数并不是一个问题,因为x86_64调用约定在CPU寄存器中传递第一个参数。因此,该参数在未声明时就被忽略了。但是,如果我们意外地使用了错误的参数类型,这将是一个问题,因为编译器不知道入口点函数的正确类型签名。
现象4-1:找不到入口签名

步骤5:入口点宏
由于我们的_start函数是从bootloader调用的,所以不发生我们的函数签名的检查。这意味着我们可以让它在没有任何编译错误的情况下使用任意参数,但它将在运行时失败或导致未定义的行为。
为了确保进入点函数始终具有引导加载程序期望的正确签名,bootloader机箱提供了一个Entry_Point宏,它提供了一种类型检查的方法,以将锈函数定义为入口点。让我们重写我们的入口点函数来使用此宏,我们将main文件进行如下修改:

我们不再需要为我们的入口点使用extern"C"或no_mangle,因为宏定义了我们的真正的低级_start入口点。kernel_main函数现在是一个完全正常的rust函数,因此我们可以为其选择任意名称。重要的是,它被类型检查,以便在使用错误的函数签名时发生编译错误,例如通过添加参数或更改参数类型。让我们在我们的lib.rs:中进行同样的改变,将lib文件进行如下修改:

由于入口点只在测试模式下使用,因此我们将#[cfg(test)]属性添加到所有项。我们给出了不同的名称test_kernel_main,以避免与我们的main.rs.的kernel_main混淆,我们现在不使用bootinfo参数,因此我们用a_来前缀参数名称以静默未使用的变量警告。
步骤6:成就
现在我们已经可以访问物理内存了,我们终于可以开始实现我们的页表代码了。首先,我们将查看内核运行的当前活动页面表。在第二步中,我们将创建一个转换函数,返回一个给定虚拟地址映射到的物理地址。作为最后一步,我们将尝试修改页面表以创建一个新的映射。在开始之前,我们为代码创建一个新的内存模块,将lib文件进行如下修改:

然后我们新建一个memory文件
步骤7:访问页面表
现在,通过创建一个Active_level_4_table函数,返回对Active Level 4页表的引用,我们可以从那里继续,我们在我们新建的memory文件中加入如下代码

首先,我们从CR3寄存器读取活动级别4表的物理框架。然后,我们获取其物理开始地址,将其转换为U64,并将其添加到physical_memory_offset中,以获得页表框架映射的虚拟地址。最后,通过as_mut_ptr方法将虚拟地址转换为*mutPageTable原始指针,然后不安全地从它创建一个&mutPageTable引用。我们创建一个&mut引用,而不是一个&引用,因为我们将在后面的文章中修改页面表。
这里我们不需要使用不安全块,因为Rust将不安全fn的完整主体视为一个大的不安全块。这使得我们的代码更加危险,因为我们可能会不小心在前面的行中引入一个不安全的操作。这也使得识别不安全操作变得更加困难。有一个RFC来改变这种行为。
现在我们可以使用这个函数打印4级表的条目:

首先,我们将BootInfo结构的physical_memory_offset转换为VirtAddr,并将其传递给Active_level_4_table函数。然后,我们使用ITER函数对页面表条目进行迭代,并使用枚举组合器向每个元素添加索引I。我们只打印非空条目,因为所有512项不适合在屏幕上。当我们运行它时,我们看到以下输出:
问题7-1:不支持32位系统

解决方法7-1:对不起,没有解决方法,作者本人就没有设计支持32位系统的情况,所以,请打包然后到64位机子上重新开始吧

对不起,大佬,打扰了,我这就改
所以,从现在开始,我们所有的工作都转移到64位系统上进行。
现象7-1:运行成功

我们看到有各种各样的非空条目,它们都映射到不同的3级表中。由于内核代码、内核堆栈、物理内存映射和引导信息都使用不同的内存区域,所以有这么多的区域。为了进一步遍历页面表并查看第3级表,我们可以将条目的映射框架再次转换为虚拟地址:
我们对main文件进行如下修改:

为了查看第2级和第1级表,我们对第3级和第2级条目重复这个过程。正如您可以想象的那样,这会变得非常冗长,因此我们不会在这里显示完整的代码。手动遍历页面表很有趣,因为它有助于理解CPU如何执行转换。但是,大多数时候我们只对给定虚拟地址的映射物理地址感兴趣,因此让我们在下面为此创建一个函数。
现象7-2:查看第3级页表的结果:

步骤8:地址翻译
为了将虚拟变为物理地址,我们必须遍历四级页面表,直到到达映射的帧为止。让我们创建一个执行此转换的函数,我们在memory.rs文件中加入如下代码

我们将函数转发到一个安全转换_addr_inside函数,以限制不安全的范围。正如我们前面所指出的,Rust将不安全FN的完整主体视为一个大的不安全块。通过调用私有安全函数,我们再次明确每个不安全操作。私有内部函数包含真正的实现,对memory文件进行修改:

我们不再重用Active_LEVEL_4_TABLE函数,而是再次从CR3寄存器读取级别4。我们这样做是因为它简化了这个原型实现。不要担心,我们会在一个时刻创造更好的解决方案。
virtaddr结构已经提供了将索引计算到四个级别的页表中的方法。我们将这些索引存储在一个小数组中,因为它允许我们使用循环遍历页表。在循环之外,我们还记得最后一次访问的帧以稍后计算物理地址。在迭代时,帧指向页表帧,并且在最后一次迭代之后,即跟随1级条目之后映射到映射的帧。
在循环中,我们再次使用physical_memory_offset将帧转换为页表引用。然后,我们读取当前页面表的条目,并使用pagetableEntry::Frame函数检索映射的帧。如果条目未映射到帧,则返回“无”。如果条目映射了一个庞大的2MIB或1GiB页,我们现在就会死机。
让我们通过翻译一些地址来测试我们的翻译功能,在main文件进行如下修改:

现在我们使用cargo xrun运行
现象8-1:

正如预期的那样,标识映射地址0xb 8000转换为相同的物理地址。代码页和堆栈页转换为一些任意的物理地址,这取决于引导加载程序如何为内核创建初始映射。值得注意的是,最后12位在翻译后始终保持不变,这是因为这些位是页面偏移量,而不是翻译的一部分。
由于每个物理地址都可以通过添加physical_memory_offset来访问,所以physical_memory_offset地址本身的转换应该指向物理地址0。但是,翻译失败了,因为映射使用了巨大的页面来提高效率,这在我们的实现中还不被支持。
步骤9:使用偏移值
将虚拟转换为物理地址是OS内核中的一个常见任务,因此,x86_64机箱为其提供了抽象。该实现已经支持了巨大的页面和一些其他页表功能,除了translate_addr之外,我们将在下面使用它,而不是向我们自己的实现添加巨大的页面支持。
抽象的基础是定义各种页表映射功能的两个特性:
·映射器特性在页面大小上是通用的,并提供了在页面上操作的功能。示例是translate_page,其将给定页转换为相同大小的帧,并且map_to在页表中创建新的映射。
·MapPerAllSize属性意味着实现或实现所有页面大小的映射器。此外,它还提供了与多个页面大小(如translate_addr或generaltranslate)一起工作的功能。
这些特性仅定义接口,它们不提供任何实现。X86_64机箱目前提供三种类型,可实现不同需求的特性。OffsetPagetable类型假定完整的物理内存在某个偏移处映射到虚拟地址空间。MappedPagetable是一个更灵活的位:它只要求将每个页表帧映射到可计算地址的虚拟地址空间。最后,可递归分页类型可用于通过递归页表访问页表框架。
在我们的情况下,bootloader将完整的物理内存映射到物理_memory_offset变量指定的虚拟地址,因此我们可以使用OffsetPagetable类型。为了初始化它,我们在memory文件中创建了一个新的init函数:

函数将physical_memory_offset用作参数,并返回一个具有“static”的新的OffsetPagetable实例。这意味着实例在内核的完整运行时保持有效。在功能实体中,我们首先调用Active_LEVEL_4_TABLE函数以检索对“Level4”页表的可变引用。然后,我们调用具有此引用的OffsetPagetable::New函数。作为第二个参数,新函数期望物理存储器的映射开始的虚拟地址在physical_memory_offset变量中给出。
Active_LEVEL_4_TABLE函数只能从Init函数调用,因为它可以在调用多次时容易导致别名可变引用,这可能会导致未定义的行为。因此,我们通过删除PUB说明符而使功能处于私有状态。
现在我们可以使用Mapper大小::translate_addr方法,而不是我们自己的memory::translate_addr函数。我们只需要在内核_main中更改几行:

为了使用它提供的translateaddr方法,我们需要导入MapperAllSizes特性。当我们现在运行它时,我们看到的翻译结果与以前一样,不同之处在于巨大的页面翻译现在也有效:
现象9-1:

正如预期的那样,0xb 8000的翻译以及代码和堆栈地址与我们自己的翻译函数保持不变。此外,我们现在看到虚拟地址物理_内存_偏移量被映射到物理地址0x0。
通过使用MempdPageTable类型的翻译功能,我们可以省去实现巨大页面支持的工作。我们还可以访问其他页面函数,如map_to,我们将在下一节中使用这些函数。
此时,我们不再需要memory::Transladdr函数,因此我们可以删除它,但这里,就先保留着吧。
步骤10:创建新映射
到目前为止,我们只查看页面表,而不修改任何内容。让我们通过为以前未映射的页面创建一个新的映射来改变这一点。
我们将使用Mapper特性的map_to函数来实现,所以让我们先看看这个函数。文档告诉我们,它需要四个参数:我们想要映射的页面、应该映射到的页面框架、一组用于页面表条目的标志以及一个Frameallocator。需要帧分配器,因为映射给定页可能需要创建额外的页表,这些表需要未使用的帧作为后备存储。
我们实现的第一步是创建一个新的CREATE_CALE_PARION函数,该函数将给定的虚拟页面映射到0xb 8000,这是VGA文本缓冲区的物理框架。我们选择该框架是因为它允许我们很容易地测试映射是否被正确创建:我们只需要写到新映射的页面,看看我们是否在屏幕上看到了写。CREATE_PLOPLE_映射函数如下所示,将memory文件进行如下修改:

除应该映射的页外,函数还需要对OffsetPagetable实例和frame_allocator的可变引用。frame_allocator参数使用Impl trait语法在实现帧分配器特性的所有类型上是通用的。该特性在PageSize特性上是通用的,可与标准4KiB页和大2mib/1GiB页一起工作。我们只希望创建4KiB映射,因此我们将通用参数设置为Size4KiB。
对于映射,我们设置了当前标志,因为所有有效条目和可写标志都需要它使映射的页可写。调用map_to是不安全的,因为它可以使用无效的参数来破坏内存安全,因此我们需要使用不安全的块。有关所有可能标志的列表,请参见上文的页表格式部分。
map_to函数可能失败,因此返回结果。因为这只是一些不需要鲁棒的示例代码,所以当出现错误时,我们只使用预期死机。在成功的情况下,函数返回MapPerFlush类型,该类型提供了一个简单的方法来从转换旁视缓冲区(TLB)中刷新新映射的页及其刷新方法。类似的结果,该类型使用#[must_use]属性在意外忘记使用该属性时发出警告。
虚拟帧分配器:要能够调用create_example_mapping,我们需要首先创建实现帧分配器特性的类型。如上所述,如果MAP_TO需要它们,则该特性负责为新的页表分配帧。
让我们从简单的案例入手,假设我们不需要创建新的页面表。对于这种情况,总是返回不满足的帧分配器就足够了。我们创建了这样的EmptyFrame分配器,用于测试我们的映射功能,在memory文件中加入如下修改

实现帧分配器是不安全的,因为实现者必须保证分配器仅产生未使用的帧。否则可能发生未定义的行为,例如,当两个虚拟页被映射到相同的物理帧时。我们的EmptyFrame分配器只返回None,因此这不是此情况下的问题。
选择虚拟页:现在我们有一个简单的帧分配器,我们可以通过我们的create_example_mapping函数。但是,分配器始终没有返回,因此如果不需要另外的页表框架来创建映射。要了解何时需要附加的页表帧,而何时不需要。让我们考虑一个示例:

图形显示左边的虚拟地址空间、右边的物理地址空间和中间的页表。页表存储在物理内存帧中,用虚线表示。虚拟地址空间包含地址0x803fe00000的单个映射页,以蓝色标记。要将此页面转换为其框架,CPU将遍历4级页面表,直到到达地址36 Kib的帧为止。
此外,该图形还以红色显示VGA文本缓冲区的物理框架。我们的目标是使用CREATESTORE_PARION函数将以前未映射的虚拟页面映射到此框架。因为我们的emptyFrameAllocator总是返回None,所以我们希望创建映射,这样就不需要从分配器中获得额外的帧。
这取决于我们为映射选择的虚拟页面。这张图显示了虚拟地址空间中的两个直白页,都是黄色标记的。一页位于地址0x803fdfd000,即映射页之前的3页(蓝色)。虽然第4级和第3级页表索引与蓝页相同,但第2级和第1级索引不同(见前一篇文章)。2级表中的不同索引意味着此页使用了不同的1级表。由于这个1级表还不存在,所以如果我们选择该页面作为示例映射,则需要创建该表,这将需要额外的未使用的物理框架。
相反,地址0x803fe02000的第二个候选页没有此问题,因为它使用的是与蓝页相同的级别1页表。因此,所有必需的页表都已经存在。
总之,创建新映射的难度取决于我们要映射的虚拟页。在最简单的情况下,该页的1级页面表已经存在,我们只需要写入一个条目。在最困难的情况下,该页位于内存区域中,因为没有级别3的存在,因此我们需要先创建新的级别3、级别2和级别1页表。
要使用EmptyFrame分配器调用我们的create_example_mapping函数,我们需要为已存在所有页表选择一个页面。要找到这样的页面,我们可以利用bootloader在虚拟地址空间的第一个兆字节中加载自己的事实。这意味着该区域中的所有页面都存在有效的级别1表。
因此,我们可以在此存储器区域中选择任何未使用的页,用于我们的示例映射,例如地址0处的页。通常,此页面应保持未使用状态,以保证取消引用空指针会导致页面错误,因此我们知道引导加载程序将其取消映射。
创建映射的操作:我们现在拥有调用我们的create_example_mapping函数的所有必需参数,因此让我们修改内核_main函数以将页面映射到虚拟地址0。由于我们将页面映射到VGA文本缓冲区的帧,因此我们应该能够随后通过它向屏幕写入。
实施方式如下,对main文件进行如下修改:

我们首先通过调用CREATEPLOPLE_PARION函数来创建地址为0的页面的映射,其中包含对映射程序和Frameallocator实例的可变引用。这将页面映射到VGA文本缓冲区框架,因此我们应该在屏幕上看到对它的任何写入。
然后,我们将页面转换为原始指针,并写入一个值以偏移400。我们不会写到页面的开头,因为下一个println会直接将VGA缓冲区的顶部移出屏幕。我们写入值0x_f 021_f 077_f 065_f04e,它表示字符串“New!”在白色背景下。正如我们在“VGA文本模式”文章中了解到的那样,对VGA缓冲区的写入应该是易失性的,因此我们使用了WITH_VERIAR方法。
当我们在QEMU中运行它时,我们看到以下输出:
现象10-1:

“new”在屏幕上的是我们对页面0的写,这意味着我们成功地在页面表中创建了一个新的映射。
因为负责地址为0的页面的第1级表已经存在所以创建该映射才有效。当我们试图映射一个页面时,因为它试图从emptyFrameAllocator为创建新的页面表分配帧,所以map_to函数失败了。当我们试图将0页映射为0而不是0时,我们可以看到这种情况,我们对main文件进行如下修改:

运行它时,出现以下错误消息的死机:

要映射不具有1级页表的页,我们需要创建适当的帧分配器。但是我们如何知道哪些帧是未使用的,并且有多少物理内存可用?
步骤11:分配帧
为了创建新的页表,我们需要创建适当的帧分配器。因为我们使用bootloader作为bootinfo结构的一部分传递的memory_map,我们对memory文件进行修改:

结构有两个字段:引导加载程序传递的内存映射的“静态引用”和一个next字段,用于跟踪分配器应返回的下一帧的数量。
正如我们在“引导信息”部分中解释的,内存映射是由BIOS/UEFI固件提供的。它只能在启动过程中很早就被查询,因此引导装载程序已经调用了我们各自的功能。存储器映射由包含起始地址、长度和类型(例如未使用、保留等等)。
INIT函数初始化具有给定内存映射的BOTINFOFRAME分配器。下一个字段用0初始化,并且对于每个帧分配将增加,以避免两次返回相同的帧。由于我们不知道内存映射的可用帧是否已经在其他地方使用,所以我们的init函数必须不安全,需要来自调用者的额外保证。
一种可使用的帧方法:在实现帧分配器特性之前,我们添加了一种将内存映射转换为可用帧迭代器的辅助方法,我们对memory文件进行修改:

该函数使用迭代器组合器方法将初始内存映射转换为具有可用物理帧的迭代器:
·首先,我们调用ITER方法将内存映射转换为内存区域迭代器。
·然后,我们使用Filter方法跳过任何保留的或其他不可用的区域。引导加载程序更新它创建的所有映射的内存映射,因此内核使用的帧(代码、数据或堆栈)或存储引导信息的帧已经标记为InUse或类似的。因此,我们可以确定可用的框架不会在其他地方使用。
·然后,我们使用map组合器和Rust的范围语法将内存区域的迭代器转换为地址范围的迭代器。
·下一步是最复杂的:我们通过INTO_ITER方法将每个范围转换为迭代器,然后使用Stepby选择每4096个地址。由于4096字节(=4 Kib)是页面大小,所以我们得到每个帧的开始地址。引导加载程序页面对齐所有可用的内存区域,因此这里不需要任何对齐或舍入代码。通过使用平面_map而不是map,我们得到一个Iterator<Item=U64>而不是Iterator<Item=Iterator<Item=U64>>。
·最后,我们将开始地址转换为PhysFrame类型,以构造所需的Iterator<Item=PhysFrame>。然后,我们使用这个迭代器创建并返回一个新的BootInfoFrameAllocator。
函数的返回类型使用IMPEL特性。通过这种方式,我们可以指定返回某些类型,该类型实现了Iterator特性,并使用了Iterator类型PhysFrame,但不需要命名具体的返回类型。这在这里很重要,因为我们不能命名Conrete类型,因为它依赖于不可命名的闭包类型。
实现FrameAllocator特性:现在我们可以实现FrameAllocator特性,我们对memory文件进行如下修改:

我们首先使用可用框架方法从内存映射中获得可用帧的迭代器。然后,我们使用Iterator::nth函数来获得带有索引Sel.Next的帧(从而跳过(Sel.Next-1)帧)。在返回该帧之前,我们将Self.Next增加一个,以便在下一个调用中返回下面的帧。
这个实现并不是最优的,因为它在每个分配中重新创建了usable_Frame分配程序。最好将迭代器直接存储为一个struct字段。这样我们就不需要第n个方法了,只需要在每个分配上调用Next。这种方法的问题是,目前不可能在struct字段中存储IMPEL特征类型。当命名存在类型完全实现时,它可能会在某一天起作用。
使用BootInfoFrameAllocator:我们现在可以修改kernel_Main函数,以传递一个BootInfoFrameAllocator实例,而不是emptyFrameAllocator,我们对main文件进行修改:

使用引导信息帧分配器,映射成功,我们看到黑白色的“new!”再次出现在屏幕上。在幕后,map_to方法以以下方式创建缺少的页表:
·从传递的framework_allocator分配未使用的框架。
·将框架为零以创建一个新的空页表。
·将较高级别表的条目映射到该框架。
·继续下一个表级别。
虽然我们的CREATESTESTORE_PARION函数只是一些示例代码,但我们现在能够为任意页面创建新的映射。这对于在以后的文章中分配内存或实现多线程至关重要。
现象11-1:运行成功

我们这次的实验主要改变的是lib文件、cargo.toml文件、main文件与memory文件,如下:
lib文件:

#![no_std]
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![feature(abi_x86_interrupt)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
pub mod gdt;
pub mod interrupts;
pub mod serial;
pub mod vga_buffer;
pub mod memory;
pub fn init() {gdt::init();interrupts::init_idt();unsafe { interrupts::PICS.lock().initialize() };x86_64::instructions::interrupts::enable();
}
pub fn test_runner(tests: &[&dyn Fn()]) {serial_println!("Running {} tests", tests.len());for test in tests {test();}exit_qemu(QemuExitCode::Success);
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {serial_println!("[failed]\n");serial_println!("Error: {}\n", info);exit_qemu(QemuExitCode::Failed);hlt_loop();
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {Success = 0x10,Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {use x86_64::instructions::port::Port;unsafe {let mut port = Port::new(0xf4);port.write(exit_code as u32);}
}
pub fn hlt_loop() -> ! {loop {x86_64::instructions::hlt();}
}
/// Entry point for `cargo xtest`
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {test_panic_handler(info)
}
#[cfg(test)]
use bootloader::{entry_point, BootInfo};#[cfg(test)]
entry_point!(test_kernel_main);
/// Entry point for `cargo xtest`
#[cfg(test)]
fn test_kernel_main(_boot_info: &'static BootInfo) -> ! {// like beforeinit();test_main();hlt_loop();
}

cargo.toml文件:

[package]
name = "junmo7_os"
version = "0.1.0"
authors = ["Philipp Oppermann <dev@phil-opp.com>"]
edition = "2018"
[[test]]
name = "should_panic"
harness = false
[[test]]
name = "stack_overflow"
harness = false
[dependencies]
bootloader = { version = "0.8.0", features = ["map_physical_memory"]}
volatile = "0.2.6"
spin = "0.5.2"
x86_64 = "0.7.5"
uart_16550 = "0.2.0"
pic8259_simple = "0.1.1"
pc-keyboard = "0.3.1"
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]
[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio","-display", "none"
]
test-success-exit-code = 33         # (0x10 << 1) | 1

main文件:

#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(junmo7_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use junmo7_os::println;
use core::panic::PanicInfo;
use bootloader::{BootInfo, entry_point};
entry_point!(kernel_main);
fn kernel_main(boot_info: &'static BootInfo) -> ! {use junmo7_os::memory;use junmo7_os::memory::BootInfoFrameAllocator;use x86_64::{structures::paging::Page, VirtAddr}; // new importprintln!("Hello World{}", "!");junmo7_os::init();let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);let mut mapper = unsafe { memory::init(phys_mem_offset) };//let mut frame_allocator = memory::EmptyFrameAllocator;let mut frame_allocator = unsafe { BootInfoFrameAllocator::init(&boot_info.memory_map) };let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000));memory::create_example_mapping(page, &mut mapper, &mut frame_allocator);let page_ptr: *mut u64 = page.start_address().as_mut_ptr();unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)};// as before#[cfg(test)]test_main();println!("It did not crash!");junmo7_os::hlt_loop();
}
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {println!("{}", info);junmo7_os::hlt_loop();
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {junmo7_os::test_panic_handler(info)
}

memory文件:

use bootloader::bootinfo::{MemoryMap, MemoryRegionType};
use x86_64::{structures::paging::{FrameAllocator, Mapper, OffsetPageTable, Page, PageTable, PhysFrame, Size4KiB,},PhysAddr, VirtAddr,
};
pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> {let level_4_table = active_level_4_table(physical_memory_offset);OffsetPageTable::new(level_4_table, physical_memory_offset)
}
unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) -> &'static mut PageTable {use x86_64::registers::control::Cr3;let (level_4_table_frame, _) = Cr3::read();let phys = level_4_table_frame.start_address();let virt = physical_memory_offset + phys.as_u64();let page_table_ptr: *mut PageTable = virt.as_mut_ptr();&mut *page_table_ptr // unsafe
}
pub fn create_example_mapping(page: Page,mapper: &mut OffsetPageTable,frame_allocator: &mut impl FrameAllocator<Size4KiB>,
) {use x86_64::structures::paging::PageTableFlags as Flags;let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000));let flags = Flags::PRESENT | Flags::WRITABLE;let map_to_result = unsafe { mapper.map_to(page, frame, flags, frame_allocator) };map_to_result.expect("map_to failed").flush();
}
pub struct EmptyFrameAllocator;
unsafe impl FrameAllocator<Size4KiB> for EmptyFrameAllocator {fn allocate_frame(&mut self) -> Option<PhysFrame> {None}
}
pub struct BootInfoFrameAllocator {memory_map: &'static MemoryMap,next: usize,
}
impl BootInfoFrameAllocator {pub unsafe fn init(memory_map: &'static MemoryMap) -> Self {BootInfoFrameAllocator {memory_map,next: 0,}}fn usable_frames(&self) -> impl Iterator<Item = PhysFrame> {let regions = self.memory_map.iter();let usable_regions = regions.filter(|r| r.region_type == MemoryRegionType::Usable);let addr_ranges = usable_regions.map(|r| r.range.start_addr()..r.range.end_addr());let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096));frame_addresses.map(|addr| PhysFrame::containing_address(PhysAddr::new(addr)))}
}
unsafe impl FrameAllocator<Size4KiB> for BootInfoFrameAllocator {fn allocate_frame(&mut self) -> Option<PhysFrame> {let frame = self.usable_frames().nth(self.next);self.next += 1;frame}
}

内容(二):打印 4 级页表的信息。

理论上我们上文已经实现了如何打印四级页表的信息,但是,如果我们仅仅是将我们的main函数修改会原来的样子,将会产生错误:

问题1-1:我们已经将我们的active_level_4_table函数修改为了私有类型,我们没有办法在外面进行直接的使用:

想要解决这个问题可以从不同的角度进行解决,比方说将我们的active_level_4_table函数重新变回公有类型,或者重新设计一个公有函数用来调用active_level_4_table函数
解决方法1-1:我们将我们的active_level_4_table函数重新变回私有类型

现象1-1:成功打印四级页表信息

解决方法1-2:我们也可以通过新建一个pub函数用来调用active_level_4_table函数来打印我们需要的四级页表:

问题1-2:我们调用unsafe函数是不允许的

解决方法1-3:我们将我们的函数修改为unsafe状态就可以了

问题1-3:我们没有在main函数中声明我们的ac_4_table函数,所以无法调用:

解决方法1-4:在main中声明即可

问题1-4:我们所写的函数没有办法调用内部的iter函数

因为我们的active_level_4_table函数原本返回了一个静态变量,而我们现在新建的没有返回,所以我们需要修改我们的函数:
解决方法1-5:修改函数

现象1-2:运行成功:

最终我们仅仅是对main文件和memory文件进行了修改,如下:
main文件:

#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(junmo7_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use junmo7_os::println;
use core::panic::PanicInfo;
use bootloader::{BootInfo, entry_point};
entry_point!(kernel_main);
//打印4级页表
fn kernel_main(boot_info: &'static BootInfo) -> ! {use junmo7_os::memory::active_level_4_table;//use junmo7_os::memory::ac_4_table;use x86_64::VirtAddr;println!("Hello World{}", "!");junmo7_os::init();let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);let l4_table = unsafe { active_level_4_table(phys_mem_offset) };//let l4_table = unsafe { ac_4_table(phys_mem_offset) };for (i, entry) in l4_table.iter().enumerate() {//打印4级别页表if !entry.is_unused() {println!("L4 Entry {}: {:?}", i, entry);}}// as before#[cfg(test)]test_main();println!("It did not crash!");junmo7_os::hlt_loop();
}
/// This function is called on panic.
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {println!("{}", info);junmo7_os::hlt_loop();
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {junmo7_os::test_panic_handler(info)
}

memory文件:

use bootloader::bootinfo::{MemoryMap, MemoryRegionType};
use x86_64::{structures::paging::{FrameAllocator, Mapper, OffsetPageTable, Page, PageTable, PhysFrame, Size4KiB,},PhysAddr, VirtAddr,
};
pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> {let level_4_table = active_level_4_table(physical_memory_offset);OffsetPageTable::new(level_4_table, physical_memory_offset)
}
//****1.only need one pub to let this turn to public
//****2.new build one public fn to use active_level_4_table
unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) -> &'static mut PageTable {use x86_64::registers::control::Cr3;let (level_4_table_frame, _) = Cr3::read();let phys = level_4_table_frame.start_address();let virt = physical_memory_offset + phys.as_u64();let page_table_ptr: *mut PageTable = virt.as_mut_ptr();&mut *page_table_ptr // unsafe
}
/*
pub unsafe fn ac_4_table(a: VirtAddr) -> &'static mut PageTable {let s=active_level_4_table(a);&mut *s
}*/
pub fn create_example_mapping(page: Page,mapper: &mut OffsetPageTable,frame_allocator: &mut impl FrameAllocator<Size4KiB>,) {use x86_64::structures::paging::PageTableFlags as Flags;let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000));let flags = Flags::PRESENT | Flags::WRITABLE;let map_to_result = unsafe { mapper.map_to(page, frame, flags, frame_allocator) };map_to_result.expect("map_to failed").flush();
}
pub struct EmptyFrameAllocator;
unsafe impl FrameAllocator<Size4KiB> for EmptyFrameAllocator {fn allocate_frame(&mut self) -> Option<PhysFrame> {None}
}
pub struct BootInfoFrameAllocator {memory_map: &'static MemoryMap,next: usize,
}
impl BootInfoFrameAllocator {pub unsafe fn init(memory_map: &'static MemoryMap) -> Self {BootInfoFrameAllocator {memory_map,next: 0,}}fn usable_frames(&self) -> impl Iterator<Item = PhysFrame> {let regions = self.memory_map.iter();let usable_regions = regions.filter(|r| r.region_type == MemoryRegionType::Usable);let addr_ranges = usable_regions.map(|r| r.range.start_addr()..r.range.end_addr());let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096));frame_addresses.map(|addr| PhysFrame::containing_address(PhysAddr::new(addr)))}
}
unsafe impl FrameAllocator<Size4KiB> for BootInfoFrameAllocator {fn allocate_frame(&mut self) -> Option<PhysFrame> {let frame = self.usable_frames().nth(self.next);self.next += 1;frame}
}

内容(三):自行补充访问 3 级、2 级和 1 级页表信息的代码,并打印对应的页表信息。

和上文同样,我们也已经有了一个打印3级页表信息的代码,现在先进行修复;

运行试试,发现是成功的
现象1-1:运行成功

既然如此,我们仿照此便可以打印2级页表和1级页表的值
首先是2级页表,将main文件修改如下

结果很震撼,会持续运行很长时间,整个界面一直在滚动,直到最后
现象1-2:运行成功

从打印的文字来看,的确是打印出来了从4级到2级页表的内容
现在尝试仅打印1级页表的内容
将main文件进行如下修改:

现象1-3:很震撼,运行成功

由于我们仅仅是对main文件进行了修改(在内容二的基础上),所以在这里仅仅展示main文件的代码:
main文件:

#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(junmo7_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use junmo7_os::println;
use core::panic::PanicInfo;
use bootloader::{BootInfo, entry_point};
entry_point!(kernel_main);
//打印4级页表
fn kernel_main(boot_info: &'static BootInfo) -> ! {//use junmo7_os::memory::active_level_4_table;use junmo7_os::memory::ac_4_table;use x86_64::VirtAddr;println!("Hello World{}", "!");junmo7_os::init();let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);//let l4_table = unsafe { active_level_4_table(phys_mem_offset) };let l4_table = unsafe { ac_4_table(phys_mem_offset) };for (i, entry) in l4_table.iter().enumerate() {/*//打印4级别页表if !entry.is_unused() {println!("L4 Entry {}: {:?}", i, entry);}*//*//打印4-3级页表use x86_64::structures::paging::PageTable;if !entry.is_unused() {println!("L4 Entry {}: {:?}", i, entry);// get the physical address from the entry and convert itlet phys = entry.frame().unwrap().start_address();let virt = phys.as_u64() + boot_info.physical_memory_offset;let ptr = VirtAddr::new(virt).as_mut_ptr();let l3_table: &PageTable = unsafe { &*ptr };// print non-empty entries of the level 3 tablefor (i, entry) in l3_table.iter().enumerate() {if !entry.is_unused() {println!("  L3 Entry {}: {:?}", i, entry);}}}*//*//4-2use x86_64::structures::paging::PageTable;if !entry.is_unused() {println!("L4 Entry {}: {:?}", i, entry);// get the physical address from the entry and convert itlet phys = entry.frame().unwrap().start_address();let virt = phys.as_u64() + boot_info.physical_memory_offset;let ptr = VirtAddr::new(virt).as_mut_ptr();let l3_table: &PageTable = unsafe { &*ptr };// print non-empty entries of the level 3 tablefor (i, entry) in l3_table.iter().enumerate() {if !entry.is_unused() {println!("  L3 Entry {}: {:?}", i, entry);// get the physical address from the entry and convert itlet phys2 = entry.frame().unwrap().start_address();let virt2 = phys2.as_u64() + boot_info.physical_memory_offset;let ptr2 = VirtAddr::new(virt2).as_mut_ptr();let l2_table: &PageTable = unsafe { &*ptr2 };for (j, entry) in l2_table.iter().enumerate() {if !entry.is_unused() {println!("  L2 Entry {}: {:?}", j, entry);}}}}}*///1use x86_64::structures::paging::PageTable;if !entry.is_unused() {//println!("L4 Entry {}: {:?}", i, entry);// get the physical address from the entry and convert itlet phys = entry.frame().unwrap().start_address();let virt = phys.as_u64() + boot_info.physical_memory_offset;let ptr = VirtAddr::new(virt).as_mut_ptr();let l3_table: &PageTable = unsafe { &*ptr };// print non-empty entries of the level 3 tablefor (i, entry) in l3_table.iter().enumerate() {if !entry.is_unused() {//println!("  L3 Entry {}: {:?}", i, entry);// get the physical address from the entry and convert itlet phys2 = entry.frame().unwrap().start_address();let virt2 = phys2.as_u64() + boot_info.physical_memory_offset;let ptr2 = VirtAddr::new(virt2).as_mut_ptr();let l2_table: &PageTable = unsafe { &*ptr2 };for (j, entry) in l2_table.iter().enumerate() {if !entry.is_unused() {//println!("  L2 Entry {}: {:?}", j, entry);// get the physical address from the entry and convert itlet phys1 = entry.frame().unwrap().start_address();let virt1 = phys1.as_u64() + boot_info.physical_memory_offset;let ptr1 = VirtAddr::new(virt1).as_mut_ptr();let l1_table: &PageTable = unsafe { &*ptr1 };for (o, entry) in l1_table.iter().enumerate() {if !entry.is_unused() {println!("  L1 Entry {}: {:?}", o, entry);}}}}}}}}// as before#[cfg(test)]test_main();println!("It did not crash!");junmo7_os::hlt_loop();
}
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {println!("{}", info);junmo7_os::hlt_loop();
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {junmo7_os::test_panic_handler(info)
}

内容(四):实现虚拟地址到物理地址的转换过程,并给出若干地址转换实例。

这个实际上我们在上文中已经完成了,这里重新来一遍
步骤1:地址翻译
为了将虚拟变为物理地址,我们必须遍历四级页面表,直到到达映射的帧为止。让我们创建一个执行此转换的函数,我们在memory.rs文件中加入如下代码

我们将函数转发到一个安全转换_addr_inside函数,以限制不安全的范围。正如我们前面所指出的,Rust将不安全FN的完整主体视为一个大的不安全块。通过调用私有安全函数,我们再次明确每个不安全操作。私有内部函数包含真正的实现,对memory文件进行修改:

我们不再重用Active_LEVEL_4_TABLE函数,而是再次从CR3寄存器读取级别4。我们这样做是因为它简化了这个原型实现。不要担心,我们会在一个时刻创造更好的解决方案。
virtaddr结构已经提供了将索引计算到四个级别的页表中的方法。我们将这些索引存储在一个小数组中,因为它允许我们使用循环遍历页表。在循环之外,我们还记得最后一次访问的帧以稍后计算物理地址。在迭代时,帧指向页表帧,并且在最后一次迭代之后,即跟随1级条目之后映射到映射的帧。
在循环中,我们再次使用物理_memory_offset将帧转换为页表引用。然后,我们读取当前页面表的条目,并使用pagetableEntry::Frame函数检索映射的帧。如果条目未映射到帧,则返回“无”。如果条目映射了一个庞大的2MIB或1GiB页,我们现在就会死机。
让我们通过翻译一些地址来测试我们的翻译功能,在main文件进行如下修改:

现在我们使用cargo xrun运行
现象1-1:

正如预期的那样,标识映射地址0xb 8000转换为相同的物理地址。代码页和堆栈页转换为一些任意的物理地址,这取决于引导加载程序如何为内核创建初始映射。值得注意的是,最后12位在翻译后始终保持不变,这是因为这些位是页面偏移量,而不是翻译的一部分。
由于每个物理地址都可以通过添加物理_内存_偏移量来访问,所以物理_内存_偏移地址本身的转换应该指向物理地址0。但是,翻译失败了,因为映射使用了巨大的页面来提高效率,这在我们的实现中还不被支持。

内容(五):[可选] 参照 https://os.phil-opp.com/paging-implementation/,使地址转换支持大页(huge pages)。

这个实际上我们在上文中已经完成了,这里重新来一遍
步骤1:使用偏移值
将虚拟转换为物理地址是OS内核中的一个常见任务,因此,x86_64机箱为其提供了抽象。该实现已经支持了巨大的页面和一些其他页表功能,除了translate_addr之外,我们将在下面使用它,而不是向我们自己的实现添加巨大的页面支持。
抽象的基础是定义各种页表映射功能的两个特性:
·映射器特性在页面大小上是通用的,并提供了在页面上操作的功能。示例是translate_page,其将给定页转换为相同大小的帧,并且map_to在页表中创建新的映射。
·MapPerAllSize属性意味着实现或实现所有页面大小的映射器。此外,它还提供了与多个页面大小(如translate_addr或generaltranslate)一起工作的功能。
这些特性仅定义接口,它们不提供任何实现。X86_64机箱目前提供三种类型,可实现不同需求的特性。OffsetPagetable类型假定完整的物理内存在某个偏移处映射到虚拟地址空间。MappedPagetable是一个更灵活的位:它只要求将每个页表帧映射到可计算地址的虚拟地址空间。最后,可递归分页类型可用于通过递归页表访问页表框架。
在我们的情况下,bootloader将完整的物理内存映射到物理_memory_offset变量指定的虚拟地址,因此我们可以使用OffsetPagetable类型。为了初始化它,我们在memory文件中创建了一个新的init函数:

函数将physical_memory_offset用作参数,并返回一个具有“static”的新的OffsetPagetable实例。这意味着实例在内核的完整运行时保持有效。在功能实体中,我们首先调用Active_LEVEL_4_TABLE函数以检索对“Level4”页表的可变引用。然后,我们调用具有此引用的OffsetPagetable::New函数。作为第二个参数,新函数期望物理存储器的映射开始的虚拟地址在physical_memory_offset变量中给出。
Active_LEVEL_4_TABLE函数只能从Init函数调用,因为它可以在调用多次时容易导致别名可变引用,这可能会导致未定义的行为。因此,我们通过删除PUB说明符而使功能处于私有状态。
现在我们可以使用Mapper大小::translate_addr方法,而不是我们自己的memory::translate_addr函数。我们只需要在内核_main中更改几行:

为了使用它提供的translateaddr方法,我们需要导入MapperAllSizes特性。当我们现在运行它时,我们看到的翻译结果与以前一样,不同之处在于巨大的页面翻译现在也有效:
现象1-1:

正如预期的那样,0xb 8000的翻译以及代码和堆栈地址与我们自己的翻译函数保持不变。此外,我们现在看到虚拟地址物理_内存_偏移量被映射到物理地址0x0。
通过使用MempdPageTable类型的翻译功能,我们可以省去实现巨大页面支持的工作。我们还可以访问其他页面函数,如map_to,我们将在下一节中使用这些函数。
此时,我们不再需要memory::Transladdr函数,因此我们可以删除它,但这里,就先保留着吧。

内容(六):Exercise A-2 [可选] 参照 https://os.phil-opp.com/paging-implementation/,实现新 的虚拟页映射(将其映射到地址 0xb8000)。

这个在上文中已经实现了,在这里重新进行一下:
步骤1:创建新映射
到目前为止,我们只查看页面表,而不修改任何内容。让我们通过为以前未映射的页面创建一个新的映射来改变这一点。
我们将使用Mapper特性的map_to函数来实现,所以让我们先看看这个函数。文档告诉我们,它需要四个参数:我们想要映射的页面、应该映射到的页面框架、一组用于页面表条目的标志以及一个Frameallocator。需要帧分配器,因为映射给定页可能需要创建额外的页表,这些表需要未使用的帧作为后备存储。
我们实现的第一步是创建一个新的CREATE_CALE_PARION函数,该函数将给定的虚拟页面映射到0xb 8000,这是VGA文本缓冲区的物理框架。我们选择该框架是因为它允许我们很容易地测试映射是否被正确创建:我们只需要写到新映射的页面,看看我们是否在屏幕上看到了写。CREATE_PLOPLE_映射函数如下所示,将memory文件进行如下修改:

除应该映射的页外,函数还需要对OffsetPagetable实例和frame_allocator的可变引用。frame_allocator参数使用Impl trait语法在实现帧分配器特性的所有类型上是通用的。该特性在PageSize特性上是通用的,可与标准4KiB页和大2mib/1GiB页一起工作。我们只希望创建4KiB映射,因此我们将通用参数设置为Size4KiB。
对于映射,我们设置了当前标志,因为所有有效条目和可写标志都需要它使映射的页可写。调用map_to是不安全的,因为它可以使用无效的参数来破坏内存安全,因此我们需要使用不安全的块。有关所有可能标志的列表,请参见上文的页表格式部分。
map_to函数可能失败,因此返回结果。因为这只是一些不需要鲁棒的示例代码,所以当出现错误时,我们只使用预期死机。在成功的情况下,函数返回MapPerFlush类型,该类型提供了一个简单的方法来从转换旁视缓冲区(TLB)中刷新新映射的页及其刷新方法。类似的结果,该类型使用#[must_use]属性在意外忘记使用该属性时发出警告。
虚拟帧分配器:要能够调用create_example_mapping,我们需要首先创建实现帧分配器特性的类型。如上所述,如果MAP_TO需要它们,则该特性负责为新的页表分配帧。
让我们从简单的案例入手,假设我们不需要创建新的页面表。对于这种情况,总是返回不满足的帧分配器就足够了。我们创建了这样的EmptyFrame分配器,用于测试我们的映射功能,在memory文件中加入如下修改

实现帧分配器是不安全的,因为实现者必须保证分配器仅产生未使用的帧。否则可能发生未定义的行为,例如,当两个虚拟页被映射到相同的物理帧时。我们的EmptyFrame分配器只返回None,因此这不是此情况下的问题。
选择虚拟页:现在我们有一个简单的帧分配器,我们可以通过我们的create_example_mapping函数。但是,分配器始终没有返回,因此如果不需要另外的页表框架来创建映射。要了解何时需要附加的页表帧,而何时不需要。让我们考虑一个示例:

图形显示左边的虚拟地址空间、右边的物理地址空间和中间的页表。页表存储在物理内存帧中,用虚线表示。虚拟地址空间包含地址0x803fe00000的单个映射页,以蓝色标记。要将此页面转换为其框架,CPU将遍历4级页面表,直到到达地址36 Kib的帧为止。
此外,该图形还以红色显示VGA文本缓冲区的物理框架。我们的目标是使用CREATESTORE_PARION函数将以前未映射的虚拟页面映射到此框架。因为我们的emptyFrameAllocator总是返回None,所以我们希望创建映射,这样就不需要从分配器中获得额外的帧。
这取决于我们为映射选择的虚拟页面。这张图显示了虚拟地址空间中的两个直白页,都是黄色标记的。一页位于地址0x803fdfd000,即映射页之前的3页(蓝色)。虽然第4级和第3级页表索引与蓝页相同,但第2级和第1级索引不同(见前一篇文章)。2级表中的不同索引意味着此页使用了不同的1级表。由于这个1级表还不存在,所以如果我们选择该页面作为示例映射,则需要创建该表,这将需要额外的未使用的物理框架。
相反,地址0x803fe02000的第二个候选页没有此问题,因为它使用的是与蓝页相同的级别1页表。因此,所有必需的页表都已经存在。
总之,创建新映射的难度取决于我们要映射的虚拟页。在最简单的情况下,该页的1级页面表已经存在,我们只需要写入一个条目。在最困难的情况下,该页位于内存区域中,因为没有级别3的存在,因此我们需要先创建新的级别3、级别2和级别1页表。
要使用EmptyFrame分配器调用我们的create_example_mapping函数,我们需要为已存在所有页表选择一个页面。要找到这样的页面,我们可以利用bootloader在虚拟地址空间的第一个兆字节中加载自己的事实。这意味着该区域中的所有页面都存在有效的级别1表。
因此,我们可以在此存储器区域中选择任何未使用的页,用于我们的示例映射,例如地址0处的页。通常,此页面应保持未使用状态,以保证取消引用空指针会导致页面错误,因此我们知道引导加载程序将其取消映射。
创建映射的操作:我们现在拥有调用我们的create_example_mapping函数的所有必需参数,因此让我们修改内核_main函数以将页面映射到虚拟地址0。由于我们将页面映射到VGA文本缓冲区的帧,因此我们应该能够随后通过它向屏幕写入。
实施方式如下,对main文件进行如下修改:

我们首先通过调用CREATEPLOPLE_PARION函数来创建地址为0的页面的映射,其中包含对映射程序和Frameallocator实例的可变引用。这将页面映射到VGA文本缓冲区框架,因此我们应该在屏幕上看到对它的任何写入。
然后,我们将页面转换为原始指针,并写入一个值以偏移400。我们不会写到页面的开头,因为下一个println会直接将VGA缓冲区的顶部移出屏幕。我们写入值0x_f 021_f 077_f 065_f04e,它表示字符串“New!”在白色背景下。正如我们在“VGA文本模式”文章中了解到的那样,对VGA缓冲区的写入应该是易失性的,因此我们使用了WITH_VERIAR方法。
当我们在QEMU中运行它时,我们看到以下输出:
现象1-1:

“new”在屏幕上的是我们对页面0的写,这意味着我们成功地在页面表中创建了一个新的映射。
因为负责地址为0的页面的第1级表已经存在所以创建该映射才有效。当我们试图映射一个页面时,因为它试图从emptyFrameAllocator为创建新的页面表分配帧,所以map_to函数失败了。当我们试图将0页映射为0而不是0时,我们可以看到这种情况,我们对main文件进行如下修改:

运行它时,出现以下错误消息的死机:

要映射不具有1级页表的页,我们需要创建适当的帧分配器。但是我们如何知道哪些帧是未使用的,并且有多少物理内存可用?

步骤2:分配帧
为了创建新的页表,我们需要创建适当的帧分配器。因为我们使用bootloader作为bootinfo结构的一部分传递的memory_map,我们对memory文件进行修改:

结构有两个字段:引导加载程序传递的内存映射的“静态引用”和一个next字段,用于跟踪分配器应返回的下一帧的数量。
正如我们在“引导信息”部分中解释的,内存映射是由BIOS/UEFI固件提供的。它只能在启动过程中很早就被查询,因此引导装载程序已经调用了我们各自的功能。存储器映射由包含起始地址、长度和类型(例如未使用、保留等等)。
INIT函数初始化具有给定内存映射的BOTINFOFRAME分配器。下一个字段用0初始化,并且对于每个帧分配将增加,以避免两次返回相同的帧。由于我们不知道内存映射的可用帧是否已经在其他地方使用,所以我们的init函数必须不安全,需要来自调用者的额外保证。
一种可使用的帧方法:在实现帧分配器特性之前,我们添加了一种将内存映射转换为可用帧迭代器的辅助方法,我们对memory文件进行修改:

该函数使用迭代器组合器方法将初始内存映射转换为具有可用物理帧的迭代器:
·首先,我们调用ITER方法将内存映射转换为内存区域迭代器。
·然后,我们使用Filter方法跳过任何保留的或其他不可用的区域。引导加载程序更新它创建的所有映射的内存映射,因此内核使用的帧(代码、数据或堆栈)或存储引导信息的帧已经标记为InUse或类似的。因此,我们可以确定可用的框架不会在其他地方使用。
·然后,我们使用map组合器和Rust的范围语法将内存区域的迭代器转换为地址范围的迭代器。
·下一步是最复杂的:我们通过INTO_ITER方法将每个范围转换为迭代器,然后使用Stepby选择每4096个地址。由于4096字节(=4 Kib)是页面大小,所以我们得到每个帧的开始地址。引导加载程序页面对齐所有可用的内存区域,因此这里不需要任何对齐或舍入代码。通过使用平面_map而不是map,我们得到一个Iterator<Item=U64>而不是Iterator<Item=Iterator<Item=U64>>。
·最后,我们将开始地址转换为PhysFrame类型,以构造所需的Iterator<Item=PhysFrame>。然后,我们使用这个迭代器创建并返回一个新的BootInfoFrameAllocator。
函数的返回类型使用IMPEL特性。通过这种方式,我们可以指定返回某些类型,该类型实现了Iterator特性,并使用了Iterator类型PhysFrame,但不需要命名具体的返回类型。这在这里很重要,因为我们不能命名Conrete类型,因为它依赖于不可命名的闭包类型。
实现FrameAllocator特性:现在我们可以实现FrameAllocator特性,我们对memory文件进行如下修改:

我们首先使用可用框架方法从内存映射中获得可用帧的迭代器。然后,我们使用Iterator::nth函数来获得带有索引Sel.Next的帧(从而跳过(Sel.Next-1)帧)。在返回该帧之前,我们将Self.Next增加一个,以便在下一个调用中返回下面的帧。
这个实现并不是最优的,因为它在每个分配中重新创建了usable_Frame分配程序。最好将迭代器直接存储为一个struct字段。这样我们就不需要第n个方法了,只需要在每个分配上调用Next。这种方法的问题是,目前不可能在struct字段中存储IMPEL特征类型。当命名存在类型完全实现时,它可能会在某一天起作用。
使用BootInfoFrameAllocator:我们现在可以修改kernel_Main函数,以传递一个BootInfoFrameAllocator实例,而不是emptyFrameAllocator,我们对main文件进行修改:

使用引导信息帧分配器,映射成功,我们看到黑白色的“new!”再次出现在屏幕上。在幕后,map_to方法以以下方式创建缺少的页表:
·从传递的framework_allocator分配未使用的框架。
·将框架为零以创建一个新的空页表。
·将较高级别表的条目映射到该框架。
·继续下一个表级别。
虽然我们的CREATESTESTORE_PARION函数只是一些示例代码,但我们现在能够为任意页面创建新的映射。这对于在以后的文章中分配内存或实现多线程至关重要。
现象2-1:运行成功

三、实验重难点

内容(一):

分段分页:
分段技术和分页技术。前者使用可变大小的内存区域,存在外部碎片,后者使用固定大小的页面,允许对访问权限进行更细粒度的控制。
分页将页的映射信息存储在具有一个或多个级别的页表中。x86_64体系结构使用4级页表,页面大小为4 KiB。硬件自动地遍历页面表,并将结果翻译缓存到翻译旁路缓冲区(TLB)中。此缓冲区不会透明地更新,需要在页表更改时手动刷新。
我们了解到内核已经运行在分页之上,非法的内存访问会导致页面错误异常。我们试图访问当前活动的页面表,但我们无法访问,因为CR3寄存器存储的物理地址,我们不能直接从我们的内核访问。
访问页表:
访问页表物理框架的不同技术,包括标识映射、完整物理内存的映射、临时映射和递归页表。我们选择映射完整的物理内存,因为它简单、可移植和强大。
在没有页表访问的情况下,我们无法从内核映射物理内存,因此我们需要引导加载程序的支持。引导装载机箱支持通过可选的货物特性创建所需的映射。它以&Boot Info参数的形式将所需的信息传递给我们的内核,并将其传递给我们的入口点函数。
对于我们的实现,我们首先手动遍历页面表来实现翻译函数,然后使用x86_64板条箱的MapapPage Table类型。我们还学习了如何在页面表中创建新的映射,以及如何在引导加载程序传递的内存映射之上创建必要的FrameAllocator。

内容(二)&内容(三):

现在,通过创建一个Active_level_4_table函数,返回对Active Level 4页表的引用,我们可以从那里继续,我们在我们新建的memory文件中加入如下代码.
首先,我们从CR3寄存器读取活动级别4表的物理框架。然后,我们获取其物理开始地址,将其转换为U64,并将其添加到physical_memory_offset中,以获得页表框架映射的虚拟地址。最后,通过as_mut_ptr方法将虚拟地址转换为mutPageTable原始指针,然后不安全地从它创建一个&mutPageTable引用。我们创建一个&mut引用,而不是一个&引用,因为我们将在后面的文章中修改页面表。
这里我们不需要使用不安全块,因为Rust将不安全fn的完整主体视为一个大的不安全块。这使得我们的代码更加危险,因为我们可能会不小心在前面的行中引入一个不安全的操作。这也使得识别不安全操作变得更加困难。有一个RFC来改变这种行为。
现在我们可以使用这个函数打印4级表的条目:
首先,我们将BootInfo结构的physical_memory_offset转换为VirtAddr,并将其传递给Active_level_4_table函数。然后,我们使用ITER函数对页面表条目进行迭代,并使用枚举组合器向每个元素添加索引I。我们只打印非空条目,因为所有512项不适合在屏幕上。
为了查看第2级和第1级表,我们对第3级和第2级条目重复这个过程。正如您可以想象的那样,这会变得非常冗长,因此我们不会在这里显示完整的代码。手动遍历页面表很有趣,因为它有助于理解CPU如何执行转换。但是,大多数时候我们只对给定虚拟地址的映射物理地址感兴趣,因此让我们在下面为此创建一个函数。
为了将虚拟变为物理地址,我们必须遍历四级页面表,直到到达映射的帧为止。让我们创建一个执行此转换的函数,我们将函数转发到一个安全translate_addr_inside函数,以限制不安全的范围。正如我们前面所指出的,Rust将不安全FN的完整主体视为一个大的不安全块。通过调用私有安全函数,我们再次明确每个不安全操作。
我为了能够打印4级页表,重新使用了active_level_4_table函数用来返回对Active Level 4页表的引用,并将BootInfo结构的physical_memory_offset转换为VirtAddr,并使用ITER函数进行迭代,并使用枚举组合器向每个元素添加索引I,找到所有的非空条目,并在所有的非空条目中,重新找到新的下一级页表的条目,即第3级页表,然后在进行迭代,同样适用枚举组合器向每个元素添加索引j,这个时候如果进行打印,打印出来的就是所有的第3级非空条目。至于第2级和第1级同样如此,继续进行枚举组合器的迭代即可。这里面涉及到了很多的变量,比方说phys、virt和ptr,分别代表了不同的意思:
就我个人而言,pyhs存储的是我们通过进行了active_level_4_table后所取出来的第4级页表的开始位置物理地址;而virt存储的是进行了对我们所取出来的pyhs,即物理地址的转换,转换成了u64格式后的地址,并将其添加到physical_memory_offset中,以获得页表框架映射的虚拟地址,这个时候就已经类似一个大型的一维数组了;ptr存储的是通过as_mut_ptr方法将虚拟地址转换为*mutPageTable原始指针,即数组的头结点的地址;而至于我们最后的一个变量l3_table的作用就是不安全地为它创建一个&mutPageTable引用,至于为什么不安全,个人理解是直接访问了内存。

内容(四):

我们不再重用Active_LEVEL_4_TABLE函数,而是再次从CR3寄存器读取级别4。我们这样做是因为它简化了这个原型实现。不要担心,我们会在一个时刻创造更好的解决方案。
virtaddr结构已经提供了将索引计算到四个级别的页表中的方法。我们将这些索引存储在一个小数组中,因为它允许我们使用循环遍历页表。
在循环之外,我们还记得最后一次访问的帧以稍后计算物理地址。在迭代时,帧指向页表帧,并且在最后一次迭代之后,即跟随1级条目之后映射到映射的帧。
在循环中,我们再次使用物理_memory_offset将帧转换为页表引用。然后,我们读取当前页面表的条目,并使用pagetableEntry::Frame函数检索映射的帧。如果条目未映射到帧,则返回“无”。如果条目映射了一个庞大的2MIB或1GiB页,我们现在就会死机。
正如预期的那样,标识映射地址0xb 8000转换为相同的物理地址。代码页和堆栈页转换为一些任意的物理地址,这取决于引导加载程序如何为内核创建初始映射。值得注意的是,最后12位在翻译后始终保持不变,这是因为这些位是页面偏移量,而不是翻译的一部分。
由于每个物理地址都可以通过添加physical_memory_offset来访问,所以physical_memory_offset地址本身的转换应该指向物理地址0。但是,翻译失败了,因为映射使用了巨大的页面来提高效率,这在我们的实现中还不被支持。

内容(五):

x86_64机箱为将虚拟转换为物理地址是OS内核提供了抽象。该实现已经支持了巨大的页面和一些其他页表功能,除了translate_addr之外,我们将在下面使用它,而不是向我们自己的实现添加巨大的页面支持。抽象的基础是定义各种页表映射功能的两个特性:
·映射器特性在页面大小上是通用的,并提供了在页面上操作的功能。示例是translate_page,其将给定页转换为相同大小的帧,并且map_to在页表中创建新的映射。
·MapPerAllSize属性意味着实现或实现所有页面大小的映射器。此外,它还提供了与多个页面大小(如translate_addr或generaltranslate)一起工作的功能。
这些特性仅定义接口,它们不提供任何实现。X86_64机箱目前提供三种类型,可实现不同需求的特性。OffsetPagetable类型假定完整的物理内存在某个偏移处映射到虚拟地址空间。MappedPagetable是一个更灵活的位:它只要求将每个页表帧映射到可计算地址的虚拟地址空间。最后,可递归分页类型可用于通过递归页表访问页表框架。
在我们的情况下,bootloader将完整的物理内存映射到物理_memory_offset变量指定的虚拟地址,因此我们可以使用OffsetPagetable类型。
函数将physical_memory_offset用作参数,并返回一个具有“static”的新的OffsetPagetable实例。这意味着实例在内核的完整运行时保持有效。在功能实体中,我们首先调用Active_LEVEL_4_TABLE函数以检索对“Level4”页表的可变引用。然后,我们调用具有此引用的OffsetPagetable::New函数。作为第二个参数,新函数期望物理存储器的映射开始的虚拟地址在physical_memory_offset变量中给出。
Active_LEVEL_4_TABLE函数只能从Init函数调用,因为它可以在调用多次时容易导致别名可变引用,这可能会导致未定义的行为。因此,我们通过删除PUB说明符而使功能处于私有状态。现在我们可以使用Mapper大小::translate_addr方法,而不是我们自己的memory::translate_addr函数。为了使用它提供的translateaddr方法,我们需要导入MapperAllSizes特性。
正如预期的那样,0xb 8000的翻译以及代码和堆栈地址与我们自己的翻译函数保持不变。此外,我们现在看到虚拟地址physical_memory_offset被映射到物理地址0x0。通过使用MempdPageTable类型的翻译功能,我们可以省去实现巨大页面支持的工作。

内容(六):

我们将使用Mapper特性的map_to函数来实现,所以让我们先看看这个函数。文档告诉我们,它需要四个参数:我们想要映射的页面、应该映射到的页面框架、一组用于页面表条目的标志以及一个Frameallocator。需要帧分配器,因为映射给定页可能需要创建额外的页表,这些表需要未使用的帧作为后备存储。
我们实现的第一步是创建一个新的CREATE_CALE_PARION函数,该函数将给定的虚拟页面映射到0xb 8000,这是VGA文本缓冲区的物理框架。我们选择该框架是因为它允许我们很容易地测试映射是否被正确创建:我们只需要写到新映射的页面,看看我们是否在屏幕上看到了写。
除应该映射的页外,函数还需要对OffsetPagetable实例和frame_allocator的可变引用。frame_allocator参数使用Impl trait语法在实现帧分配器特性的所有类型上是通用的。该特性在PageSize特性上是通用的,可与标准4KiB页和大2mib/1GiB页一起工作。我们只希望创建4KiB映射,因此我们将通用参数设置为Size4KiB。
对于映射,我们设置了当前标志,因为所有有效条目和可写标志都需要它使映射的页可写。调用map_to是不安全的,因为它可以使用无效的参数来破坏内存安全,因此我们需要使用不安全的块。有关所有可能标志的列表,请参见上文的页表格式部分。
map_to函数可能失败,因此返回结果。因为这只是一些不需要鲁棒的示例代码,所以当出现错误时,我们只使用预期死机。在成功的情况下,函数返回MapPerFlush类型,该类型提供了一个简单的方法来从转换旁视缓冲区(TLB)中刷新新映射的页及其刷新方法。类似的结果,该类型使用#[must_use]属性在意外忘记使用该属性时发出警告。
虚拟帧分配器:要能够调用create_example_mapping,我们需要首先创建实现帧分配器特性的类型。如上所述,如果MAP_TO需要它们,则该特性负责为新的页表分配帧。
让我们从简单的案例入手,假设我们不需要创建新的页面表。对于这种情况,总是返回不满足的帧分配器就足够了。我们创建了这样的EmptyFrame分配器,用于测试我们的映射。
但实现帧分配器是不安全的,因为实现者必须保证分配器仅产生未使用的帧。否则可能发生未定义的行为,例如,当两个虚拟页被映射到相同的物理帧时。我们的EmptyFrame分配器只返回None,因此这不是此情况下的问题。
选择虚拟页:现在我们有一个简单的帧分配器,我们可以通过我们的create_example_mapping函数。但是,分配器始终没有返回,因此如果不需要另外的页表框架来创建映射。要了解何时需要附加的页表帧,而何时不需要。让我们考虑一个示例:

图形显示左边的虚拟地址空间、右边的物理地址空间和中间的页表。页表存储在物理内存帧中,用虚线表示。虚拟地址空间包含地址0x803fe00000的单个映射页,以蓝色标记。要将此页面转换为其框架,CPU将遍历4级页面表,直到到达地址36 Kib的帧为止。
此外,该图形还以红色显示VGA文本缓冲区的物理框架。我们的目标是使用CREATESTORE_PARION函数将以前未映射的虚拟页面映射到此框架。因为我们的emptyFrameAllocator总是返回None,所以我们希望创建映射,这样就不需要从分配器中获得额外的帧。
这取决于我们为映射选择的虚拟页面。这张图显示了虚拟地址空间中的两个直白页,都是黄色标记的。一页位于地址0x803fdfd000,即映射页之前的3页(蓝色)。虽然第4级和第3级页表索引与蓝页相同,但第2级和第1级索引不同(见前一篇文章)。2级表中的不同索引意味着此页使用了不同的1级表。由于这个1级表还不存在,所以如果我们选择该页面作为示例映射,则需要创建该表,这将需要额外的未使用的物理框架。
相反,地址0x803fe02000的第二个候选页没有此问题,因为它使用的是与蓝页相同的级别1页表。因此,所有必需的页表都已经存在。
总之,创建新映射的难度取决于我们要映射的虚拟页。在最简单的情况下,该页的1级页面表已经存在,我们只需要写入一个条目。在最困难的情况下,该页位于内存区域中,因为没有级别3的存在,因此我们需要先创建新的级别3、级别2和级别1页表。
要使用EmptyFrame分配器调用我们的create_example_mapping函数,我们需要为已存在所有页表选择一个页面。要找到这样的页面,我们可以利用bootloader在虚拟地址空间的第一个兆字节中加载自己的事实。这意味着该区域中的所有页面都存在有效的级别1表。
因此,我们可以在此存储器区域中选择任何未使用的页,用于我们的示例映射,例如地址0处的页。通常,此页面应保持未使用状态,以保证取消引用空指针会导致页面错误,因此我们知道引导加载程序将其取消映射。
创建映射的操作:我们现在拥有调用我们的create_example_mapping函数的所有必需参数,因此让我们修改内核_main函数以将页面映射到虚拟地址0。由于我们将页面映射到VGA文本缓冲区的帧,因此我们应该能够随后通过它向屏幕写入。
我们首先通过调用CREATEPLOPLE_PARION函数来创建地址为0的页面的映射,其中包含对映射程序和Frameallocator实例的可变引用。这将页面映射到VGA文本缓冲区框架,因此我们应该在屏幕上看到对它的任何写入。
然后,我们将页面转换为原始指针,并写入一个值以偏移400。我们不会写到页面的开头,因为下一个println会直接将VGA缓冲区的顶部移出屏幕。我们写入值0x_f 021_f 077_f 065_f04e,它表示字符串“New!”在白色背景下。正如我们在“VGA文本模式”文章中了解到的那样,对VGA缓冲区的写入应该是易失性的,因此我们使用了WITH_VERIAR方法。
因为负责地址为0的页面的第1级表已经存在所以创建该映射才有效。当我们试图映射一个页面时,因为它试图从emptyFrameAllocator为创建新的页面表分配帧,所以map_to函数失败了。当我们试图将0页映射为0而不是0时,我们可以看到这种情况。
为了创建新的页表,我们需要创建适当的帧分配器。因为我们使用bootloader作为bootinfo结构的一部分传递的memory_map,结构有两个字段:引导加载程序传递的内存映射的“静态引用”和一个next字段,用于跟踪分配器应返回的下一帧的数量。
正如我们在“引导信息”部分中解释的,内存映射是由BIOS/UEFI固件提供的。它只能在启动过程中很早就被查询,因此引导装载程序已经调用了我们各自的功能。存储器映射由包含起始地址、长度和类型(例如未使用、保留等等)。
INIT函数初始化具有给定内存映射的BOTINFOFRAME分配器。下一个字段用0初始化,并且对于每个帧分配将增加,以避免两次返回相同的帧。由于我们不知道内存映射的可用帧是否已经在其他地方使用,所以我们的init函数必须不安全,需要来自调用者的额外保证。
在实现帧分配器特性之前,我们添加了一种将内存映射转换为可用帧迭代器的辅助方法,该函数使用迭代器组合器方法将初始内存映射转换为具有可用物理帧的迭代器:
·首先,我们调用ITER方法将内存映射转换为内存区域迭代器。
·然后,我们使用Filter方法跳过任何保留的或其他不可用的区域。引导加载程序更新它创建的所有映射的内存映射,因此内核使用的帧(代码、数据或堆栈)或存储引导信息的帧已经标记为InUse或类似的。因此,我们可以确定可用的框架不会在其他地方使用。
·然后,我们使用map组合器和Rust的范围语法将内存区域的迭代器转换为地址范围的迭代器。
·下一步是最复杂的:我们通过INTO_ITER方法将每个范围转换为迭代器,然后使用Stepby选择每4096个地址。由于4096字节(=4 Kib)是页面大小,所以我们得到每个帧的开始地址。引导加载程序页面对齐所有可用的内存区域,因此这里不需要任何对齐或舍入代码。通过使用平面_map而不是map,我们得到一个Iterator<Item=U64>而不是Iterator<Item=Iterator<Item=U64>>。
·最后,我们将开始地址转换为PhysFrame类型,以构造所需的Iterator<Item=PhysFrame>。然后,我们使用这个迭代器创建并返回一个新的BootInfoFrameAllocator。
函数的返回类型使用IMPEL特性。通过这种方式,我们可以指定返回某些类型,该类型实现了Iterator特性,并使用了Iterator类型PhysFrame,但不需要命名具体的返回类型。这在这里很重要,因为我们不能命名Conrete类型,因为它依赖于不可命名的闭包类型。
现在实现FrameAllocator特性,我们首先使用可用框架方法从内存映射中获得可用帧的迭代器。然后,我们使用Iterator::nth函数来获得带有索引Sel.Next的帧(从而跳过(Sel.Next-1)帧)。在返回该帧之前,我们将Self.Next增加一个,以便在下一个调用中返回下面的帧。
这个实现并不是最优的,因为它在每个分配中重新创建了usable_Frame分配程序。最好将迭代器直接存储为一个struct字段。这样我们就不需要第n个方法了,只需要在每个分配上调用Next。这种方法的问题是,目前不可能在struct字段中存储IMPEL特征类型。现在修改kernel_Main函数,以传递一个BootInfoFrameAllocator实例,而不是emptyFrameAllocator。
使用引导信息帧分配器,映射成功,我们看到黑白色的“new!”再次出现在屏幕上。在幕后,map_to方法以以下方式创建缺少的页表:
·从传递的framework_allocator分配未使用的框架。
·将框架为零以创建一个新的空页表。
·将较高级别表的条目映射到该框架。
·继续下一个表级别。
虽然我们的CREATESTESTORE_PARION函数只是一些示例代码,但我们现在能够为任意页面创建新的映射。这对于在以后的文章中分配内存或实现多线程至关重要。

四、实验心得体会

作为最后的一个压轴实验,我只能说:很完美,题目够难,内容够多,解释够详实,翻译够挑战,以及和我们书本的结合够紧密。
这一整篇实验报告,可以说有很大一部分,或者说绝大部分都是我对网站博客的外文资料的翻译和整理,然后是我在进行过程中所遇到的各种问题。我最没有想到的事竟然会对32位系统不友好,连作者最后都没有为32位机子完善,导致我在做实验的过程中不得不换了一次虚拟机,然后重新进行了一遍下载安装配置环境之后才能够完成这次的实验。
而且,在这次实验进行之前(准确来讲应该是这次实验之前的三周),我们正好在学习页、帧,以及虚拟内存的知识,可以很明显的感觉到,即便上课的时候认真听讲,感觉自己都学会了,真正要在代码,系统上进行实现,对不起,打扰了。
很难,真的很难,理论知识比上课的时候更深了,而且在实践过程中不免会牵扯到对各种函数的使用,算法倒是其次,重点是每一个函数有什么意思,可以说,即便我囫囵吞枣一般做完了整个实验,也很难对这些函数的作用有一个明确的概念。太多了,太杂了,就比方说最简单的自旋锁和互斥锁结构,它的原理是什么?它们之间的区别是什么?我还是能够说出来,但是如果要我真正去实现他们,比方说.lock函数怎么使用,还是很难,不会用。
而这次的实验中,as_u64函数的用法,我们所写的active_level_4_table函数的用法,它的原理,每一行代码是做什么用的,我到现在也只是有一个大体的概念,而这也是因为我们这次的实验要求我们去学习并自己设计如何能够在实现了打印第4级页表的内容之后,还能够打印第3级、第2级乃至第1级页表的内容。
如果让我说这种层次页表到底是怎么做的,我能够说出来,就是一个嵌套的数组罢了,或者也可以理解为二维、三维或者多维数组,但要我来实现,并实现访问,还是一头雾水,看实验的理论能够看懂,真正自己做,还是更多的是复制粘贴,然后进行稍微的修改。
总而言之,这次的实验作为压轴的实验,很厉害,很满足,也很有力,当然,也有可能是因为我提前了三个周完成这个实验的原因,但即便如此,这个实验我还是写了一个多周呢,在我于考试周刚刚结束,完成了实验四之后我就立刻开始了,仍旧做了一个周的时间,所以,这次实验真的还是很难的。
当然,相比于第三第四次实验所缺失了一部分导致我卡在基础部分一个多周的情况来讲,我对于这次的实验还是很高兴,坑还是比较少的,只要按照网站博客上所写的步骤一步一步一丝不苟地完成,还是可以作出来的。(2019.11.22)

操作系统原理实验(5):内存管理相关推荐

  1. java的内存管理_操作系统实验——java内存管理

    1.Test.java import java.util.Scanner; public class Test { public static void main(String[] args) { T ...

  2. 操作系统(三)内存管理

    操作系统(三)内存管理 一.程序执行过程 装入的三种方式 链接的三种方式 二.内存管理的概念 内存空间的分配与回收 连续分配管理方式 单一连续分配 固定分区分配 动态分区分配 首次适应算法 最佳适应算 ...

  3. 操作系统课设之内存管理

    前言 课程设计开始了,实验很有意思,写博客总结学到的知识 白嫖容易,创作不易,学到东西才是真 本文原创,创作不易,转载请注明!!! 本文链接 个人博客:https://ronglin.fun/arch ...

  4. (王道408考研操作系统)第三章内存管理-第二节1:虚拟内存管理基本概念

    文章目录 一:传统存储管理方式的弊端 二:局部性原理与高速缓冲技术Cache (1)Cache基本原理 (2)局部性原理 三:虚拟内存的定义和特征 (1)定义 (2)特征 四:虚拟内存实现 内存管理需 ...

  5. linux内存实验,LINUX编程-实验五 内存管理实验

    实验五内存管理实验 1.目的要求 (1)学习使用内存管理库函数. (2)学习分析.改正内存错误. 2.实验内容 (1)内存库函数实验 ●malloc函数 原型:extern void *malloc( ...

  6. 操作系统概念学习笔记 15 内存管理(一)

    操作系统概念学习笔记 15 内存管理(一) 背景 内存是现代计算机运行的中心.内存有非常大一组字或字节组成,每一个字或字节都有它们自己的地址.CPU依据程序计数器(PC)的值从内存中提取指令.这些指令 ...

  7. 操作系统概念学习笔记 16 内存管理(二) 段页

    操作系统概念学习笔记 16 内存管理 (二) 分页(paging) 分页(paging)内存管理方案允许进程的物理地址空间可以使非连续的.分页避免了将不同大小的内存块匹配到交换空间上(前面叙述的内存管 ...

  8. (王道408考研操作系统)第三章内存管理-第二节3:页面置换算法2

    上接: (王道408考研操作系统)第三章内存管理-第二节2:页面置换算法1 文章目录 一:时钟置换算法(CLOCK) (1)简单时钟置换算法 (2)改进型时钟置换算法 二:页面置换算法总结 一:时钟置 ...

  9. ZUCC_操作系统原理实验_Lab9进程的通信消息队列

    lab9进程的通信–消息队列 一.两个进程并发执行,通过消息队列,分别进行消息的发送和接收 1.代码: //接受消息 #include<stdio.h> #include<stdli ...

  10. ZUCC_操作系统原理实验_实验九 消息队列

    操作系统原理实验报告 课程名称 操作系统原理实验 实验项目名称 实验九 消息队列 实验目的 了解 Linux 系统的进程间通信机构 (IPC): 理解Linux 关于消息队列的概念: 掌握 Linux ...

最新文章

  1. PowerShell学习笔记(三)
  2. Overload Overwrite Override
  3. 106:HttpResponse对象讲解
  4. C#入门篇5-5:流程控制语句 dowhile
  5. 自然语言处理期末复习(1)n元模型
  6. java 取整_javascript 解决默认取整的坑(目前已知的最佳解决方案)
  7. 如何定位Release程序崩溃原因
  8. c语言 sqlite_SQLite与C语言
  9. cython加密代码python_利用Cython对python代码进行加密
  10. 微信小程序 不能跳转页面 跳转不生效
  11. android 电量详情,Android应用开发之Android 8.0 电池-)耗电详情获取方法
  12. 曝微软将发布基于 Excel 的低代码语言:Power Fx
  13. Android成长日记-Activity
  14. stm32毕业设计 超声波雷达可视化系统
  15. python中tab的用法_pyhton 使用tab键自动补全
  16. C# 使用NPOI.XSSF对Excel进行操作
  17. EasyUI项目驱动学习
  18. 不是抽象的, 并且未覆盖Handler中的抽象方法
  19. STM32F100R4 单片机解密特性 ST芯片解密
  20. 全“芯”升级,浩辰CAD 2021赋能全国产化CAD应用

热门文章

  1. 阿里云云.速成美站和云.企业官网建站介绍
  2. 用户的虚拟地址 linux 0 4gb,Linux驱动虚拟地址和物理地址的映射
  3. NYOJ-999-师傅又被妖怪抓走了
  4. 2020年电商设计风格分析
  5. IG赢了,让我们先理直气壮的喊出那句 我们是冠军!
  6. 1055: 兔子繁殖问题
  7. 每天读论语《论语·学而》02
  8. 浮点变量(float, double等)和零值的比较
  9. C#中Property和Attribute的区别
  10. 弘辽科技:拼多多怎么提升访客量?有哪些方法?