MIT 6.S081 Lab3 website

做 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() 后获得的实际物理地址的引用。

按照一定格式打印出页表的信息,这个非常的简单,只要递归的调用就行了。

// 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