Lab 3: page tables


Lab 3: page tables

环境配置

xv6 代码切换到pgtbl 分支并初始化

1
2
3
$ git fetch
$ git checkout pgtbl
$ make clean

一、加速系统调用(难度:easy)

在内核和用户态之间创建一个共享的只读页,是用户态能够直接读取内核态写入的数据,从而加速系统调用。这里的页指页表中用户态和内核态均可读的一个 PTE,其指向一块物理内存。

1、基本原理

用户态是不能直接读取内核态的数据,而是要通过系统调用。如果创建一个可读 PTE 指向一块内存,该 PTE 是用户态和内核态共享的,那么用户态就可以直接读取这块内核数据,而无需经过复杂的系统调用。为每一个进程多分配一个虚拟地址位于 USYSCALL 的页,然后这个页的开头保存一个 usyscall 结构体,结构体中存放这个进程的 pid

2、具体实现

  • USYSCALL 页是独立于进程页表的一个页,把定义加到kernel/proc.hproc 结构体中:

    1
    2
    3
    4
    5
    struct 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
    16
    static 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
    11
    pagetable_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
    8
    static void freeproc(struct proc *p)
    {
    ...
    if (p->usyscall)
    kfree((void *)p->usyscall);
    p->usyscall = 0;
    ...
    }
  • 解除页表中的映射

    1
    2
    3
    4
    5
    6
    void 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
    29
    void 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
    #define PTE_A (1L << 6) // 1 -> have accessed
  • kernel/sysproc.c 中完善 sys_pgaccess 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #ifdef LAB_PGTBL
    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);
    }
    #endif
  • kernel/proc.c 中 新增 pgaccess 函数,用于具体实现页的访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    uint64 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);

实验结果


文章作者: leven
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 leven !
评论
  目录