Speed up system calls (easy)
题目
一些操作系统(例如 Linux)通过在用户空间和内核之间共享一个只读区域的数据来加速某些系统调用。这消除了在执行这些系统调用时需要跨越内核边界的需要。为了帮助你学习如何将映射插入页表,你的第一个任务是在 xv6 中为 getpid() 系统调用实现这种优化。
当每个进程被创建时,在 USYSCALL(在 memlayout.h 中定义的一个虚拟地址)处映射一个只读页面。在该页面的起始位置存储一个 struct usyscall(也在 memlayout.h 中定义),并将其初始化为存储当前进程的 PID。在本实验中,用户空间一侧已经提供了 ugetpid(),它将自动使用 USYSCALL 映射。如果在运行 pgtbltest 时 ugetpid 测试用例通过,你将获得这部分实验的全部分数。
以下是一些提示:
- 你可以在
proc_pagetable()(位于 kernel/proc.c)中执行映射。 - 选择权限位,只允许用户空间读取该页面。
- 你可能会发现
mappages()是一个有用的工具。 - 不要忘记在
allocproc()中分配和初始化该页面。 - 确保在
freeproc()中释放该页面。
思考问题:
xv6 中还有哪些其他系统调用可以通过这种共享页面的方式加速?解释如何实现。
思考
- 因为每个进程都需要存储一个usyscall,所以我们应该在
proc.h,在结构体上加入。
1 | // Per-process state |
- 使用mappages来实现页面映射
1 | // map 一个只读页面USYSCALL |
特别注意:VA虚拟地址是定义一个位置。而pa,物理地址实际上是proc.h中定义的。
1 | // static struct proc* allocproc(void) |
1 | if(p->usyscall) |
打印页表(简单)
为了帮助你可视化RISC-V页表,并可能为未来的调试提供帮助,你的第二个任务是编写一个打印页表内容的函数。
定义一个名为vmprint()的函数。它应该接受一个pagetable_t类型的参数,并按照下面描述的格式打印该页表。在exec.c中,在return argc之前插入if (p->pid == 1) vmprint(p->pagetable),以打印第一个进程的页表。如果你通过了make grade中的pte打印测试,你将获得这部分实验的全部分数。
现在,当你启动Xv6时,它应该会打印类似以下内容,描述第一个进程在完成exec()初始化时的页表:
复制
1 | page table 0x0000000087f6e000 |
第一行显示传递给vmprint的参数。之后,每一行对应一个PTE(页表条目),包括引用树中更深层次的页表页面的PTE。每个PTE行通过缩进的“…”数量表示其在树中的深度。每个PTE行显示其在页表页面中的索引、PTE的位以及从PTE中提取的物理地址。不要打印无效的PTE。在上面的例子中,顶级页表页面有条目0和255的映射。条目0的下一层只有索引0被映射,而该索引0的底层有条目0、1和2被映射。
你的代码可能会输出与上面不同的物理地址。条目数量和虚拟地址应该是相同的。
一些建议:
- 你可以将
vmprint()放在kernel/vm.c中。 - 使用
kernel/riscv.h文件末尾的宏。 freewalk函数可能会给你一些启发。- 在
kernel/defs.h中定义vmprint的原型,以便从exec.c中调用它。 - 在
printf调用中使用%p打印完整的64位十六进制PTE和地址,如示例所示。
解释vmprint的输出
根据文本中的图3-4解释vmprint的输出。页0中包含什么?页2中有什么?在用户模式下运行时,进程能否读写页1映射的内存?倒数第三页包含什么?
解释示例输出
- 页表的结构:
- 页表是一个多级结构,每一级由页表条目(PTE)组成。
- 每个PTE可以指向一个物理页面或下一级页表。
- 页0的内容:
- 页0通常包含内核代码或数据。在Xv6中,页0可能映射了内核的某些部分。
- 页2的内容:
- 页2可能包含用户空间的堆或栈。具体取决于进程的内存布局。
- 页1的访问权限:
- 在用户模式下,进程是否可以读写页1映射的内存取决于PTE的权限位(如
PTE_U和PTE_W)。 - 如果PTE的
PTE_U位被设置,则用户模式可以访问该页面;如果PTE_W位被设置,则可以写入。
- 在用户模式下,进程是否可以读写页1映射的内存取决于PTE的权限位(如
- 倒数第三页的内容:
- 倒数第三页可能是一个用户空间的页面,用于存储进程的代码、数据或栈。
- 具体内容取决于进程的内存布局和页表的映射。
通过vmprint的输出,你可以清晰地看到进程的内存布局以及每个页面的映射情况。
思考
在分页机制中,页表项(PTE)可以有两种类型:
- 叶子页表项(Leaf PTE):直接映射到物理页面(通常是 4KB 的内存块)。
- 非叶子页表项(Non-Leaf PTE):指向下一个级别的页表。
注意如何判断一个PTE是否是叶子表项,也就是根据flags进行判断
1 | // 如果页表项有效(PTE_V),并且它指向下一级页表(无 PTE_R/W/X 权限位)。 |
该题的要点,在于引导我们思考,虚拟地址到物理地址的传播路径
检测哪些页面已被访问(难度较大)
一些垃圾回收器(一种自动内存管理的形式)可以从关于哪些页面已被访问(读或写)的信息中受益。在本实验的这一部分,你需要为 xv6 添加一个新功能,通过检查 RISC-V 页表中的访问位,将这些信息报告给用户空间。当 RISC-V 硬件页表解析器解决 TLB 缺失时,它会在 PTE 中标记这些位。
你的任务是实现 pgaccess(),这是一个系统调用,用于报告哪些页面已被访问。该系统调用接受三个参数。首先,它接受要检查的第一个用户页面的起始虚拟地址。其次,它接受要检查的页面数量。最后,它接受一个用户地址,用于存储结果到一个位掩码(一种数据结构,每个页面使用一个位,其中第一个页面对应最低有效位)。如果在运行 pgtbltest 时,pgaccess 测试用例通过,你将获得本实验部分的全部分数。
以下是一些提示:
- 从在
kernel/sysproc.c中实现sys_pgaccess()开始。 - 你需要使用
argaddr()和argint()解析参数。 - 对于输出的位掩码,将临时缓冲区存储在内核中并在填充正确的位后将其复制到用户空间(通过
copyout())会更方便。 - 设置可扫描页面数量的上限是可以接受的。
kernel/vm.c中的walk()函数对于找到正确的 PTE 非常有用。- 你需要在
kernel/riscv.h中定义访问位PTE_A。请查阅 RISC-V 手册以确定其值。 - 在检查后务必清除
PTE_A。否则,将无法确定自上次调用pgaccess()以来页面是否被访问(即,该位将永远被设置)。 vmprint()可能有助于调试页表。