MIT 6.S081 Fall 2020 Lab 3

page tables

学!

Makefile 写好了调试功能, 用的时候需要进行一些准备工作.

首先 make .gdbinit 在目录下生成 .gdbinit 文件, 这个文件就包括了链接远程调试和加载符号表.

接下来需要配置一下 ~/.gdbinit, 添加一行:

add-auto-load-safe-path /path/to/xv6-labs-2020/.gdbinit

如果不加这个的话, gdb 默认有保护, 不会加载当前目录下的 .gdbinit, 这一行相当于是加白名单. 不加这一行启动 gdb 会出现如下警告:

warning: File "/path/to/xv6-labs-2020/.gdbinit" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add
        add-auto-load-safe-path /home/wings/study/MIT-6S081/xv6-labs-2020/.gdbinit
line to your configuration file "/home/wings/.gdbinit".
To completely disable this security protection add
        set auto-load safe-path /
line to your configuration file "/home/wings/.gdbinit".

加好以后, make CPUS=1 qemu-gdb, 然后在另一个终端里启动 gdb: riscv64-unknown-elf-gdb. 用 CPUS=1 是让 qemu 仅使用一个 CPU, 这有利于我们的调试.

先明确几个概念. xv6 (RISC-V) 使用三级页表, 页表中的每一项称为页表项Page Table Entry, PTE. PTE 由两部分组成, 一部分是物理页号Physical Page Number, PPN, 另一部分是标识 flag. PPN 就是物理地址左移 12 位 (因为页表对齐, 后 12 位为 0, 不需要存). flag 有 10 位, 标记的是这个页的一些属性, 如是否可读可写可执行. 由于 RISC-V 物理内存实际上只用了 $2^56$, 所以 PPN 最大为 $2^56 / 2^12 = 2^44$, 所以 PTE 的 PPN 部分只需要 44 位. 一个 PTE 的低 10 位是 flag, 再往上 44 位是 PNN. 所以, 一个 PTE 转换成对应的物理地址, 需要: (pte >> 10) << 12

一些转换的宏在 kernel/riscv.h 中写好了.

kernel/vm.c 中写 vmprint 函数如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void ptprint(pagetable_t pagetable, int level) {
  for (int i = 0; i < 512; i++) {
    pte_t pte = pagetable[i];
    if (pte & PTE_V) {
      for (int j = 1; j <= level; j++)
        printf("..%s", j == level ? "" : " ");
      printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
      if ((pte & (PTE_R | PTE_W | PTE_X)) == 0) {
        pagetable_t child = (pagetable_t)PTE2PA(pte);
        ptprint(child, level+1);
      }
    }
  }
}

void vmprint(pagetable_t pagetable) {
  printf("page table %p\n", pagetable);
  ptprint(pagetable, 1);
}

这里用到了一个小 trick, 如果一个页表是中间级的, 那么 PTE 的 flag 部分, RWX 这些权限标志位都是 0. 所以只需要判断这个, 就知道该不该递归往下走了. 这样不会强制限制页表级数就是 3.

最后按照要求在 exec.cdefs.h 中写相应的代码就可以了.

记一点以前没注意到的知识. 用户程序陷入内核态时, 会进行上下文切换, 这包括了 sp 指针. 也就是用户空间的栈和内核空间的栈不一样. 而内核栈上会保存上下文信息, 所以每个程序都有自己独立的内核栈. 内核栈在内存的 kernel 部分.

程序无论如何都必须使用页表, 无法绕过硬件上的 MMU, 所以内核也有一个内核页表, 里面保存了内核程序, 数据, IO 等映射. 页表只在寻址的时候有用, 而保存地址什么的, 还是可以有物理地址的, 比如 xv6 实现的 kvmmap, 传入的物理地址参数, 它是一个变量而已.

在 xv6 中, 所有程序陷入内核时, 都用的同一张内核页表. 于是内核栈在虚拟内存中也有很多个, 它们被映射在了内核虚拟地址的较高处, 依次排下来. 每个程序的内核栈大小为一页. 为了防止栈溢出导致覆盖其他内核栈, 每个栈上方还有一个 未映射 的页, 称为 守护页Guard Page.

这个 lab 要我们对每个程序维护一个内核页表. 根据提示一步一步来.

kernel/proc.h 中的 struct proc 添加一项 pagetable_t kpagetable;

allocproc 中调用一个自己写的函数, 实现新建内核页表的功能. 仿照新建程序页表的代码, 在 kernel/proc.callocproc 中加入以下代码:

1
2
3
4
5
6
7
  // An proc kernel pagetalbe.
  p->kpagetable = proc_kpagetable(p);
  if (p->kpagetable == 0) {
    freeproc(p);
    release(&p->lock);
    return 0;
  }

proc_kpagetable() 需要实现创建页表, 映射页表的功能. 创建可以使用封装好的 uvmcreate(), 其本质上是 kalloc(). 映射除了内核栈, 需要和全局内核页表映射保持一致.

举个例子, fork 一个程序后, 新的子进程上下文的返回地址是内核态中的 forkret. 父进程放弃 CPU 使用权后, 调度器调度子进程, 由于子进程的运行暂时还不在用户态, 而是 forkret 的内核态, 所以需要先切换该子进程的内核页表, 后续内核栈才不会出错. 然而, 切换在 xv6 中是这么写的 swtch(&c->context, &p->context);, 其中 c 是 CPU, p 是调度的进程. 切换内核页表需要在这句话之前. 而这句话中有数据段的寻址, 必然经过 MMU 的页映射, 所以切换后的程序内核页表要与切换前的全局内核页表数据段映射保持一致. 实际上, ip 也是经过 MMU 映射得到的, 所以代码段也要保持一致. (其他的暂时不知道.)

于是, 新的程序内核页表, 需要与全局内核页表保持一致, 每个程序内核页表也要保持一致.

那么借鉴一下 kernel/vm.c 中的 kvminit()kvmmap(), 不难写出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Create a proc kernel page table for a given process
// Adding kernel memory layout mapping
pagetable_t
proc_kpagetable(struct proc *p) {
  pagetable_t kpagetable;

  kpagetable = uvmcreate();
  if (kpagetable == 0)
    return 0;

  uvmmap(kpagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W);
  uvmmap(kpagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
  uvmmap(kpagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
  uvmmap(kpagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
  uvmmap(kpagetable, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
  uvmmap(kpagetable, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
  uvmmap(kpagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);

  return kpagetable;
}
1
2
3
4
5
6
7
// Add a mapping to an user page table
void
uvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm)
{
  if (mappages(pagetable, va, sz, pa, perm) != 0)
    panic("uvmmap");
}

原本 xv6 是在虚拟地址的最高处, 开辟 16 个 (最大进程个数) 内核栈和 Guard Page. 由于每个程序陷入内核时, 用的是自己的内核页表, 所以需要映射到自己的内核页表上. 先将 xv6 在 procinit() 中初始化内核栈的代码注释掉, 然后在我们写的每个程序新建一个内核页表的地方自己去映射内核栈. 这里就直接在 allocproc() 新建内核页表之后写. 由于每个程序维护一个内核页表, 所以把自己的内核栈映射到自己的虚拟地址最高处, 不必像 xv6 那样依次排列.

1
2
3
4
5
6
7
8
9
  // Allocate a page for the process's kernel stack.
  // Map it high in the process's kernel stack,
  // followed by an invalid, guard page.
  char *pa = kalloc();
  if(pa == 0)
    panic("kalloc");
  uint64 va = KSTACK(0);  // 每个程序内核页表只映射一个自己的内核栈
  uvmmap(p->kpagetable, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
  p->kstack = va;

每个程序一个内核栈, 操作系统也是一个程序, 那操作系统的栈在哪? 是不是还要自己分配? 其实不然. 在操作系统启动的时候, 就已经为 sp 赋了一个值了. 之后进入所谓的 main 函数, 其实已经分配好了栈 (参考 xv6 手册, 我就是没读手册而想不明白 [其实是OS没学明白] ). 这里的内核栈指的是用户程序陷入内核时使用的栈, 而不是操作系统启动时的栈. 当然后续陷入内核, 内核还是使用的程序内核栈, 有点像代码复用? (确实, 从复用的角度理解起来就容易很多了.)

在程序调度前, 需要切换内核页表, 使用程序自己的内核页表. 调度在 kerenl/proc.cscheduler() 中实现.

解释一下 xv6 这个调度器. 它是一个死循环, 遍历所有进程, 找到状态为 RUNNABLE 的, 运行这个程序. 将状态改为 RUNNING, 并且设置 cpu 的进程, 然后 swtch. 注意这个 swtch, 他修改了 ra 寄存器, 最后调用了 ret, 达到了跳转的效果. 也就是这一行 swtch 并不会执行完后到下面的代码, 而是直接去运行 p 对应的程序了. (这个 p 对应的程序依然在内核态, 还需要经过切换才能到用户态). xv6 中, 程序只有自己转让 CPU 的使用权, 才可能跳转回来. 这个在 sched() 函数中实现了 swtch(&p->context, &mycpu()->context);. 同样这里是保存p 的上下文信息, 加载 CPU 之前保存的上下文信息并跳转. 那么这会跳转到哪里呢? 再回头看调度器的 swtch, 是以 C 语言函数的形式出现的, 所以会将 ra 寄存器设置为 swtch 的下一条语句. 然后 ra 被保存在了 cpu context 中. 所以, 程序 sched 中的 swtch, 加载的 cpu context 就回到了调度器 swtch 的下一条. 也就是说, swich 之后, 程序已经不在运行状态了. 正如注释里说的那样.

所以, 我们切换程序自己的内核页表需要在 swtch 执行之前, 运行完切换回全局内核页表要在 swtch 之后. 整个 scheduler() 函数修改如下:

 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns.  It loops, doing:
//  - choose a process to run.
//  - swtch to start running that process.
//  - eventually that process transfers control
//    via swtch back to the scheduler.
void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();
  
  c->proc = 0;
  for(;;){
    // Avoid deadlock by ensuring that devices can interrupt.
    intr_on();
    
    int found = 0;
    for(p = proc; p < &proc[NPROC]; p++) {
      acquire(&p->lock);
      if(p->state == RUNNABLE) {
        // Switch to chosen process.  It is the process's job
        // to release its lock and then reacquire it
        // before jumping back to us.
        p->state = RUNNING;
        c->proc = p;

        // Swtch kernel page talbe
        w_satp(MAKE_SATP(p->kpagetable));
        sfence_vma();

        swtch(&c->context, &p->context);

        // Process is done running for now.
        // It should have changed its p->state before coming back.
        kvminithart();
        c->proc = 0;

        found = 1;
      }
      release(&p->lock);
    }
#if !defined (LAB_FS)
    if(found == 0) {
      intr_on();
      asm volatile("wfi");
    }
#else
    ;
#endif
  }
}

程序结束, 需要释放内核页表. 需要注意的是, 只是释放页表, 而不释放页表中映射的物理内存. 否则释放掉了内核就没了… 但是内核栈是独立的, 可以释放的.

首先在 kernel/proc.cfreeproc() 照猫画虎:

1
2
3
4
5
6
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  if(p->kpagetable)
    proc_freekpagetable(p->kpagetable, p->kstack);
  p->pagetable = 0;
  p->kpagetable = 0;

kpagetable 部分是新增的.

proc_freepagetable() 以及其调用的其他函数可以发现, 需要首先调用 uvmunmap() 去取消 虚拟地址 到 物理地址 的映射. uvmunmap()walk() 找到第三级页表的 pte (可以转换为物理地址), 然后根据参数 do_free 决定是否把物理地址 kfree 掉. 最后页表项 pte 设为 0. 注意, 此时一, 二级页表对应的 pte 还是存在的. 接下来 uvmfree(), 会先把虚拟地址从 0 开始的, 整个程序的映射全给 uvmunmap() 掉, 并且释放物理内存 (xv6 中程序从虚拟地址 0 开始一直往上). 这时, 第三级页表的 pte 就都为 0 了. 最后调用 freewalk(), 递归释放一二级页表.

按照这个逻辑, 来实现 proc_freekpagetable() 这个函数. 首先将 proc_kpagetable() 中创建的一堆映射给 uvmunmap() 掉, 注意 KERNBASE 和 PHYSTOP 之间的内存在映射的时候有 etext 分割, 如果释放也按照这样, 可能会导致大小不是整页 (xv6 实现的 uvmunmap() 要求整页), 所以可以一起释放. (当然也可以修改 uvmunmap() 使其支持非整页). 还要注意这里不能释放掉物理地址.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void
proc_freekpagetable(pagetable_t kpagetable, uint64 kstack)
{
  uvmunmap(kpagetable, UART0, 1, 0);
  uvmunmap(kpagetable, VIRTIO0, 1, 0);
  uvmunmap(kpagetable, CLINT, 0x10000 / PGSIZE, 0);
  uvmunmap(kpagetable, PLIC, 0x400000 / PGSIZE, 0);
  uvmunmap(kpagetable, KERNBASE, (PHYSTOP-KERNBASE) / PGSIZE, 0);
  uvmunmap(kpagetable, TRAMPOLINE, 1, 0);
  ...
}

然后释放内核栈, 注意内核栈虽然是在最高地址减去两个页大小的地方, 但是 Guard Page 没有映射, 所以实际上释放一页就行.

1
2
3
4
5
6
7
void
proc_freekpagetable(pagetable_t kpagetable, uint64 kstack)
{
  ...
  uvmunmap(kpagetable, kstack, 1, 1);
  ...
}

最后释放页表. 由于 freewalk 没有定义在 defs.h 中, 用不了 (当然可以去定义一下就能用了). 这里用 uvmfree() 传参数 sz = 0, 也可以达到相同的效果. 整个函数如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Free a process's kernel page table
// Don't free physical memory except kernel stack
void
proc_freekpagetable(pagetable_t kpagetable, uint64 kstack)
{
  uvmunmap(kpagetable, UART0, 1, 0);
  uvmunmap(kpagetable, VIRTIO0, 1, 0);
  uvmunmap(kpagetable, CLINT, 0x10000 / PGSIZE, 0);
  uvmunmap(kpagetable, PLIC, 0x400000 / PGSIZE, 0);
  uvmunmap(kpagetable, KERNBASE, (PHYSTOP-KERNBASE) / PGSIZE, 0);
  uvmunmap(kpagetable, TRAMPOLINE, 1, 0);
  uvmunmap(kpagetable, kstack, 1, 1);
  uvmfree(kpagetable, 0);
}

hint 给到这就没有了, 但是即使写完调试好运行还是会出问题. 原因在内核态虚拟地址到物理地址转换的 kvmpa() 这个函数, 用的还是 kernel_pagetable. 而这个函数会被一个控制台设备驱动调用, 进行内核栈上的地址的转换. 修改 kvmpa() 如下, 使映射使用程序的内核页表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// translate a kernel virtual address to
// a physical address. only needed for
// addresses on the stack.
// assumes va is page aligned.
uint64
kvmpa(uint64 va)
{
  uint64 off = va % PGSIZE;
  pte_t *pte;
  uint64 pa;
  
  struct proc *p = myproc();

  pte = walk(p->kpagetable, va, 0);
  if(pte == 0)
    panic("kvmpa");
  if((*pte & PTE_V) == 0)
    panic("kvmpa");
  pa = PTE2PA(*pte);
  return pa+off;
}

这么写好像不太对劲, vm.c 中并不使用 proc 的任何内容, 这里加进来就很奇怪.

可以找到唯一调用 kvmpa()virtio_disk.c, 修改如下:

1
2
3
  // buf0 is on a kernel stack, which is not direct mapped,
  // thus the call to kvmpa().
  disk.desc[idx[0]].addr = (uint64) kvmpa((uint64) &buf0, myproc()->kpagetable);

根据注释, 在这里加就感觉很对. 每个程序有它自己的, kernel stack, 映射在自己的内核页表上.

将 user memory 映射到 kernel pagetable 中, 这样内核态在访问用户空间地址的时候 (比如从用户空间拷贝数据), 就不用带一个 user pagetable 了. 上一个实验实现了每个程序独立一个内核页表, 所以这个实验很显然是把用户空间开辟的物理内存, 再映射到内核虚拟地址上去.

根据提示, 需要找到用户空间添加映射的地方, 都再映射一遍到内核页表上去. 这样的地方有: fork(), exec(), sbrk(), userinit(). 还需要注意, PTE 有一位 PTE_U 标志了是否为 user page, 获取 PTE 后, 会检查这一位, 如果是内核态且 PTE_U, 则无法访问. 也就是在映射内核页表的时候需要注意, 不要这一位.

由于程序已经开辟了物理页面, 所以内核页表只需要映射一下, 并不用 kalloc(). 仿照 uvmcopy() 写一个专门处理的函数 vmukmap():

 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

// Copy maps of user page in kernel page table
// Address start from begin to end
// Does not create physical memory, do map to a
// existing page created by and mapped in user page table
// Used by add user memory maps in kernel page table
int
vmukmap(pagetable_t pagetable, pagetable_t kpagetable, uint64 begin, uint64 end)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;
  uint64 begin_page = PGROUNDUP(begin);
  for(i = begin_page; i < end; i += PGSIZE){
    if((pte = walk(pagetable, i, 0)) == 0)
      panic("vmukmap: pte should exist");
    if((*pte & PTE_V) == 0)
      panic("vmukmap: page not present");
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte) & ~PTE_U;
    if(mappages(kpagetable, i, PGSIZE, pa, flags) != 0)
      goto err;
  }
  return 0;

 err:
  uvmunmap(kpagetable, begin_page, (i - begin_page) / PGSIZE, 1);
  return -1;
}

考虑到 sbrk 等可能并不从 0 开始映射, 所以这里加了个起始地址. 还要注意, 这里修改的是内核页表, 所以要取消 PTE_U 位.

第一个程序, 找下去后在 kernel/proc.c 里, 直接在 uvminit 之后 vmukmap() 从虚拟地址 0 开始的一个页面大小即可.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Set up first user process.
void
userinit(void)
{
  ...
  // allocate one user page and copy init's instructions
  // and data into it.
  uvminit(p->pagetable, initcode, sizeof(initcode));
  p->sz = PGSIZE;
  // add init progress text page in kernel page table
  vmukmap(p->pagetable, p->kpagetable, 0, p->sz);
  ...
}

除了 init 外, 所有新的程序都是 fork + exec 出来的. 而 fork 的程序, 本身一定有 user memory. xv6 的 fork 没有 cow, 而是直接复制的, 所以在赋值完后, 往内核页表上加一个映射就行.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void)
{
  ...
  // Copy user memory from parent to child.
  if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  np->sz = p->sz;

  // copy map from user to kernel page table
  if (vmukmap(np->pagetable, np->kpagetable, 0, np->sz) < 0) {
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  ...
}

exec 直接新开了一个 proc pagetable, 并且开辟物理空间, 将程序写入内存中, 然后写新的 pagetable, 把旧的释放掉. 一开始我也照着它的方法, 新开一个 kpagetable, 但是把 kstack 给干掉了, kstack 里面存了返回地址, 结果 exec 函数结束后 ret 不回去了… 后来想了想 kstack 直接映射原来的, 测试了一下可行. 不过更简单高效的做法是, unmap 掉旧的用户空间映射, map 新的. 这样也不会修改掉 kstack.

在 return 前加上这么一段就可以了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int
exec(char *path, char **argv)
{
  ...
  // delete old map of user page in kernel page table
  // and add new map of user page
  uvmunmap(p->kpagetable, 0, PGROUNDUP(oldsz) / PGSIZE, 0);
  if (vmukmap(p->pagetable, p->kpagetable, 0, p->sz) < 0)
    goto bad;
  ...
}

sbrk 找下去是 kernel/proc.c 中的 growproc() 函数. 这个函数可以增加或者减少空间. xv6 的程序都是从虚拟地址 0 开始的, 拓展就都是往上, 缩减就都是往下. 拓展时向 kernel pagetable 中加映射就行了, 缩减需要取消映射, 但不释放空间. 因为它写好的 uvmdealloc() 已经释放过了. 可以用 uvmunmap() 这个函数, 参数需要处理一下就是.

注意, 在这个函数的实现中, 变量 szuvmalloc() 或者 uvdemalloc() 后会改变, 不能直接抄参数, 否则会出错. (我调了一个下午, 才发现是这里的问题, 太艰难了.)

提示还说了, 由于 kernel space 被映射在高地址处, 而 user space 被映射在低地址处, 且程序会向上增长, 需要注意不能访问到 kernel sfpace 的虚拟地址. 新版 xv6 内核最低处是 CLINT 而不是 hint 中写的 PLIC.

 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
// Grow or shrink user memory by n bytes.
// Return 0 on success, -1 on failure.
int
growproc(int n)
{
  uint sz;
  struct proc *p = myproc();

  sz = p->sz;
  if(n > 0){
    // can not grow to kernel space
    if (sz + n > CLINT)
      return -1;
    if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    // add map of new pages in kernel page table
    if (vmukmap(p->pagetable, p->kpagetable, p->sz, sz) < 0)
      return -1;
  } else if(n < 0){
    sz = uvmdealloc(p->pagetable, sz, sz + n);
    // delete map of unused pages in kernel page table
    uvmunmap(p->kpagetable, PGROUNDUP(sz), (PGROUNDUP(p->sz) - PGROUNDUP(sz)) / PGSIZE, 0);
  }
  p->sz = sz;
  return 0;
}

不过它写的评分程序里还是按照 PLI 来的. 于是修改它的评分程序, 在 user/usertests.c 中找到 sbrkmuch(), 将变量 BIG 改到 CLINT (0x02000000) 以下

1
2
3
4
5
6
void
sbrkmuch(char *s)
{
  enum { BIG=30*1024*1024 };  // in this xv6, kernel space start from CLINT (0x02000000)
  ...
}

现在 kernel pagetable 中还有 user space 的映射, 释放时也要考虑进去. 由于物理地址已经被释放了, 所以 unmap 的 do_free 参数是 0. 当然参数还要加一个程序大小 sz. 记得修改之前引用过的地方.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Free a process's kernel page table
// Don't free physical memory except
// the kernel stack
void
proc_freekpagetable(pagetable_t kpagetable, uint64 kstack, uint64 sz)
{
  // delete user page map in kernel page table
  if (sz > 0)
    uvmunmap(kpagetable, 0, PGROUNDUP(sz)/PGSIZE, 0);
  uvmunmap(kpagetable, UART0, 1, 0);
  uvmunmap(kpagetable, VIRTIO0, 1, 0);
  uvmunmap(kpagetable, CLINT, 0x10000 / PGSIZE, 0);
  uvmunmap(kpagetable, PLIC, 0x400000 / PGSIZE, 0);
  uvmunmap(kpagetable, KERNBASE, (PHYSTOP-KERNBASE) / PGSIZE, 0);
  uvmunmap(kpagetable, TRAMPOLINE, 1, 0);
  uvmunmap(kpagetable, kstack, 1, 1);
  uvmfree(kpagetable, 0);
}

写完可以进 xv6, 运行 usertests 测试是否有问题. 没问题以后, 把 copyincopyinstr 里面的内容注释掉, 换成它在 vmcopyin.c 中写好的 copyin_newcopyinstr_new 即可.

改 page 大小? 还得改一堆相关的东西, 如 pte 语义, 映射关系等等, 好麻烦, 咕.

在 sbrk 的时候使用 CLINT 之上的未被内核使用的虚拟地址? 还得处理好多东西, 比如释放用户空间, 就不是简单的从 0 开始释放连续 sz 大小了. 好麻烦, 咕.

这个好像可以写写, 用户程序的第一个页面不从 0 开始, 但是得改改 uvmalloc 等函数, 因为他们全部默认从 0 开始. 不过不难改. 先咕吧, 有空试试.