前面我们修改了原有的分支仓库名, origin -> old-origin,目的就是为了将完成后的实验推送到origin处。使用git branch命令观察远程分支可以看到如下输出。
bash
$ git branch -r
old-origin/cow
old-origin/fs
old-origin/lock
old-origin/mmap
old-origin/net
old-origin/pgtbl
old-origin/riscv
old-origin/syscall
old-origin/thread
old-origin/traps
old-origin/util
origin/pgtbl
origin/syscall
现在我们想要在old-origin/pgtbl基础上,新建一个pgtbl的本地分支并推送到origin仓库,可以按下面步骤操作:
sql
$ git checkout old-origin/pgtbl 先切换到old-origin/pgtbl分支
...
HEAD is now at 1e6b2de pgtbl lab: initial commit
$ git checkout -b pgtbl 在此分支基础上创建本地分支pgtbl
Switched to a new branch 'pgtbl'
$ git push -u origin pgtbl 推送到远程仓库
...
* [new branch] pgtbl -> pgtbl
branch 'pgtbl' set up to track 'origin/pgtbl'.
再次使用git branch命令应该可以看到origin/pgtbl
bash
$ git branch -r
old-origin/cow
old-origin/fs
old-origin/lock
old-origin/mmap
old-origin/net
old-origin/pgtbl
old-origin/riscv
old-origin/syscall
old-origin/thread
old-origin/traps
old-origin/util
origin/pgtbl
origin/syscall
origin/util
Lab: page tables
这一关的代码量不多,主要是对内核空间、内存管理的理解。
Speed up system calls
为了减少因系统调用引起的上下文切换、内核态转换所带来的开销,这里为用户进程和其内核态分配一个共享page,page内容在内核下设置,在用户进程下可以读取内容。
这一关加速的是getpid()调用,使进程在用户态下可以直接读取当前进程pid。
在kernel/proc.h中添加共享页面的定义:
arduino
// Per-process state
struct proc {
...
struct usyscall *usyscall; // 共享 page
...
};
在kernel/proc.c中,进程创建页表时为USYSCALL映射物理页面
mappages
:这是一个函数,用于将虚拟地址映射到物理地址。它的参数通常包括:
pagetable
:要修改的页表。USYSCALL
:映射的起始虚拟地址。PGSIZE
:映射的页面大小。(uint64) (p->usyscall)
:要映射的物理地址,这里从进程结构p
中获取usyscall
字段。PTE_R | PTE_U
:页表项的属性,这里设置为可读(PTE_R
)和用户可访问(PTE_U
)。
scss
// Create a user page table for a given process,
// with no user memory, but with trampoline pages.
pagetable_t
proc_pagetable(struct proc *p)
{
...
// map the trapframe just below TRAMPOLINE, for trampoline.S.
if (mappages(pagetable, TRAPFRAME, PGSIZE,
(uint64)(p->trapframe), PTE_R | PTE_W) < 0)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
// 映射共享页面
if (mappages(pagetable, USYSCALL, PGSIZE,
(uint64)(p->usyscall), PTE_R | PTE_U) < 0)
{
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
return pagetable;
}
经过上一步我们可以在用户进程中通过访问USYSCALL来获取**p->usyscall
** 了
在kernel/proc.c创建进程时,开辟并初始化共享页面,使其真正指向一片内存区域,并将pid保存在共享页面中
rust
static struct proc *
allocproc(void)
{
...
// 共享页面
if ((p->usyscall = (struct usyscall *)kalloc()) == 0)
{
freeproc(p);
release(&p->lock);
return 0;
}
...
// 保存pid
p->usyscall->pid = p->pid;
return p;
}
/kernel/proc.c 在释放进程时,释放共享页面
scss
static void
freeproc(struct proc *p)
{
...
// 释放共享页面
if (p->usyscall)
{
kfree((void *)p->usyscall);
}
p->usyscall = 0;
...
}
/kernel/proc.c 在释放进程页表时,移除对应的页面
scss
void proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
//移除共享页面
uvmunmap(pagetable, USYSCALL, 1, 0);
uvmfree(pagetable, sz);
}
Print a page table
要求实现打印页表,并在exec中执行。
/kernel/defs.h 中定义函数
arduino
//vm.c
void vmprint(pagetable_t);
/kernel/exec.c 中,在执行pid为1的进程时打印页表:
arduino
int exec(char *path, char **argv)
{
...
// 当pid == 1时 打印页表
if (p->pid == 1)
{
vmprint(p->pagetable);
}
return argc; // this ends up in a0, the first argument to main(argc, argv)
bad:
...
}
/kernel/vm.c 中,实现打印页表逻辑,就是一个简单的递归实现深度优先遍历,根据层级调整输出格式,并通过页表有效位PTE_V判断是否打印,页表的读写执行权限位判断是否为叶子结点:
scss
int pgtblprint(pagetable_t pagetable, int depth) {
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if(pte & PTE_V) { // 如果页表项有效
// 按格式打印页表项
printf("..");
for(int j=0;j<depth;j++) {
printf(" ..");
}
printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
// 如果该节点不是叶节点,递归打印其子节点。
if((pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
pgtblprint((pagetable_t)child,depth+1);
}
}
}
return 0;
}
// 打印页表
void vmprint(pagetable_t root)
{
printf("page table %p\n", root);
pgtblprint(root, 0);
}
Detecting which pages have been accessed
要求实现pgaccess()系统调用,用于获取哪些页表已被访问,接收3个参数:
- 首先,接收要检查的第一个用户页面的起始虚拟地址。 va
- 其次,它接收要检查的页面数量。 pnum
- 最后,它接收一个用户地址,指向一个缓冲区,用于将结果存储到位掩码中。 Ua
/kernel/riscv.h中定义访问位PTE_A
arduino
#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_A (1L << 6) // 1 -> 页面被访问过
/kernel/sysproc.c中实现sys_pgaccess()
这里要使用walk(),先声明。标记的过程并不复杂,主要是综合使用。
scss
extern pte_t *walk(pagetable_t, uint64, int);
#ifdef LAB_PGTBL
int sys_pgaccess(void)
{
uint64 va, ua;
int pnum; /* pnum-扫描页面数 */
// get args
if (argaddr(0, &va) < 0 ||
argint(1, &pnum) < 0 ||
argaddr(2, &ua) < 0)
return -1;
// 若扫描的页大于PGSIZE*8 返回-1
if (pnum > 8*PGSIZE)
return -1;
// 开辟缓冲区
char *buf = kalloc();
// 初始化缓冲区
memset(buf, 0, PGSIZE);
//依次扫描页面
for(int i=0;i<pnum;i++){
pte_t *p = walk(myproc()->pagetable, va + i*PGSIZE, 0);
if(*p & PTE_A){
// 访问过,标记并重置
buf[i/8] |= 1<<(i%8);
*p &= ~PTE_A;
}
}
//结果传递给用户空间
copyout(myproc()->pagetable, ua, buf, pnum);
//释放页面
kfree(buf);
return 0;
}
#endif
实验完成
make grade 验证
ini
== Test pgtbltest ==
$ make qemu-gdb
(5.3s)
== Test pgtbltest: ugetpid ==
pgtbltest: ugetpid: OK
== Test pgtbltest: pgaccess ==
pgtbltest: pgaccess: OK
== Test pte printout ==
$ make qemu-gdb
pte printout: OK (1.0s)
== Test answers-pgtbl.txt == answers-pgtbl.txt: FAIL
Cannot read answers-pgtbl.txt
== Test usertests ==
$ make qemu-gdb