Appearance
page tables
补充内容:Chapter 3: Page Tables - 知乎 (zhihu.com)
开启新实验
git fetch
git checkout pgtbl
make clean
Speed up system calls
为了加速系统调用,很多操作系统都会在用户空间内开辟一些只读的虚拟内存,内核会把一些数据分享在这里。这样就可以减少来回在用户态和内核态中切换的操作。
任务描述:为系统调用getpid()
实现这样的加速。
这是这个任务中需要使用到的重要函数mappages()
,它在kernel/vm.c
中被实现。
它实现将pagetable的从va开始大小为size的空间映射到物理地址pa,并设置标志位为perm。
- 在
kernel/proc.h
中给struct proc
添加成员usyscall
struct usyscall *usyscall;
- 在
kernel/proc.c
的proc_pagetable()
函数中实现,当进程创建时在USYSCALL映射一个只读的页
proc_pagetable()
会在创建新进程时被调用,符合我们的要求。
观察一下proc_pagetable()
是如何使用mappages()
来创建 trampoline 和 trapframe 页的:
如果映射失败,需要uvmunmap()
取消之前映射成功的映射,并uvmfree()
释放内存,然后返回。
在kernel/riscv.h
中有标志位的定义:
对于本任务来说,标志位应该是PTE_R | PTE_U
,代表允许读,和允许用户访问。
实现代码如下:
if(mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p->usyscall), PTE_R | PTE_U) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAM, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
我们已经成功创建了从虚拟内存到物理的映射,但是并没有在创建进程的时候申请物理内存(如果没申请物理内存,就会把一个虚拟内存映射到空指针上)。
- 接下来在
kernel/proc.c
的allocproc()
函数中为USYSCALL申请物理内存,并初始化p->usyscall
。
可以照抄参考allocproc()
中给 trapframe 分配物理内存的过程:
if((p->usyscall = (struct usyscall *)kalloc()) == 0){
freeproc(p->usyscall);
release(&p->lock);
return 0;
}
p->usyscall->pid = p->pid;
- 还需要修改
kernel/proc.c
中的freeproc()
,同样参考对trapframe的处理即可。
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;
- 虽然Hint里没说,但是还需要修改
kernel/proc.c
中的proc_freepagetable()
函数,取消USYSCALL的映射。(不然会panic freewalk leaf
)
uvmunmap(pagetable, USYSCALL, 1, 0);
用户态的函数就不需要我们自己写了,根据实验提示,已经在 user\ulib.c
中实现了。
Print a page table
任务描述:如题。
- 在
kernel/vm.c
添加vmprint()
函数,接收一个pagetable_t
参数。
因为 xv6 的页表是三级的,所以是一个树的结构,那么本质上就是需要写一个dfs打印树的函数。还需要层数的信息所以多写了一个函数比较方便。
可以参考同样在kernel/vm.c
中的freewalk()
函数的实现。
int vmprint_dfs(pagetable_t pagetable, int dep)
{
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if(pte & PTE_V){ // 判断是否存在
uint64 child = PTE2PA(pte);
for(int j = 0; j < dep; j++)
printf(".. ");
printf("%d: pte %p pa %p\n", i, pte, child);
if(dep < 3) vmprint_dfs((pagetable_t)child, dep+1);
}
}
return 0;
}
int vmprint(pagetable_t pagetable)
{
printf("page table %p\n", pagetable);
vmprint_dfs(pagetable, 1);
return 0;
}
- 在
kernel/exec.c
中return argc;
之前插入以下代码:
if(p->pid == 1) vmprint(p->pagetable, 0);
因为 init
是系统创建的第一个进程,所以 init
的 pid 是 1,那么在创建 init 时,就会打印这个页表。
Detecting which pages have been accessed
任务描述:实现一个 pgaccess()
函数,这个函数的申明为:int pgaccess(void *base, int len, void *mask);
。这个函数的主要作用就是检测从上次调用这个函数开始,页表是否被访问过。其中 base
参数是要检测的第一个页表,len
从这个页表开始,要检测多少个页表,而我们需要把每个页表的访问情况写到 mask
上,如果当前页表被访问,那么 mask
中对应的位应该是 1。
- 需要自行在
kernel/riscv.h
中定义一下PTE_A
查阅资料后得知,记录是否访问的位置是第六位。
#define PTE_A (1L << 6)
- 在
kernel/sysproc.c
实现sys_pgaccess()
需要使用到kernel/vm.c
中的walk()
函数,对于一个给定的页表和虚拟地址,walk()
函数会返回对应这个虚拟地址的叶子 PTE。
int sys_pgaccess(void)
{
// 接收参数
int len;
uint base, mask_addr;
if(argaddr(0, &base) < 0) return -1;
if(argint(1, &len) < 0) return -1;
if(argaddr(2, &mask_addr) < 0) return -1;
// 设置上限,因为更长的话mask位数不够
if(len > 32) return -1;
int mask = 0;
pagetable_t pagetable = myproc()->pagetable;
pte_t *pte = walk(pagetable, base, 0);
for(int i = 0; i < len; i++){
// 如果页表存在且访问过
if((pte[i] & PTE_A) && (pte[i] & PTE_V)){
mask |= (1 << i); // mask置位
pte[i] ^= PTE_A; // PTE_A复位
}
}
// 将mask写入指定位置
if(copyout(pagetable, mask_addr, &mask, sizeof(mask)) < 0)
return -1;
return 0;
}
受不了了
walk()
为啥没在kernel/defs.h
里声明!!!
The End
(这个最后一个测试是在干啥???是不是要等久一点我直接给终止了。。)
一个主要考察页表的Lab。