许多操作系统通过共享一系列信息到用户态只读页面来加速某些系统调用的执行时间,因此我们需要实现该功能来加速系统调用 getpid()
每当进程创建的时候.,在 USYSCALL
(该VA定义在 memlayout.h
) 处映射一个只读的物理页,这个物理页的起始位置存储一个 struct usyscall
(同样定义在 memlayout.h
),初始该结构体存储当前进程的 PID
。当前实验,用户态已经准备好了一个应用pgtbltest
,该应用使用ugetpid()
函数获取进程pid
,ugetpid()
函数实现非常简单,就是从用户态的 USYSCALL
虚拟地址处读取 usyscall
中记录的 pid
。
所以,我们要做的事情也很简单,在创建进程的时候,为其用户态页表分配一个物理页,该物理页映射的虚拟地址为 USYSCALL
,然后在该地址处存储一个 struct usyscall
即可。
用户态的代码已经准备完毕,我们只需要改动内核部分即可
allocproc()
申请并初始化物理页freeproc()
释放物理页kernel/proc.c
中的 proc_pagetable()
实现物理地址和虚拟地址的映射,该函数用于创建一个用户态的页表,可以使用 mappages()
完成物理地址和虚拟地址的映射allocproc()
创建进程时申请一个物理页,设置物理页用户态只读, proc_pagetable()
创建用户态页表时,通过 mappages()
建立虚拟地址和物理地址映射kernel/proc.h
// Per-process state
struct proc {
// ...
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct usyscall *usyscall; // 存储我们映射到用户态的物理页的物理地址
// ... 其他代码
};
kernel/proc.c:alloc_proc()
static struct proc*
allocproc(void)
{
struct proc *p;
//...其他代码
found:
// ...其他代码
// Allocate a USYSCALL page (新增代码)
if((p->usyscall = kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
p->usyscall->pid = p->pid;
// An empty user page table.
// ... 其他代码
return p;
}
kernel/proc.c:proc_pagetable()
pagetable_t
proc_pagetable(struct proc *p)
{
pagetable_t pagetable;
// An empty page table.
pagetable = uvmcreate();
if(pagetable == 0)
return 0;
// ... 其他代码
// map the trapframe page just below the trampoline page, for
// trampoline.S.
if(mappages(pagetable, TRAPFRAME, PGSIZE,
(uint64)(p->trapframe), PTE_R | PTE_W) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
// map the usyscall page to USYSCALL (新增代码)
if(mappages(pagetable, USYSCALL, PGSIZE,
(uint64)(p->usyscall), PTE_R | PTE_U) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
return pagetable;
}
kernel/proc.c:proc_freepagetable()
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmunmap(pagetable, USYSCALL, 1, 0); //新增代码
uvmfree(pagetable, sz);
}
kernel/proc.c:free_proc()
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
//新增代码
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;
// 新增结束
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
}
实现一个函数,用于打印页表。定义一个函数 vmprint()
,参数为pagetable_t
,功能为输出这个页表的格式化信息。 同时在 exec.c 的 exec()函数
return argc
之前插入 Insert if(p->pid==1) vmprint(p->pagetable)
,打印第一个进程的页表。
做完上述这些之后,启动 xv6 的时候,应该会输出如下打印(在通过 exec() 加载第一个进程 init 时):
page table 0x0000000087f6b000
..0: pte 0x0000000021fd9c01 pa 0x0000000087f67000
.. ..0: pte 0x0000000021fd9801 pa 0x0000000087f66000
.. .. ..0: pte 0x0000000021fda01b pa 0x0000000087f68000
.. .. ..1: pte 0x0000000021fd9417 pa 0x0000000087f65000
.. .. ..2: pte 0x0000000021fd9007 pa 0x0000000087f64000
.. .. ..3: pte 0x0000000021fd8c17 pa 0x0000000087f63000
..255: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..511: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..509: pte 0x0000000021fdcc13 pa 0x0000000087f73000
.. .. ..510: pte 0x0000000021fdd007 pa 0x0000000087f74000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
init: starting sh
页表输出的时候按照一级、二级、三级、物理页的层次来打印,每一级缩进一个 ..
,同时只打印有效的页表,因此,上述输出中,一级页表只有 0
和 255
的页表项是有效的,0
号页表项索引的二级页表同样只有 0
号页表项有效。二级页表的 0
号页表项索引的三级页表有 0,1,2,3
四个表项有效,指向四个物理页。
kernel/vm.c
中实现 vmprint()
函数kernel/defs.h
中定义 vmprint()
函数的原型,这样就可以在 exec.c
中调用kernel/riscv.h
中的宏freewalk()
的实现方式我们要做的事情也比较简单,实现一个新的函数 vmprint()
即可。同时为了验证我们的函数功能,在exec
中稍加修改,只打印第一个进程的页表,后续该函数作为调试功能存在。
// vm.c
// 其他代码
void vmprint(pagetable_t pagetable); //新增
// 如果是多核OS,会存在哪些问题呢?此时应该怎么实现?
static void __vmprint(pagetable_t pagetable, int deep){
// 页表最大深度为3
if(deep > 3){
return;
}
// 打印第一行输出
if(deep == 0){
printf("page table %p\n", (uint64)pagetable);
}
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
// 打印页表项
if(pte & PTE_V){
//打印页表项前缀
for(int j = 0; j <= deep; j++){
printf("..");
}
printf("%d: pte %p pa %p\n", i, (uint64)pte, (uint64)PTE2PA(pte));
}
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
deep++;
uint64 child = PTE2PA(pte);
__vmprint((pagetable_t)child, deep);
deep--;
}
}
}
void
vmprint(pagetable_t pagetable)
{
__vmprint(pagetable, 0);
}
int
exec(char *path, char **argv)
proc_freepagetable(oldpagetable, oldsz);
//新增代码
if(p->pid == 1){
vmprint(p->pagetable);
}
return argc;
}
一些垃圾回收器需要获取当前哪些页面已经被访问过(读或者写),因此该实验需要我们判断页表的状态位来检查当前页是否被访问过,并报告给用户态。(RISC-V硬件页遍历在解决TLB miss的时候会设置这些bit位)
实现系统调用 pgaccess()
,该调用需要三个参数,分别是要被检查的用户态页的起始虚拟地址,第二个为页的数目,第三个为用户态接收结果的内存地址(结果使用一个 bitmask 保存,每一位表示一个页的访问情况)
user/pgtbltest.c
的 pgaccess_test()
显示了如何调用 pgaccess()
kernel/sysproc.c
中实现 sys_pgaccess()
系统调用argaddr() and argint()
获取参数copyout()
拷贝到用户态kernel/vm.c
中的 walk()
可以用于寻找 PTEs.
kernel/riscv.h
中定义 PTE_A
pgaccess()
访问完这些页表之后,设置这些页表的 PTE_A
状态为 0,因此硬件只会在页表被访问时,给对应的PTE置位,但并不会将其清0,因此如果我们不清0,那么只要该PTE被访问过一次,此后将一直为1vmprint
协助debug因此,我们的目标也很简单,实现一个系统调用,系统调用的大多数步骤实验代码已经写好,我们只需要在 sys_proc.c
中实现 sys_pgaccess()
即可。该系统调用使用 walk()
获取 VA 对应的pte
,判断标志位,汇总并通过 copyout
返回给用户态即可。
//由前面的图可知,PTE_A 在第7个bit上
#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // user can access
#define PTE_A (1L << 6) // 新增PTE_A
#ifdef LAB_PGTBL
int
sys_pgaccess(void)
{
// lab pgtbl: your code here.
uint64 addr;
uint64 a;
int num;
uint64 result_addr;
uint64 result = 0;
pte_t *pte;
argaddr(0, &addr);
argint(1, &num);
argaddr(2, &result_addr);
struct proc* cur = myproc();
for(a = 0; a < num; a++){
if((pte = walk(cur->pagetable, addr + a * PGSIZE, 0)) == 0){
printf("pgacess: page is not exist");
return -1;
}
if(PTE_FLAGS(*pte) & PTE_A){
result = result | (1L << a);
}
*pte &= (~(PTE_A)); //将PTE_A置0
}
if(copyout(cur->pagetable, result_addr, (char *)&result, sizeof(result)) < 0){
printf("pgacess: copyout result to user space failed");
return -1;
}
return 0;
}
#endif