Lab 3: page tables
环境配置
将xv6 代码切换到pgtbl 分支并初始化
1 | $ git fetch |
一、加速系统调用(难度:easy)
在内核和用户态之间创建一个共享的只读页,是用户态能够直接读取内核态写入的数据,从而加速系统调用。这里的页指页表中用户态和内核态均可读的一个 PTE,其指向一块物理内存。
1、基本原理
用户态是不能直接读取内核态的数据,而是要通过系统调用。如果创建一个可读 PTE 指向一块内存,该 PTE 是用户态和内核态共享的,那么用户态就可以直接读取这块内核数据,而无需经过复杂的系统调用。为每一个进程多分配一个虚拟地址位于 USYSCALL 的页,然后这个页的开头保存一个 usyscall 结构体,结构体中存放这个进程的 pid。
2、具体实现
USYSCALL页是独立于进程页表的一个页,把定义加到kernel/proc.h的proc结构体中:1
2
3
4
5struct proc{
...
struct usyscall *usyscall; // to spped up user's syscall
...
}在
kernel/proc.c中初始化该页,在allocproc函数中分配usyscall结构体,并将进程号写入结构体中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16static struct proc * allocproc(void){
...
found:
...
// allocate a syscall page
if ((p->usyscall = (struct usyscall *)kalloc()) == 0) {
freeproc(p);
release(&p->lock);
return 0;
}
// An empty user page table.
p->pagetable = proc_pagetable(p);
...
p->usyscall->pid = p->pid;
return p;
}由于用户态寻址的时候都要经过页表硬件的翻译,所以
usyscall也要映射在进程的pagetable上,在proc_pagetable()中加入映射逻辑:1
2
3
4
5
6
7
8
9
10
11pagetable_t proc_pagetable(struct proc *p){
...
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;
}
return pagetable;
}在进程回收时,需要将该页一起回收
1
2
3
4
5
6
7
8static void freeproc(struct proc *p)
{
...
if (p->usyscall)
kfree((void *)p->usyscall);
p->usyscall = 0;
...
}解除页表中的映射
1
2
3
4
5
6void proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
...
uvmunmap(pagetable, USYSCALL, 1, 0);
uvmfree(pagetable, sz);
}
二、打印页表(难度:easy)
实现一个内核函数 vmprint,其接收一个 pagetable,能够将其中所有的可用 PTE 的信息全部打印出来。
1、解题思路
递归遍历页表,碰到有效的就遍历进下一层页表
2、具体实现
在
kernel/vm.c中新增页表打印函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29void vmprint_kenel(pagetable_t pagetable, int level)
{
for (int i = 0; i < 512; i++)
{
pte_t pte = pagetable[i];
if (pte & PTE_V)
{
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
for (int k = 0; k < level; k++)
{
printf(" ..");
}
printf("%d: pte %p pa %p\n", i, pte, child);
if (level < 3)
{
vmprint_kenel((pagetable_t)child, level + 1);
}
}
}
}
void vmprint(pagetable_t pagetable)
{
printf("page table %p\n", pagetable);
vmprint_kenel(pagetable, 1);
}在
kernel/defs.h函数中声明vmprint函数1
2
3
4...
// vm.c
void vmprint(pagetable_t);
...为了启动内核时打印页表,在
kernel/exec.c中的exec函数中加入以下内容1
2
3
4
5...
if (p->pid == 1)
vmprint(p->pagetable);
return argc; // this ends up in a0, the first argument to main(argc, argv)
...
三、检测页面是否访问(难度:hard)
实验要求实现一个系统调用 sys_pgaccess,其会从一个虚拟地址对应的 PTE 开始,往下搜索一定数量的被访问(read/write)过的页表,并把结果通过 mask 的方式返回给用户。每当 sys_pgacess 调用一次,页表被访问标志就要清 0。
1、解题思路
该题的关键是要解决两个问题:怎么知道哪些页表被访问了、怎么通过虚拟地址依次遍历后续 PTE?
- 怎么知道哪些页表被访问了?
- 检查页表中的
PTE_A标识位。该位被置 1 则说明被访问过,该位被置 0 则说明没被访问过。 - 置位操作有硬件完成,无需我们考虑。但是,硬件只能做到置位,无法做到复位。因此每次
sys_pgacess时要手动将PTE_A复位 0。
- 检查页表中的
- 怎么通过虚拟地址依次遍历后续
PTE?- 首先,通过
walk可得到虚拟地址对应的PTE - 其次,
PTE是连续的,那么对应的虚拟地址也应是连续的 - 最后,一个
PTE大小为PGSIZE,因此只要将虚拟地址按PGSIZE累加即可得到后续的PTE
- 首先,通过
PTE_A 的值在第 6 位。
2、具体实现
在
kernel/riscv.h中加入PTE_A的宏定义1
在
kernel/sysproc.c中完善sys_pgaccess函数1
2
3
4
5
6
7
8
9
10
11
12
13
uint64 sys_pgaccess(void) {
// lab pgtbl: your code here.
// get argument
uint64 buf;
int number;
uint64 ans;
if (argaddr(0, &buf) < 0) return -1;
if (argint(1, &number) < 0) return -1;
if (argaddr(2, &ans) < 0) return -1;
return pgaccess((void*)buf, number, (void*)ans);
}在
kernel/proc.c中 新增pgaccess函数,用于具体实现页的访问1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19uint64 pgaccess(void *addr, int n, void *buf)
{
struct proc *p = myproc();
if (p == 0)
return 1;
pagetable_t pagetable = p->pagetable;
int ans = 0;
for (int i = 0; i < n; i++)
{
pte_t *pte;
pte = walk(pagetable, (uint64)addr + (uint64)PGSIZE * i, 0);
if (pte && ((*pte) & PTE_A))
{
ans |= 1 << i;
(*pte) ^= PTE_A;
}
}
return copyout(pagetable, (uint64)buf, (char *)&ans, sizeof(int));
}在
kernel/defs.h中加入pgaccess函数声明1
uint64 pgaccess(void *addr, int n, void *buf);
实验结果
