做 Lab 3 需要提前阅读 XV6 book,了解 RISC-V SR39 的地址格式,并且实验中大量用到了页表的准换函数,需要查阅 XV6 手册。不过,熟记 RISC-V 的地址说实在的没有什么用处,通过这个实验理解页表的工作方式并且 hands on 才是真的。
Speed up system calls
目前有很多的操作系统(Linux)在用户区和内核区之间共享一块数据(Read-Only for user),这样用户在进行系统调用的时候就不需要陷入内核态后,由内核态拷贝数据进用户态,而是将数据写在这个共享的区块内。这样可以加快操作系统的运行速度(毕竟大部分系统调用需要的内存消耗是很小的,内存的消耗在当今已经不是问题)。
在本实验中我们需要使用 ugetpid()
来进行加速获得进程的 pid
。
首先就是为每一个进程创建一个页表作为共享内存区块。我们发现在 kernel\memlayout.h
中已经为我们定义好了需要的数据结构:
struct usyscall
{
int pid; // Process ID
};
那么我们只需要在 kernel/proc.c /proc.h
中加入代码,来实现进程创建时创建页表,销毁时销毁页表的动作就行了。
在 proc.h
中,我们需要在进程的 PCB
中加入新的数据结构:
struct proc
{
...
struct usyscall *usyscall; // using read only shared data to accelerate.
...
}
为了让进程的正常创建和释放,我们需要向进程创建和销毁函数中加入对应的页表操作代码。
static struct proc *
allocproc(void)
{
...
found:
...
// -- Modified --
// To alloc a mem for usyscall_page
p->usyscall_page = (struct usyscall *)kalloc();
if (p->usyscall_page == 0) {
freeproc(p);
release(&p->lock);
return 0;
}
...
// -- Modified --
// Init the syscall pid
p->usyscall_page->pid = p->pid;
return p;
}
static void
freeproc(struct proc *p)
{
...
// -- Modified --
// free usyscall page
if (p->usyscall_page) {
kfree((void*)p->usyscall_page);
}
p->usyscall_page = 0;
...
}
因为用户态寻址的时候都要经过页表硬件的翻译,所以 usyscall
也要映射在进程的 pagetable
上。我们需要修改映射函数和释放映射的函数。
pagetable_t
proc_pagetable(struct proc *p)
{
...
if (mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p->usyscall_page), PTE_R | PTE_U) < 0) {
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
...
}
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
// -- Modified --
// free mapping of usyscall page
uvmunmap(pagetable, USYSCALL, 1, 0);
uvmfree(pagetable, sz);
}
总结:
-
先使用 kalloc(kernel malloc) 在系统的页表中申请到一块空间,初始化。
-
然后把这块空间的地址翻译到用户空间对应的页表中去,页表中对应着
USYSCALL
,在kernel\memlayout.h
中是这样定义的#define USYSCALL (TRAPFRAME - PGSIZE)
可以看到这是紧挨着
TRAPFRAME
的。 -
当应用程序调用了
ugetpid()
指令的时候,只需要去USYSCALL
页表找到对应的内容就可以了。而USYCALL
页表的内容实际上是proc.c
中调用kalloc()
后获得的实际物理地址的引用。
Print a page table
按照一定格式打印出页表的信息,这个非常的简单,只要递归的调用就行了。
// Lab 3. Not visiable to user.
void k_vmprint_recur(pagetable_t pgt, int blank) {
for(int i = 0; i < 512; i++) {
pte_t pte = pgt[i];
if (pte & PTE_V) {
uint64 child = PTE2PA(pte);
for (int j = 0; j < blank; j++) {
printf(" ..");
}
printf("%d: pte %p pa %p\n", i, pte, child);
if ((pte & (PTE_R|PTE_W|PTE_X)) == 0) {
k_vmprint_recur((pagetable_t)child, blank+1);
}
}
}
}
// Lab 3
void vmprint(pagetable_t pgt) {
// recursively print the three level page.
printf("page table %p\n", pgt);
k_vmprint_recur(pgt, 1);
}
Detecting which pages have been accessed
一些 GC(garbage cllector) 可以从有关哪些页面已被访问(读取或写入)的信息中获益。 在实验的这一部分,我们将向 xv6 添加一项新功能,通过检查 RISC-V 页表中的访问位来检测此信息并将其报告给用户空间。每当解决 TLB 未命中时,RISC-V 硬件页面遍历器都会在 PTE 中标记这些位。
首先要在 kernel/sysproc.c
中加入系统调用的实现
#ifdef LAB_PGTBL
int
sys_pgaccess(void)
{
// lab pgtbl: your code here.
uint64 buffer, ans;
int number;
if (argaddr(0, &buffer), buffer < 0) return -1;
if (argint(1, &number), number < 0) return -1;
if (argaddr(2, &ans), ans < 0) return -1;
return pgaccess((void*)buffer, number, (void*)ans);
}
#endif
然后着手实现 pgacess
的实现,这个实现需要添加头文件定义,并且在 kernel/proc.c
中添加实现:
uint64
pgaccess(void *pg, int number, void *store) {
struct proc *p = myproc();
if (p == 0) return 1;
int ans = 0;
pagetable_t pagetable = p->pagetable;
for (int i = 0; i < number && i < 32; i++) {
pte_t *pte;
pte = walk(pagetable, (uint64)pg + (uint64)PGSIZE * i, 0);
if (pte != 0 && ((*pte) & PTE_A)) {
ans |= 1 << i;
*pte ^= PTE_A; //clear PTE_A
}
}
// copy the value to user page.
return copyout(pagetable, (uint64)store, (char *)&ans, sizeof(int));
}
这个实现是这样的,通过遍历指定的页表之后的 n 个 page,访问每个 page 的 PTE_A bit,如果这个 bit 是 1,就使用 bit map 来记录他。最终别忘了把这个 int 类型的 ans 返回给用户态。
对于 PTE_A,需要在 kernel\risc.h
中进行添加,查阅 XV6 手册得知,改 PTE_A 位于第 6 位,故为 1 << 6
。