Skip to content

page tables

Giovanna

About 1273 wordsAbout 4 min

2024-08-03

Lab: page tables (mit.edu)

补充内容:Chapter 3: Page Tables - 知乎 (zhihu.com)


开启新实验

git fetch
git checkout pgtbl
make clean

Speed up system calls

为了加速系统调用,很多操作系统都会在用户空间内开辟一些只读的虚拟内存,内核会把一些数据分享在这里。这样就可以减少来回在用户态和内核态中切换的操作。

任务描述:为系统调用getpid()实现这样的加速。

这是这个任务中需要使用到的重要函数mappages(),它在kernel/vm.c中被实现。

tmp5D2F.png

它实现将pagetable的从va开始大小为size的空间映射到物理地址pa,并设置标志位为perm。

  1. kernel/proc.h中给struct proc添加成员usyscall
struct usyscall *usyscall;
  1. kernel/proc.cproc_pagetable()函数中实现,当进程创建时在USYSCALL映射一个只读的页

proc_pagetable()会在创建新进程时被调用,符合我们的要求。

观察一下proc_pagetable()是如何使用mappages()来创建 trampoline 和 trapframe 页的:

tmpA694.png

如果映射失败,需要uvmunmap()取消之前映射成功的映射,并uvmfree()释放内存,然后返回。

kernel/riscv.h中有标志位的定义:

tmp8958.png

对于本任务来说,标志位应该是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;
}

我们已经成功创建了从虚拟内存到物理的映射,但是并没有在创建进程的时候申请物理内存(如果没申请物理内存,就会把一个虚拟内存映射到空指针上)。

  1. 接下来在kernel/proc.callocproc()函数中为USYSCALL申请物理内存,并初始化p->usyscall

可以照抄参考allocproc()中给 trapframe 分配物理内存的过程:

tmpEFB8.png

if((p->usyscall = (struct usyscall *)kalloc()) == 0){
    freeproc(p->usyscall);
    release(&p->lock);
    return 0;
}
p->usyscall->pid = p->pid;
  1. 还需要修改kernel/proc.c中的freeproc(),同样参考对trapframe的处理即可。
if(p->usyscall)
	kfree((void*)p->usyscall);
p->usyscall = 0;
  1. 虽然Hint里没说,但是还需要修改kernel/proc.c中的proc_freepagetable()函数,取消USYSCALL的映射。(不然会panic freewalk leaf
uvmunmap(pagetable, USYSCALL, 1, 0);

用户态的函数就不需要我们自己写了,根据实验提示,已经在 user\ulib.c 中实现了。

任务描述:如题。

  1. kernel/vm.c添加vmprint()函数,接收一个pagetable_t参数。

因为 xv6 的页表是三级的,所以是一个树的结构,那么本质上就是需要写一个dfs打印树的函数。还需要层数的信息所以多写了一个函数比较方便。

可以参考同样在kernel/vm.c中的freewalk()函数的实现。

tmp5B14.png

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;
}
  1. 在 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。

  1. 需要自行在kernel/riscv.h中定义一下PTE_A

image.png

查阅资料后得知,记录是否访问的位置是第六位。

#define PTE_A (1L << 6)
  1. kernel/sysproc.c实现sys_pgaccess()

需要使用到kernel/vm.c中的walk()函数,对于一个给定的页表和虚拟地址,walk() 函数会返回对应这个虚拟地址的叶子 PTE。

tmpC13B.png

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

tmp8A50.png

(这个最后一个测试是在干啥???是不是要等久一点我直接给终止了。。)

一个主要考察页表的Lab。