本文为本人完成6.s081 2021fall时的一些记录,仅作为备忘录使用。
代码仓库地址:代码
task 1: Speed up system calls (easy)
题意描述
When each process is created, map one read-only page at USYSCALL (a VA defined in
memlayout.h
). At the start of this page, store astruct usyscall
(also defined inmemlayout.h
), and initialize it to store the PID of the current process. For this lab,ugetpid()
has been provided on the userspace side and will automatically use the USYSCALL mapping. You will receive full credit for this part of the lab if theugetpid
test case passes when runningpgtbltest
.
xv6执行系统调用的时候,需要通过ecall指令陷入内核,然后在内核态执行完系统调用时再返回用户态,这样是比较耗时的。
出于性能方面考虑,可以让用户空间和内核空间共享一片只读的物理内存空间,这样可以避免用户态和内核态之间的切换(减少上下文切换),从而加速某些系统调用的性能。
解决思路
在每个进程创建时,分配一个物理页面,将其与 USYSCALL
页面建立映射关系,用于存放进程号PID。(如下图所示这个地址在TRAPFRAME
下面,kernel/memlayout.h
文件中定义)进行映射。
之后再获取PID时就可以不陷入内核态,直接访问 USYSCALL
页面即可。
xv6中的定义如下:
|
|
因此要做的事情很简单:
创建进程时:
- 创建进程的时候分配物理页。
- 建立虚拟地址和物理地址的映射关系。
- 给这个
USYSCALL
的pid赋值。
销毁进程时:
- 归还这个页的物理内存。
- 解除映射关系。
实现
创建进程时:
创建进程的时候分配物理页(
kernel/proc.c@allocproc
)。1
2
3
4
5
6
7
8
9+ if((p->usyscallframe = (struct usyscall *)kalloc()) == 0){
+ freeproc(p);
+ release(&p->lock);
+ return 0;
+ }
+
// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){建立虚拟地址和物理地址的映射关系(
kernel/proc.c@proc_pagetable
)。1
2
3
4
5
6
7
8
9
10
11+ // map pid
+ if(mappages(pagetable, USYSCALL, PGSIZE,
+ (uint64)(p->usyscallframe), PTE_R | PTE_U) < 0){
+ uvmunmap(pagetable, TRAMPOLINE, 1, 0);
+ uvmunmap(pagetable, TRAPFRAME, 1, 0);
+ uvmfree(pagetable, 0);
+ return 0;
+ }
+
return pagetable;
}给这个
USYSCALL
的pid赋值(kernel/proc.c@allocproc
)。1
2
3
4
5+ p->usyscallframe->pid = p->pid;
+
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
销毁进程时:
归还这个页的物理内存(
kernel/proc.c@freeproc
)。1
2
3
4
5
6
7if(p->trapframe)
kfree((void*)p->trapframe);
+ if(p->usyscallframe)
+ kfree((void*)p->usyscallframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);解除映射关系(
kernel/proc.c@proc_freepagetable
)。1
2
3
4
5
6
7void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
+ uvmunmap(pagetable, USYSCALL, 1, 0);
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, sz);
task 2: Print a page table (easy)
题意描述
Define a function called
vmprint()
. It should take apagetable_t
argument, and print that pagetable in the format described below. Insertif(p->pid==1) vmprint(p->pagetable)
in exec.c just before thereturn argc
, to print the first process’s page table. You receive full credit for this part of the lab if you pass thepte printout
test ofmake grade
.
打印pid=1的用户页表,效果如下,需要显示出三级页表的结构:
解决思路
可以参考 walk
函数的实现:
|
|
其中,查找页表中一个PTE的关键代码是:
|
|
借助上面的在页表中查找PTE的代码,采用dfs就行(纯算法题),注意输出格式即可。
实现
kernel/vm.c
:
|
|
task 3: Detecting which pages have been accessed (hard)
题意描述
Some garbage collectors (a form of automatic memory management) can benefit from information about which pages have been accessed (read or write). In this part of the lab, you will add a new feature to xv6 that detects and reports this information to userspace by inspecting the access bits in the RISC-V page table. The RISC-V hardware page walker marks these bits in the PTE whenever it resolves a TLB miss.
Your job is to implement
pgaccess()
, a system call that reports which pages have been accessed. The system call takes three arguments. First, it takes the starting virtual address of the first user page to check. Second, it takes the number of pages to check. Finally, it takes a user address to a buffer to store the results into a bitmask (a datastructure that uses one bit per page and where the first page corresponds to the least significant bit). You will receive full credit for this part of the lab if thepgaccess
test case passes when runningpgtbltest
.
给页表Flags中添加一位用于标志页面是否被访问,并实现pgaccess系统调用。
系统调用pgaccess:获取从上次pgaccess到现在,一段虚拟内存空间的页面是否被访问过。
- 输入:页面起始地址、页面数量、返回结果地址,
- 输出:通过位图保存的页面的access状态,将其复制到输入的返回结果地址中。
解决思路
首先要明确的是,我们要利用PTE的FLAGS,其形式如下,可以看到在Sv39硬件下,有一个A标记位:
参考RISC-V的资料[1],可以看到下面一句话:
Each leaf PTE contains an accessed (A) and dirty (D) bit. The A bit indicates the virtual page has been read, written, or fetched from since the last time the A bit was cleared. The D bit indicates the virtual page has been written since the last time the D bit was cleared.
所以第6位即是我们要利用的标记,称之为 PTE_A
。
|
|
那么此时只要搞清楚需要谁来设置这个标记就可以了:
根据以下lab文档描述,可以看出,PTE标记是MMU硬件去设置的(置1)。
The RISC-V hardware page walker marks these bits in the PTE whenever it resolves a TLB miss.
根据RISC-V的资料[1],可以看出,PTE标记是需要操作系统这个软件来清除的(置0)。
Mandating that the PTE updates to be exact, atomic, and in program order simplifies the spec- ification, and makes the feature more useful for system software. Simple implementations may instead generate page-fault exceptions.
The A and D bits are never cleared by the implementation. If the supervisor software does not rely on accessed and/or dirty bits, e.g. if it does not swap memory pages to secondary storage or if the pages are being used to map I/O space, it should always set them to 1 in the PTE to improve performance.以及
For non-leaf PTEs, the D, A, and U bits are reserved for future standard use and must be cleared by software for forward compatibility.
综合来说,PTE_A
的设置(置1)是MMU完成的,清除(置0)是操作系统完成的。
这样,这个task就比较好完成了,实现一个系统调用去访问给定范围这些PTE,得到一个bitmap的结果,然后再将PTE_A
置0即可。
实现
先定义 PTE_A
:(kernel/riscv.h
)
|
|
实现系统调用,是用户态程序与内核交互的接口:(kernel/sysproc.c
)
|
|
实现对应的获取访问标志位的bitmap的功能,并把PTE_A
标记清除:(kernel/proc.c
)
|
|
需要注意的是,把内核数据往用户空间写要使用 copyout
方法:该方法将页表看作是一个数据结构,然后使用软件模拟MMU功能的方式,将虚拟地址转换为物理地址,来进行数据读写。
|
|
切换内核态的时候,cpu的参数没变(这个参数的改变和调度相关),因此可以在内核态用 myproc
访问当前用户态的进程信息:
|
|
测试结果
执行 make grade
进行评分:
代码仓库地址:代码