Lab链接:Lab: page tables
Lab源码:momo/MIT-6S081/pgtbl - Gitee.com
1 Speed up system calls
题目
创建进程时,在虚拟地址USYSCALL
映射一个只读页面。在此页面的开头,存储一个结构体usyscall
(也在memlayout.h
中定义),并以当前进程PID初始化该结构体。
思路
在 kernel/proc.h
中:
- 在进程结构体
struct proc
中增加字段struct usyscall *usyscall;
,用于存储指向共享页的指针
在 kernel/proc.c
中:
- 创建进程时,需为共享页分配物理页,并将其映射到进程的虚拟地址空间
- 在销毁进程页表之前,必须取消所有物理页在虚拟内存中的映射,确保资源正确释放。因此,无论是进程正常退出,还是创建进程过程中因映射失败而退出,都应在调用页表销毁函数之前,先执行取消映射的操作
报错:【panic:freewalk:leaf】
定位:只有uvmfree调用了freewalk;多处调用uvmfree之前使用uvunmap unmap页表项
释放页表之前,要先unmap页表的对应关系,即清除pte中的PTE_V为0:
- 三、二级页表:(pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0
- 一级页表:pte & PTE_V == 1
freewalk在释放一级页表时,如果一级页表项是有效的(pte & PTE_V == 1),则发生上述错误
所以调用freewalk之前,需要unmap一级页表页表项
源码
kernel/proc.h
// struct proc
struct usyscall *usyscall;
kernel/proc.c
void allocproc() {
// Allocate a USYSCALL page for kernel to share with
if((p->usyscallpage = (pagetable_t)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
p->usyscall->pid = p->pid;
}
void proc_pagetable() {
// map the usyscall just below USYSCALL
if(mappages(pagetable, USYSCALL, PGSIZE,
(uint64)(p->usyscall), PTE_R | PTE_U) < 0){
uvmunmap(pagetable, USYSCALL, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
}
void freeproc() {
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;
}
void proc_freepagetable(pagetable_t pagetable, uint64 sz) {
uvmunmap(pagetable, USYSCALL, 1, 0);
// ...
}
2 Print a page table
题目
打印进程页表中所有页表项的信息
思路
定义两个函数:
vmprint(pagetable_t)
:接收进程页表的地址作为参数vmsprint(pagetable_t, int level)
:接收任意页表地址和当前页表层级,递归打印该页表及其所有子页表项的信息
vmsprint(pagetable_t, int level)
的具体流程如下:
- 若当前为最末级页表(
level == 3
),说明已是数据页,无需进一步处理,直接返回 - 否则,当前页表为中间层级页表,需遍历其所有页表项
- 对于每一个有效的页表项(
pte & PTE_V
):- 打印该页表项对应的虚拟地址范围、物理地址及权限标志
- 传入下一级页表地址和层级,递归调用
vmsprint
源码
kernel/vm.c
void vmprint(pagetable_t pagetable) {
printf("page table %p\n", *pagetable);
vmsprint(pagetable, 0);
}
void vmsprint(pagetable_t pagetable, int depth) {
if(depth == 3)
return;
pte_t pte;
uint64 child;
for(int i = 0; i < 512; ++i) {
pte = pagetable[i];
if(pte & PTE_V) {
printf("..");
for(int k = 0; k < depth; ++k) {
printf(" ..");
}
child = PTE2PA(pte);
printf("%d: pte %p pa %p\n", i, pte, child);
vmsprint((pagetable_t)child, depth+1);
}
}
}
C语言中,使用%p
占位符打印指针变量的值,也就是虚拟地址。
int *p;
printf("the address is %p\n", p);
PTE_R & PTE_W & PTE_X // x
3 Detecting which pages have been accessed
题目
实现pgaccess
系统调用,汇报哪些page在上次该系统调用之前被修改或访问过,包含以下三个参数:
- va:待检查的第一个user page的虚拟地址
- checknum:检查页数
- dstva:位于用户空间,用于存储结果的缓冲区的虚拟地址(最低位对应第一页)
思路
- 计算虚拟地址
va
对应页的起始地址 - 调用
walk()
查找该页对应的页表项 - 若页表项中访问位(
PTE_A
)已被设置,则将该页在掩码(mask)中的对应位置为1,并清除PTE_A
位,防止干扰后续检查 - 将最终得到的掩码从内核空间复制回用户空间
源码
kernel/riscv.h
#define PTE_A (1L << 6) // 必须设置为6
kernel/sysproc.c
直接利用walk版
int sys_pgaccess(void) {
uint64 va, base_va, mask_addr;
int len, i;
unsigned int mask = 0;
struct proc *p;
pte_t *pte;
if(argaddr(0, &base_va) < 0 || argint(1, &len) < 0 || argaddr(2, &mask_addr) < 0)
return -1;
if(len <= 0 || len >= MAX_PGACCESS_CNT)
return -1;
if(base_va >= MAXVA)
return -1;
p = myproc();
va = PGROUNDDOWN(base_va);
for(i = 0; i < len; i++) {
pte = walk(p->pagetable, va, 0);
if(!pte)
return -1;
if(*pte & PTE_A) {
mask = mask | (1 << i);
*pte = *pte & (~PTE_A);
}
va += PGSIZE;
}
if(copyout(p->pagetable, mask_addr, (char*)(&mask), sizeof(mask)) < 0)
return -1;
return 0;
}
自行寻找页表项版
uint64 sys_pgaccess(void) {
uint64 va, dstva, mask = 0L;
int i, level, checknum;
struct proc *p;
pagetable_t pagetable;
pte_t *pte;
if(argaddr(0, &va) < 0 || argint(1, &checknum) < 0 || argaddr(2, &dstva) < 0)
return -1;
if(va >= MAXVA || checknum > 64)
return -1;
va = PGROUNDDOWN(va);
p = myproc();
for(i = 0; i < checknum; ++i) {
pagetable = p->pagetable;
for(level = 2; level > 0; level--) {
pte = &pagetable[PX(level, va)];
pagetable = (pagetable_t)PTE2PA(*pte);
}
pte = &pagetable[PX(0, va)];
if(*pte & PTE_A) {
mask |= 1 << i;
*pte &= (~PTE_A);
}
va += PGSIZE;
}
if(copyout(p->pagetable, dstva, (char *)&mask, (uint64)(sizeof(mask))) < 0)
return -1;
return 0;
}
#define PTE_A (1L << 5) // x