Lab page tables
内核地址空间,进程地址空间。
地址映射
守护页,PTE的flags
物理内存分配
sbrk和exec
Speed up system calls
通过在用户空间和内核之间的只读区域共享数据加速特定的系统调用,执行这些系统调用可以不再进入内核。本实验可以学习向页表中插入映射。
实验方法:当进程创建时,将地址 USYSCALL
映射为只读页。在该页的起始处,存储一个 struct usyscall
,设为当前进程的 pid 。ugetpid()
已经实现了用户空间的代码。
在 struct proc
中加入指向共享区域的指针。(kernel/proc.h)
struct usyscall *usyscall; // shared data page to speed up system calls.
(kernel/proc.c)
在allocproc
中分配物理页,并设置该页内容。
// Allocate a USYSCALL page which is shared with kernel.
if((p->usyscall = (struct usyscall *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
p->usyscall->pid = p->pid;
在freeproc
中释放物理页。
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;
在proc_pagetable
中完成虚拟地址到物理地址的映射,并设置权限。
// map the USYSCALL just below TRAPFRAME, for speeding system calls.
if(mappages(pagetable, USYSCALL, PGSIZE,
(uint64)(p->usyscall), PTE_R | PTE_U) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
在proc_freepagetable
中完成映射的解除。
uvmunmap(pagetable, USYSCALL, 1, 0);
这里参数0代表不free物理页,因为在 freeproc
中已经释放。
此处如果不解除映射,会造成系统执行 exec 系统调用成功后,free 旧的镜像,运行到 uvmfree
,free 掉所有的三级页表 PTE 的映射(大小按照 p.sz
,不包括 USYSCAL这个页),直到freewalk
,会发现 USYSCALL 这页的 PTE 没有free(不等于0),且 PTE_R 存在(是三级页表的PTE),且 PTE_V 存在(该页映射存在),然后会panic("freewalk: leaf")
。
这里 debug 的方法总结:在 panic 处用 gdb 的 backtrace
查看调用栈,然后找到 uvmfree
查看 sz 发现只有 \(4096B\) ,也就是 uvmfree
不会解除 USYSCALL 处的映射。需要去 proc_freepagetable
中解除。
Print a page table
根据进程的 p->pagetable
按照特定格式打印出该进程的页表。
模仿 walk
的实现:从一级页表开始遍历,找出 valid PTE,按照是一级页表的 PTE ,二级页表的 PTE,还是三级页表的 PTE,设置一定的输出格式。
PTE2PA
用于实现将 PTE 转换为物理地址(Chapter3:取 \(44\) 位的 PPN 和全 \(0\) 的低12位,因为 \(4096B\) 对齐格式,所以低 \(12\) 位全 \(0\))。
结果显示一级页表项不是 \(512\) 个,而是 \(255\) 个,为什么?
Preparation 中说明虚拟地址本应为 \(39\) 位(这个位数是硬件提供的位数),但 xv6 只使用了 \(38\) 位,一级页表的页表项只有 \(256\) 个。为了简单,将内核的 text 和 data 被放在一页。
// Recursively print a page table of the process.
void
walkprint(pagetable_t pagetable, int level)
{
for (int i = 0; i < 512; i++) {
pte_t pte = pagetable[i];
if (pte & PTE_V) {
if (level == 1) {
printf("..");
} else if (level == 2) {
printf(".. ..");
} else {
printf(".. .. ..");
}
printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
if ((pte & (PTE_X|PTE_R|PTE_W)) == 0) {
uint64 child = PTE2PA(pte);
walkprint((pagetable_t)child, level + 1);
}
}
}
}
void
vmprint(pagetable_t pagetable)
{
printf("page table %p\n", pagetable);
walkprint(pagetable, 1);
}
Detecting which pages have been accessed
一些 GC 的实现或者自动内存管理需要哪些页被读写过的信息。本实验需要通过检测 RISC-V 页表的 access bits,并向用户空间传递这样的信息。
pgaccess
系统调用:向用户返回哪些页被访问过。接受三个参数:一个虚拟地址base,一个页长度len,一个返回结果的地址maskaddr。
实现:虚拟地址所在页作为第一个页,从这个页开始之后len个页,找出每个页对应的 PTE(通过walk
),判断每个 PTE 的 PTE_A 位,记录结果在 unsigned int mask
中,然后将该位clear,第一页的结果作为mask的最低有效位。最后将结果传输到用户空间(copyout
)。
系统调用需要校验参数,len不能大于 \(32\) ,因为 unsigned int 可以存储的结果位为 \(32\) 位。
最终需要clear PTE_A位,否则下次无法判断之前是否access过。
定义访问位 PTE_A
(kernel/riscv.h)
#define PTE_A (1L << 6)
// The A bit indicates the virtual page has been read, written, or fetched from since the last time the A bit was cleared.
完成系统调用的实现,从用户空间获取参数并check。
(kernel/sysproc.c)
int
sys_pgaccess(void)
{
uint64 base;
int len;
uint64 mask;
if(argaddr(0, &base) < 0 || argint(1, &len) < 0
|| argaddr(2, &mask) < 0)
return -1;
// set an upper limit to unsigned int mask
if (len > 32) {
return -1;
}
return pgaccess(base, len, mask);
}
通过walk
找出需要判断是否访问过的页的PTE,并clear掉PTE_A,将结果传至用户空间。
// Mask the page accessed from base address
// to the bits which matched the order of pages.
int
pgaccess(uint64 base, int len, uint64 maskaddr)
{
struct proc *p = myproc();
uint mask;
for (int i = 0; i < len; i++) {
pte_t *pte;
pte = walk(p->pagetable, base + PGSIZE * (uint64)i, 0);
if (pte != 0 && ((*pte) & PTE_A)) {
mask |= (1 << i);
*pte &= ~PTE_A;
}
}
if (copyout(p->pagetable, maskaddr, (char *)&mask, sizeof(mask)) < 0)
return -1;
return 0;
}