4. Project 3: Virtual Memory#
到现在为止,您应该已经熟悉了Pintos。您的操作系统可以适当地处理多个执行线程同步,并且可以一次加载多个用户程序。然而,可运行程序的数量和大小受计算机的主存限制。在此作业中,您将去掉该限制。
您将在最后一次作业的基础上建立此任务。测试程序项目2中的内容也应与项目3中的内容一起使用。开始进行项目3之前,请先修复您的Project 2提交中的所有错误,因为这些错误很可能会在项目3引起相同的问题。
您将继续以与上一个作业相同的方式处理Pintos磁盘和文件系统(请参阅3.1.2 使用文件系统部分)。
4.1 Background#
4.1.1 Source Files#
您将在此项目的“vm”目录中工作。“vm”目录仅包含“Makefile
”。与“userprog”的唯一变化是,这个新的“Makefile”打开了“-DVM”设置。您编写的所有代码都将位于新文件或早期项目中引入的文件中。
您可能会第一次遇到几个文件:
“devices/block.h
”
“devices/block.c
”
提供对块设备的基于扇区的读写访问。您将使用此接口作为块设备访问交换分区。
4.1.2 Memory Terminology#
需要仔细定义以防止对虚拟内存的讨论造成混淆。 因此,我们首先介绍一些有关内存和存储的术语。 在项目2中应该熟悉其中一些术语(请参见[3.1.4 虚拟内存布局]部分),但是其中很多是新的。
4.1.2.1 Pages#
页面,有时也称为虚拟页面,是虚拟内存的连续区域,长度为4,096字节(页面大小)。页面必须是“页面对齐”的,也就是说,从一个虚拟地址开始,该地址可以被页面大小均匀地整除。因此,可以将32位虚拟地址分为20位页号和12位页偏移量(或仅偏移量),如下所示:
31 12 11 0
+-------------------+-----------+
| Page Number | Offset |
+-------------------+-----------+
虚拟地址
每个进程都有一组独立的用户(虚拟)页面,这些页面位于虚拟地址“ PHYS_BASE”(通常为“0xc0000000”(3GB))以下。另一方面,“内核(虚拟)页面”的集合是全局的,无论哪个线程或进程处于活动状态,它们都保持不变。内核可以访问用户页面和内核页面,但是用户进程只能访问其自己的用户页面。有关更多信息,请参见[3.1.4 虚拟内存布局]部分。
Pintos提供了一些有用的功能来处理虚拟地址。 有关详细信息,请参见[A.6 虚拟地址]部分。
4.1.2.2 Frames#
frame,有时也称为physical frame或page frame,是物理内存的连续区域。像页面一样,框架必须是页面大小并且页面对齐。因此,可以将32位物理地址分为20位帧号和12位帧偏移(或仅偏移),如下所示:
31 12 11 0
+-------------------+-----------+
| Frame Number | Offset |
+-------------------+-----------+
物理地址
80x86没有提供任何直接访问物理地址上的内存的方法。Pintos通过将内核虚拟内存直接映射到物理内存来解决此问题:内核虚拟内存的第一页映射到物理内存的第一帧,第二页映射到第二帧,依此类推。因此,可以通过内核虚拟内存访问帧。
Pintos提供了在物理地址和内核虚拟地址之间转换的功能。有关详细信息,请参见[A.6 虚拟地址]。
4.1.2.3 Page Tables#
在Pintos中,“页表”是CPU用来将虚拟地址转换为物理地址(即从页面转换为帧)的数据结构。页表格式由80x86体系结构决定。Pintos在“ pagedir.c”中提供了页表管理代码(请参见[A.7 页表]部分)。
下图说明了页面和框架之间的关系。左侧的虚拟地址由页码和偏移量组成。页表将页码转换为帧号,然后将其与未修改的偏移量组合在一起以获得右侧的物理地址。
+----------+
.--------------->|Page Table|---------.
/ +----------+ |
31 | 12 11 0 31 V 12 11 0
+-----------+-------+ +------------+-------+
| Page Nr | Ofs | | Frame Nr | Ofs |
+-----------+-------+ +------------+-------+
Virt Addr | Phys Addr ^
\_____________________________________/
4.1.2.4 Swap Slots#
交换插槽是交换分区中磁盘空间的连续页面大小区域。尽管指示插槽放置的硬件限制比页面和框架宽松,但交换插槽应按页面对齐,因为这样做没有不利之处。
4.1.3 Resource Management Overview#
您将需要设计以下数据结构:
补充页表
通过补充页面表来启用页面错误处理。请参阅[4.1.4 管理补充页表]部分。
页框表Frame table
允许有效执行迁移政策。请参阅[4.1.5 管理框架表].
交换表
跟踪交换插槽的使用情况。 请参阅[4.1.6 管理交换表]。
文件映射表 进程可以将文件映射到其虚拟内存空间。您需要一个表来跟踪哪些文件映射到哪些页面。
您不一定需要实现四个完全不同的数据结构:将相关资源全部或部分合并到一个统一的数据结构中可能很方便。
对于每个数据结构,您需要确定每个元素应包含哪些信息。您还需要确定数据结构的范围(本地(每个进程)还是全局(适用于整个系统)),以及在其范围内需要多少实例。
为了简化设计,您可以将这些数据结构存储在不可分页的内存中。这意味着您可以确保其中的指针将保持有效。
数据结构的可能选择包括数组、列表、位图和哈希表。数组通常是最简单的方法,但是数据稀疏的数组会浪费内存。列表也很简单,但是遍历较长的列表以查找特定位置会浪费时间。数组和列表都可以调整大小,但列表更有效地支持中间的插入和删除。
Pintos在“lib/kernel/bitmap.c”和“lib/kernel/bitmap.h”中包含一个位图数据结构。位图是位的数组,每个位可以为true或false。位图通常用于跟踪一组(相同)资源的使用情况:如果正在使用资源n,则位图的位n为true。Pintos位图的大小是固定的,尽管您可以扩展其实现以支持调整大小。
Pintos还包括一个哈希表数据结构(请参阅[A.8 哈希表]部分)。Pintos哈希表可有效支持各种表大小的插入和删除。
尽管更复杂的数据结构可能会带来性能或其他好处,但它们也可能不必要地使您的实现复杂化。因此,我们不建议您在设计中实现任何高级数据结构(例如,平衡的二叉树)。
4.1.4 Managing the Supplemental Page Table#
补充页表用有关每页的其他数据补充页表。由于页表格式的限制,因此需要它。这种数据结构通常也称为“页表”。我们添加“补充”一词以减少混乱。
补充页表至少用于两个目的。最重要的是,在出现页面缺失时,内核会在补充页面表中查找出现缺失的虚拟页面,以查找应该包含哪些数据。其次,内核在进程终止时查阅补充页表,以决定释放哪些资源。
您可以根据需要组织补充页表。至少有两种基本的组织方法:按段或按页。(可选)您可以使用页表本身作为索引来跟踪补充页表的成员。您必须在“pagedir.c”中修改Pintos页面表的实现。我们仅建议高级学生使用此方法。有关更多信息,请参见A.7.4.2页表条目格式。
补充页面表的最重要用户是页面缺失处理程序。在项目2中,页面缺失始终表示内核或用户程序中存在错误。在项目3中,这不再成立。现在,页面错误可能仅指示必须从文件或交换中引入页面。您将必须实现更复杂的页面错误处理程序来处理这些情况。您应该通过修改“userprog/exception.c”中的“page_fault()”来实现的页面错误处理程序,大致需要执行以下操作:
-
在补充页面表中找到出现故障的页面。如果内存引用有效,则使用补充页表条目来定位该页中可能存在的数据,该数据可能在文件系统中,也可能在交换插槽中,或者可能只是全零页面。如果实现共享,则页面的数据甚至可能已经在页面框架中,但不在页面表中。
如果补充页表指示用户进程不应在其尝试访问的地址处期望任何数据,或者该页位于内核虚拟内存中,或者如果该访问是尝试写入只读页,则访问无效。任何无效的访问都会终止该过程,从而释放其所有资源。
-
获取一个框架来存储页面。有关详细信息,请参见[4.1.5 管理框架表]。
如果实现共享,则所需的数据可能已经在框架中,在这种情况下,您必须能够找到该框架。
-
通过从文件系统读取或交换数据,将数据清零等将数据提取到帧中。
如果实现共享,则所需页面可能已经在框架中,在这种情况下,此步骤无需采取任何操作。
-
将错误虚拟地址的页表项指向物理页。您可以使用“userprog/pagedir.c”中的功能。
4.1.5 管理框架表#
对于每个包含用户页面的框架,“框架表”包含一个条目。框架表中的每个条目都包含一个指向该页面的指针(如果有的话)以及当前选择的其他数据。框架表允许Pintos通过选择没有可用框架时退出的页面来有效地实施逐出策略。
用于用户页面的帧应通过调用“palloc_get_page(PAL_USER)”从“用户池”中获取。 您必须使用“PAL_USER”来避免从“内核池”中进行分配,这可能会导致某些测试用例意外失败(请参阅Why PAL_USER?。 如果您在框架表实现中修改“ palloc.c”,请确保保留两个池之间的区别。
帧表上最重要的操作是获取未使用的帧。框架空闲时,这很容易。如果没有可用的框架,则必须通过从框架中逐出某个页面来释放框架。
如果没有分配交换插槽就无法收回任何帧,但是交换已满,则内核崩溃panic。实际的OS应用了各种各样的策略来恢复或防止此类情况,但是这些策略不在本项目的范围之内。
替换过程大致包括以下步骤:
-
使用您的页面替换算法选择要逐出的帧。如下所述,页表中的“已访问”和“脏”位将派上用场。
-
从任何引用该框架的页表中删除对该框架的引用.
除非您已实现共享,否则在任何给定时间,应只有一个页面引用一个框架。
-
如有必要,将该页面写入文件系统或交换。
被逐出的帧然后可以用于存储不同的页面。
4.1.5.1 Accessed and Dirty Bits#
80x86硬件通过每个页面的页面表条目(PTE)中的一对位,为实现页面替换算法提供了一些帮助。 在对页面进行任何读取或写入时,CPU会将页面的PTE中的accessed位设置为1,而在写入时,CPU会将dirty位设置为1。CPU从不将这些位重置为0,但是 操作系统可能会这样做。
您需要了解alias,即两个(或更多)引用同一框架的页面。当访问别名帧时,仅在一个页表项(用于访问的页的表项)中更新已访问位和脏位。其他别名的访问位和脏位不会更新。
在Pintos中,每个用户虚拟页面都别名为其内核虚拟页面。您必须以某种方式管理这些别名。例如,您的代码可以检查和更新两个地址的访问位和脏位。或者,内核可以通过仅通过用户虚拟地址访问用户数据来避免此问题。
有关使用访问位和脏位的功能的详细信息,请参见[A.7.3 访问位和脏位]。
4.1.6 Managing the Swap Table#
交换表跟踪使用中和可用交换插槽。 它应该允许选择一个未使用的交换插槽,以将页面从其框架移到交换分区。 当它的页面被读回或页面被交换的进程终止时,它应该允许释放交换插槽。
您可以使用“BLOCK_SWAP”块设备进行交换,通过调用“block_get_role()”获得代表它的“结构块”。在“vm/build”目录中,使用命令“pintos-mkdisk swap.dsk --swap-size=n”创建一个名为“swap.dsk”的磁盘,该磁盘包含一个n-MB交换分区。当您运行“ pintos”时,“swap.dsk”将作为附加磁盘自动附加。或者,您可以告诉pintos使用“--swap-size = n”将临时n-MB交换磁盘用于一次运行。
交换插槽应延迟分配,即仅在驱逐实际需要时才分配。从可执行文件读取数据页并将它们写入进程启动时立即进行交换并不是一件容易的事。交换插槽不应保留用于存储特定页面。
当其内容读回一帧时,释放一个交换插槽。
4.1.7 Managing Memory Mapped Files#
文件系统最常通过“读取”和“写入”系统调用来访问。第二个接口是使用“mmap”系统调用将文件“映射”到虚拟页面。然后,程序可以直接在文件数据上使用存储指令。
假设文件“foo”的长度为“0x1000”字节(4kB或一页)。如果将“foo”映射到从地址“0x5000”开始的内存中,则任何对位置“0x5000...0x5fff”的访问都将访问“foo”的相应字节。
这是一个使用mmap
将文件打印到控制台的程序。 它打开在命令行上指定的文件,将其映射到虚拟地址“ 0x10000000”,将映射的数据写入控制台(fd 1),然后取消映射文件。
#include <stdio.h>
#include <syscall.h>
int main (int argc UNUSED, char *argv[])
{
void *data = (void *) 0x10000000; /* Address at which to map. */
int fd = open (argv[1]); /* Open file. */
mapid_t map = mmap (fd, data); /* Map file. */
write (1, data, filesize (fd)); /* Write file to console. */
munmap (map); /* Unmap file (optional). */
return 0;
}
“examples”目录中的“mcat.c”包含一个具有完整错误处理的类似程序,其中还包含“mcp.c”作为“mmap”的第二个示例。
您的提交必须能够跟踪内存映射文件使用的内存。 这对于正确处理映射区域中的页面错误并确保映射文件不会与流程中的任何其他段重叠是必要的。
4.2 Suggested Order of Implementation#
我们建议以下初始执行顺序:
-
框架表(请参阅[4.1.5 管理框架表]。 更改“
process.c
”以使用您的框架表分配器。暂不执行交换。 如果帧用完,请使分配器失败或使内核崩溃。
完成此步骤后,您的内核仍应通过所有项目2测试用例。
-
补充页表和页故障处理程序(请参阅[4.1.4 管理补充页表]部分)。更改“process.c”以在加载可执行文件并设置其堆栈时在补充页表中记录必要的信息。在页面错误处理程序中实现代码和数据段的加载。目前,仅考虑有效访问。
完成此步骤后,您的内核应通过所有项目2功能测试用例,但仅通过了一些健壮性测试。
从这里,您可以并行实现堆栈增长、映射文件和进程退出时的页面回收。
下一步是实施淘汰(请参阅[4.1.5 管理框架表]。) 最初,您可以选择要随机淘汰的页面。此时,您需要考虑如何管理用户和内核页面的访问位和脏位以及别名。同步也是一个问题:如果进程A在其帧进程B处于退出过程的页面上出现故障,您该如何处理?最后,实现一种淘汰策略,例如时钟算法。
4.3 Requirements#
此作业是一个开放式设计问题。我们将尽可能少地谈论如何做事情。相反,我们将重点关注需要您的操作系统支持的功能。我们希望您能提出一个合理的设计。您将可以自由选择如何处理页面错误,如何组织交换分区,如何实现分页等。
4.3.1 Design Document#
在上交项目之前,必须将project 3设计文档模板)复制到源代码树中,名称为“pintos/src/vm/DESIGNDOC
”,并填写 我们建议您在开始进行项目之前阅读设计文档模板。 请参阅D. 项目文档),以获得与虚构项目一起提供的示例设计文档。
4.3.2 Paging#
为从可执行文件加载的段实现分页。所有这些页面都应延迟加载,即仅在内核拦截它们的页面错误时才加载。逐出后,应将自加载以来已修改的页面(例如,如“脏位”所示)写入交换页。未经修改的页面(包括只读页面)绝对不能写入交换位置,因为它们始终可以从可执行文件中读取。
实现近似LRU的全局页面替换算法。您的算法应至少执行“第二次机会”或“时钟”算法的简单变体。
您的设计应考虑并行性。如果一页故障需要I/O,则与此同时,没有故障的进程应继续执行,而其他不需要I/O的页面故障应能够完成。这将需要一些同步工作。
您需要修改程序加载器的核心,即“userprog/process.c”中“ load_segment()”中的循环。每次循环时,“page_read_bytes”都会从可执行文件中读取要读取的字节数,而“page_zero_bytes”会在读取的字节后接收初始化为零的字节数。两者总和为PGSIZE(4,096)。页面的处理取决于以下变量的值:
-
如果“page_read_bytes”等于“PGSIZE”,则应在首次访问时从基础文件中请求对页面进行分页.
-
如果“ page_zero_bytes”等于“ PGSIZE”,则根本不需要从磁盘读取页面,因为它全为零。您应该通过在首页错误时创建一个由全零组成的新页面来处理此类页面。
-
否则,“page_read_bytes”和“page_zero_bytes”都不等于PGSIZE。在这种情况下,将从底层文件中读取页面的初始部分,并将其余部分清零。
4.3.3 Stack Growth#
实现堆栈增长。在项目2中,堆栈是用户虚拟地址空间顶部的单个页面,并且程序限于该堆栈。现在,如果堆栈超出其当前大小,请根据需要分配其他页面。
仅当其他页面“看起来”是堆栈访问时才分配它们。设计一种试探法,尝试将堆栈访问与其他访问区分开。
如果用户程序在堆栈指针下方写入堆栈,则会出现故障,因为典型的实际OS可能会随时中断进程以传递“信号”,从而将数据压入堆栈。[(4)] {“ DOCF4”} 但是,80x86PUSH
指令在调整堆栈指针之前会检查访问权限,因此它可能导致页面错误低于堆栈指针4个字节。 (否则,PUSH
将无法以直接的方式重新启动。)类似地,PUSHA
指令一次压入32个字节,因此它可能会在堆栈指针以下32个字节出错。
您将需要能够获取用户程序的堆栈指针的当前值。 在系统调用或用户程序生成的页面错误中,您可以分别从传递给syscall_handler()或page_fault()的struct intr_frame的ʻesp成员中检索它。 如果在访问用户指针之前先对其进行了验证(请参阅3.1.5 访问用户内存)),则只有这些情况需要处理。 另一方面,如果您依靠页面错误来检测无效的内存访问,则需要处理另一种情况,即内核中发生页面错误。 由于处理器仅在异常导致从用户模式切换到内核模式时保存堆栈指针,因此从传递给page_fault()的struct intr_frame中读取`esp'会产生未定义的值,而不是用户堆栈指针。 您将需要安排另一种方式,例如在从用户模式到内核模式的初始转换中将“esp”保存到“struct thread”中。
您应该像大多数操作系统一样对堆栈大小施加一定的绝对限制。 某些操作系统使限制可由用户调整,例如 在许多Unix系统上使用ulimit
命令。在许多GNU/Linux系统上,默认限制为8MB。
第一个堆栈页面无需延迟分配。 您可以在加载时使用命令行参数来分配和初始化它,而无需等待其出错。
所有堆栈页面都应为驱逐对象。被淘汰的堆栈页面应写入交换区。
4.3.4 Memory Mapped Files#
实现内存映射文件,包括以下系统调用。
System Call: mapid_t mmap (int fd, void *addr)
将以fd打开的文件映射到进程的虚拟地址空间。整个文件映射到从addr开始的连续虚拟页面。
您的VM系统必须懒惰地加载`mmap`区域中的页面,并使用`mmap`ed文件本身作为映射的后备存储。也就是说,逐出mmap映射的页面会将其写回到其映射的文件中。
如果文件的长度不是PGSIZE`的倍数,则最终映射页面中的某些字节会“伸出”到文件末尾。 当页面从文件中出错时,将这些字节设置为零系统,并在页面写回到磁盘时丢弃它们。
如果成功,此函数将返回一个“映射ID”,该ID唯一标识进程中的映射。 失败时,它必须返回-1,否则不应为有效的映射ID,并且进程的映射必须保持不变。
如果以<var>fd</var>打开的文件的长度为零字节,则对“mmap”的调用可能会失败。如果<var>addr</var>不是页面对齐的,或者如果映射的页面范围与任何现有的映射页面集(包括堆栈或可执行加载时映射的页面)重叠,则它必须失败。如果<var>addr</var>为0,它也必须失败,因为某些Pintos代码假定未映射虚拟页面0。 最后,代表控制台输入和输出的文件描述符0和1是不可映射的。
System Call: void munmap (mapid_t mapping) 取消映射由 mapping 指定的映射,该映射必须是由尚未取消映射的同一进程先前调用mmap所返回的映射ID。
当进程退出时,无论是通过“退出”还是通过任何其他方式,所有映射都将隐式取消映射。 当取消映射时,无论是隐式还是显式地,该进程写入的所有页面都将被写回到文件中,而未写入的页面则不能被写入。 然后,将页面从进程的虚拟页面列表中删除。
关闭或删除文件不会取消映射任何映射。 创建后,按照Unix约定,映射将一直有效,直到调用munmap或进程退出为止。 有关更多信息,请参见删除打开的文件。 您应该使用file_reopen
函数为文件的每个映射获取一个独立的文件引用。
如果两个或多个进程映射同一文件,则不要求它们看到一致的数据。 Unix通过使两个映射共享相同的物理页面来处理此问题,但是“ mmap”系统调用还具有一个参数,允许客户端指定页面是共享页面还是私有页面(即写时复制)。
4.3.5 Accessing User Memory#
您需要修改代码以访问用户内存(请参见[3.1.5 访问用户内存]) 处理系统调用。 就像用户进程可以访问其内容当前位于文件或交换空间中的页面一样,它们也可以将引用此类非驻留页面的地址传递给系统调用。 此外,除非内核采取措施防止这种情况发生,否则即使在内核代码正在访问页面的情况下,也可能会将页面从其框架中逐出。 如果内核代码访问此类非居民用户页面,则将导致页面错误。
访问用户内存时,您的内核必须准备好处理此类页面错误,或者必须防止它们发生。 内核在保存处理这些错误所需的资源时,必须防止此类页面错误。 在Pintos中,此类资源包括由设备驱动程序获取的锁,这些驱动程序控制包含文件系统和交换空间的设备。 举一个具体的例子,在设备驱动程序访问传递给file_read
的用户缓冲区时,您一定不允许页面错误发生,因为在处理此类错误时您将无法调用驱动程序。
要防止此类页面错误,需要在其中进行访问的代码与您的页面逐出代码之间进行协作。 例如,您可以扩展框架表以记录框架中包含的页面何时不能被驱逐。 (这也称为“固定”或“锁定”其框架中的页面。)固定会限制页面替换算法在寻找要逐出的页面时的选择,因此请确保不再将页面固定为多余的时间,并避免固定页面 不必要的时候。
4.4 FAQ#
我需要写多少代码?
这是由diffstat程序生成的参考解决方案的摘要。最后一行给出了插入和删除的总行数;更改的行将同时算作插入和删除。
此摘要与Pintos基本代码有关,但是项目3的参考解决方案从参考解决方案到项目2。请参阅[3.4 FAQ],用于项目2的摘要.
参考解决方案仅代表一种可能的解决方案。许多其他解决方案也是可能的,其中许多与参考解决方案有很大不同。某些优秀的解决方案可能不会修改参考解决方案修改的所有文件,而某些解决方案可能会修改参考解决方案未修改的文件。
Makefile.build | 4
devices/timer.c | 42 ++
threads/init.c | 5
threads/interrupt.c | 2
threads/thread.c | 31 +
threads/thread.h | 37 +-
userprog/exception.c | 12
userprog/pagedir.c | 10
userprog/process.c | 319 +++++++++++++-----
userprog/syscall.c | 545 ++++++++++++++++++++++++++++++-
userprog/syscall.h | 1
vm/frame.c | 162 +++++++++
vm/frame.h | 23 +
vm/page.c | 297 ++++++++++++++++
vm/page.h | 50 ++
vm/swap.c | 85 ++++
vm/swap.h | 11
17 files changed, 1532 insertions(+), 104 deletions(-)
我们是否需要一个可行的项目2来实施项目3?
是的.
还有什么额外的积分?
您可以实现共享:创建多个使用同一可执行文件的进程时,请在这些进程之间共享只读页面,而不是为每个进程创建只读段的单独副本。 如果您精心设计了数据结构,则共享只读页面不会使此部分变得很难.
处理页面错误后,如何恢复进程?
从“ page_fault()”返回将继续当前的用户进程(请参阅[A.4.2 内部中断处理](pintos_6.html#SEC109))。 然后它将重试指令指针指向的指令点。
为什么用户进程有时会在堆栈指针上方缺页?
您可能会注意到,在堆栈增长测试中,即使`PUSH`和`PUSHA`指令会导致错误在当前堆栈下方4和32字节,用户程序也会在用户程序当前堆栈指针上方的地址上发生错误。 指针。
这并不罕见。 PUSH和PUSHA指令并不是触发用户堆栈增长的唯一指令。 例如,用户程序可以通过使用“ SUB $ n,%esp”指令递减堆栈指针来分配堆栈空间,然后使用“ MOV ...,m(%esp)”指令写入堆栈位置 在当前堆栈指针上方<var> m </var>个字节的分配空间内。这样的访问是完全有效的,并且您的内核必须增加用户程序的堆栈,以使这些访问成功。
虚拟内存系统是否需要支持数据段增长?
否。数据段的大小由链接器确定。我们仍然没有在Pintos中进行动态分配(尽管可以通过使用内存映射文件在用户级别“伪造”它)。支持数据段增长应该为设计良好的系统增加一点额外的复杂性。
为什么我要用PAL_USER
来分配页框?
将PAL_USER传递给palloc_get_page()会导致它从用户池而不是主内核池分配内存。 用户池中的页面用完只会导致用户程序进行页面调度,但是内核池中的页面用完则会导致许多故障,因为需要获取内存的内核功能太多。 如果愿意,您可以在palloc_get_page()之上放置其他一些分配器,但这应该是基础机制。
另外,您可以使用“-ul”内核命令行选项来限制用户池的大小,这使您可以轻松地测试具有各种用户内存大小的VM的实现。