不落辰

知不可乎骤得,托遗响于悲风

0%

操作系统-xv6-lab3-pagetable

地址映射关系 即 va -> pa 的 映射

  • 在做本实验之前,xv6的虚拟内存机制如下

    • 对于kernel , 内核只有一张全局的kernel pagetable. 所有kernel thread 共用这一张pagetable. 通过MMU使用.
    • 对于user , 每个user process的user thread 有一张user pagetable. user通过MMU使用.
    • 但对于user传入kernel的va,kernel该如何使用user pgtbl来进行寻址呢 ? kernel 通过软件模拟(walkaddr)来查询user pgtbl 来进行user va的寻址.
      • 很显然,效率很低 不如硬件. 现代操作系统采用的也并非是这种机制.
  • 本实验 : 那么 为了让kernel可以直接通过硬件MMU来使用user pagetable. 我们实现如下措施

    • (以下就是所谓的将内核页表和用户页表合并)
      • 一个process的kernel thread 都 分配一个 pagetable.
      • 且在这个kernel thread pgtable
          1. 不但维护了kernel 的地址空间映射
        • 2. 还要 维护user thread的user pgtable的地址空间的映射(va->pa)
      • 对于1. 我们应当 为 每个kernel thread pgtbl建立和 全局 kenrel pgtbl 基本一样的地址映射关系 (kvminitproc)
      • 对于2. 我们应当 将user thread pgtbl 的维护的地址映射关系 拷贝到 kernel thread pgtbl 的 [0,0XC00..00-1]处
        • 并在 user thread pgtbl 的地址映射关系发生改变时(pte的增加/删除/修改),即时拷贝到kernel thread pgtbl上
    • 合并之后,kernel 使用 user va的方式 就是 直接通过MMU在kernel thread pgtbl的user部分进行查询. 而不用通过walkaddr,提高效率
  • 下图就是一个合并user部分后的kernel thread pgtbl

  • 本实验后,xv6的虚拟内存机制如下

    • 3个pgtbtl
      • 全局 kernel pgtbl (kernel的scheduler thread用)
      • user thread pgtbl (user thread用)
      • kernel thread pgtbl(包含user部分) (kernel 用)
    • 主流的OS也是用这种方法.
  • 关于 vm.c 注释见文

  • 关于虚拟内存是否连续见文末

part 1

  • 简单递归
  • 对于PTE,当PTE_V=1时,代表着该PTE有效。
    • 对于非叶子pte,代表整个pte指向下一级pgtbl,且可以被使用。
    • 对于叶子pte,也即其所指向的physical page 有效的physical page,所谓有效,也即该pagetable建立了从virtual address 到这个 physical page的映射,也即这个physical page 归本pgtbl所有。
  • 当PTE = 0时,意味着本PTE第一次被使用,故其也没有指向下一级页表或是物理页。
    • 核心code
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      void vmprint(pagetable_t pagetable,int depth)
      {
      if(depth > 2) return ;
      // pagetable = 512 ptes = 512 uint64 = 512 * 8 bytes = 4096 bytes
      for(int i=0;i<512;++i)
      {
      pte_t pte = pagetable[i];
      if(pte & PTE_V)
      {
      uint64 next = PTE2PA(pte); // nextlevel_pagetable_or_mem_pa
      printf("%d: pte %p pa %p\n",i,(void*)pte,next);
      vmprint((pagetable_t)next,depth+1);
      }
      }
      return ;
      }

part2 :独立内核页表

  • 独立内核页表

    • 我们需要 将共享内核页表改成独立内核页表 ,使得每个进程拥有自己独立的内核页表。
  • 在这一部分,仅仅是让每个process有自己的kernel pagetable,但实际上,在这里,对于该proc的kernel page table,使用的方法都是和之前全局的kernel pagetable一样的。

  • proc.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
      // Per-process state
    struct proc {
    struct spinlock lock;

    // p->lock must be held when using these:
    enum procstate state; // Process state
    struct proc *parent; // Parent process
    void *chan; // If non-zero, sleeping on chan
    int killed; // If non-zero, have been killed
    int xstate; // Exit status to be returned to parent's wait
    int pid; // Process ID

    // these are private to the process, so p->lock need not be held.
    uint64 kstack; // Virtual address of kernel stack // kernel stack 不是直接映射
    uint64 kstack_pa; // kernel stack的physical address
    uint64 sz; // Size of process memory (bytes)
    pagetable_t pagetable; // User page table
    pagetable_t kpagetable; // kernel page table
    struct trapframe *trapframe; // data page for trampoline.S
    struct context context; // swtch() here to run process
    struct file *ofile[NOFILE]; // Open files
    struct inode *cwd; // Current directory
    char name[16]; // Process name (debugging)
    };

RISCV PGTABLE结构

三级页表

PTE结构

  • PTE分为两部分:物理地址页帧号和标志位。具体的结构如上图。
    • 保留位:第54-63位,共10位。
    • PPN: 物理页帧号,第10-53位,共44位。
    • Flags:标志位,第0-9位,共10位。其中第8-9位为保留位,暂不使用。
      • 第0位 Valid:该页表项有效。对于叶子页表来说,这个位置为1说明虚拟地址有映射到物理地址,否则说明没有该映射。对于次页表来说,为1说明有对应的叶子页表,对于根页表说明有对应次页表。
      • 第1-3位 Readable/ Writable/ Executable: 该页表项是可读/写/执行(作为代码运行)的。通常我们只需要关心叶子页表项的这三个位,因为这代表对应的物理页帧的标志,而不是页表的标志。对于根页表和次页表的目录项,这三个位往往置为0。
      • 第4位 User: 该页表项指向的物理页能在用户态访问。内核页表中的代码和数据我们不会将其给用户使用,所以会置0,但是用户页表的代码和数据应该要置1。同样,对于内核页表中的页表项,如果该位置1,则计算机的硬件不会允许内核访问对应地址,但可以通过其他的手段访问,后文会介绍。
      • 其他位,查阅riscv-privileged.pdf 4.3~4.4

关键问题

  • 问题:proc的kernel pagetable都需要映射什么?

    • 在本部分,与全局kernel pagetable的不同之处在于
      • 只需要映射自己proc的kernel stack
      • 不需要映射CLINT
    • 实际上,在下一部分完成后,proc kernel pagetable的PLIC之下,还需映射到proc user pgtbl维护的地址空间(即user虚拟地址指向的物理地址)
    • 详情见下一部分。
    • 本部分只是负责建立一个不完全proc的kernel pgtbl,并替代全局kernel pgtbl,并不改变其他机制。
  • 一个进程到底是什么?

    • 感觉就是一堆上下文和代码。。
  • 为什么要保留初始内核页表?

    • 保留原有的全局 kernel pagetable
    • 因为cpu上不是一直都在执行用户进程。当执行用户进程之前,需要切换成user的kpgtbl;执行结束后,需要切成全局的kpgtbl。因为user的kpgtbl此时可能已经在freeproc函数中被释放,如果继续使用user的kpgtbl,会crash。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      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;
      // 切换进程的同时 切换成proc的kernel pgtbl
      kvminithartproc(p->kpagetable);
      // 运行p进程
      swtch(&c->context, &p->context);
      // 没有user进程运行时 即 运行scheduler时 换回全局kernel pgtbl
      kvminithart();
      // Process is done running for now.
      // It should have changed its p->state before coming back.
      // cpu dosen't run any user process now
      c->proc = 0;
      }
      release(&p->lock);
      }

流程

  • Step 1 :proc中新添 pagetable_t kernel_pagetable 以及kstack_pa

    • 属于proc的kernel pagetable
    • kernel stack的物理地址
  • Step 2 :实现 pagetable_t kvminitproc()。

    • 模仿kvminit,创建进程的内核页表,并建立除了CLINT之外的映射。
  • Step 3 :修改procinit。

    • procinit():系统引导时(见kernel/main.c的main函数),用于给proc分配kernel stack的physical page 并在全局kernel pgtbl建立映射。
    • 记录下kernel stack的physical address。为了之后allocproc分配进程创建proc kernel pagetable时,在proc kpgtbl中建立kernel stack的映射。
  • Step 4 :修改allocproc。

    • allocproc何时被调用?
      • 在系统启动时被第一个process 和 fork 调用
    • allocproc功能
      • 在进程表proc数组中查找UNSUEDPCB,
        • 如果找到,
          • 创建用户页表初始化在内核中运行所需的状态trapframe,user pagetable , kpgtable , 处于forkret的上下文等,并保持p->lock返回。
        • 如果没有PCB,或者内存分配失败
          • return 0
    • 创建proc的kpgtbl,并在其中建立该proc的kstack的映射。
      • 只是把本proc的kstack在全局的kernel pgtbl的映射又在proc的kernel pgtbl上又做了一次。virtual address 和 kernel address 都是一样的
      • 之前记录kstack_pa就是为了在这里建立映射。
      • 虚拟地址也是之前创建kstack时记录下的p->kstack
        1
        2
        3
        4
        5
        p->kpagetable = kvminitproc();
        // 映射kstack
        kvmmapproc(p->kpagetable,p->kstack,p->kstack_pa,PGSIZE,PTE_R | PTE_W);
        // 此时user pgtbl 除了trapframe和trampoline之外还没有section 无需同步 user kernel pgtbl
        // 也即0xC000000之下 user pgtbl没有建立va-pa
  • Step 5 :scheduler 切换进程的同时切换pagetable。没有用户进程的时候切回全局kernel pgtbl

    1
    2
    3
    4
    5
    kvminithartproc(p->kpagetable);
    // 运行p进程
    swtch(&c->context, &p->context);
    // 没有user进程运行时 即 运行scheduler时 换回全局kernel pgtbl
    kvminithart();
  • Step 6 :进程结束时,释放proc的kpgtbl,但不释放其映射的物理内存。因为内核的代码和数据都是唯一的。只是有很多人指向而已.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //  释放以pgtbl为根节点的所有pgtbl,并将pgtbl的所有pte清0,但不释放映射到的物理内存
    void freepgtblonly(pagetable_t pgtbl)
    {
    for(int i=0;i<512;++i)
    {
    pte_t pte = pgtbl[i];
    // 前两级的pte
    if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0)
    {
    uint64 child = PTE2PA(pte);
    freepgtblonly((pagetable_t)child); // 释放下一级pte
    }
    // 对于叶子pte 不必操作
    }
    // 释放整个pgtbl自身
    kfree((void*)pgtbl);
    }
  • 一遍过 yes!

part 3 :简化软件模拟地址翻译

思路

进程user态的虚拟地址 即 proc的user pgtbl维护的虚拟地址
进程内核(kernel) 态的虚拟地址 即 proc的kernel pgtbl维护的虚拟地址
全局的kernel pgtbl 即 全局的kernel pgtbl

  • 在做part3之前,proc在内核态如何使用user态传递到kenerl的user virtual address – 通过walkaddr
    • kernel从user态的va读取数据:copyin ; 写入数据到user的va:copyout
    • 比如在copyin的时候,如果拿到了user态的虚拟地址,如果按照之前的流程的话,需要通过copyin函数,copyin函数通过walkaddr软件模拟翻译获得user va对应的pa,再读取pa的内容,将其复制进kernel_dst对应的pa的physical memory。
    • 软件模拟翻译 :
      • 通过walkaddr去user pgtbl查找user va对应的pa的过程。效率低。不如MMU查找快。
    • kernel_dst如何得到pa?
      • 经硬件MMU查询翻译得到。不是在软件层面实现的。
      • 当cpu发出这条kernel_dst后,会经MMU查询全局的kernel pagetable 翻译成pa。
    • 大概意思如下

  • 即便在做完part2之后,上述情况也并无差别,只是将MMU查询使用的全局的kernel pagetable替换成了proc的kernel pagetable。proc的kpgtbl和全局的kpgtbl的区别只是CLINT,不影响上述行为。
  • 做完part3,如何使用user的传入kernel的va? – 直接使用
    • 手段:将proc的user pgtbl所维护的从va到pa的映射,拷贝到kernel pgtbl的中。
    • 这样我们在kernel的时候,比如在copyin的时候,拿到user传来的user va,我们不必去walkaddr去查询user va对应的pa,而是直接使用就好。将va到pa的翻译交给MMU。这样,user的va 直接使用,kernel的va 本就直接使用,省去了walkaddr的过程,全权交给硬件实现物理地址的翻译,提高效率。
      • 因为此时cpu的$satp 是 proc的kernel pagetable。而我们又将整个user pgtbl维持的va到pa的映射关系,原封不动的搬到了proc 的 kernel pgtbl。
      • 因此MMU可以使用user的va在kernel的pgtbl中找到对应的pte,得到对应的pa。
    • copyin_new核心code如下
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // Copy from user to kernel.
      // Copy len bytes to dst from virtual address srcva in a given page table.
      // Return 0 on success, -1 on error.
      int
      copyin_new(pagetable_t user_pagetable, char *kernel_dst, uint64 user_srcva, uint64 len)
      {
      struct proc *p = myproc();
      // 直接使用user的srcva 而不必先去walkaddr得到pa!
      memmove((void *) kernel_dst, (void *)user_srcva, len);
      stats.ncopyin++; // XXX lock
      return 0;
      }

实现

  • part3实现思路

    • 让proc的kpgtbl也维护一套user态的地址空间(拷贝并同步user pgtbl)
    • 拷贝:开始,将proc的user pgtbl所维护的从va到pa的映射,拷贝到kernel pgtbl的中
    • 同步:user pgtbl发生改变的时候,将 变化的pte拷贝到user pgtbl中。所谓改变,即新增pte 、删除pte、改变pte的时候。
      • 具体什么函数会令user pgtbl改变?
        • uvmalloc –> mappages —> walk(1)
        • uvmdealloc –> uvmunmap –> kfree(pa) *pte = 0
      • 但是 如果是对那块已经建立的physical memory填充字节或者读取写入之类的云云(比如copyin copyout),就不必同步kernel pgtbl,因为映射关系没变,pte自然也没有变化
  • step

    • step1 : 将user pgtbl的pte复制到kernel中并同步变化.
      • 用户页表是从虚拟地址0开始,用多少就建多少,但最高地址不能超过内核的起始地址,这样用户程序可用的虚拟地址空间就为0x0 - 0xC000000。
      • 也即,在user pgtbl变化的时候,将变化同步到kpgtbl中
      • 故在growproc fork userinit exec中 调用u2kvmcopymappingonly。
        • 例如fork中
          1
          2
          3
          4
          5
          if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0 || u2kvmcopymappingonly(np->kpagetable,np->pagetable,0,p->sz) < 0){
          freeproc(np);
          release(&np->lock);
          return -1;
          }
      • 还有在user pgtbl清空pte时 也要同步到kpgtbl中,也即uvmdalloc和kvmdeallocpgtblonly要配套使用。代码见growproc以及exec。
    • step2 : copyin 替换为 copin_new

3个pgtbl

  • 全局的kernel pgtbl

    • 仅包含内核的代码和数据的虚实地址映射,用户程序的代码和数据不包含在内。
    • xv6内核的大部分虚实地址映射是恒等映射,虚拟地址和物理地址是一模一样的。对于既要读写虚拟页又要通过PTE管理物理页的xv6内核来说,这样的直接映射降低了复杂性。
    • 从图中,我们可以看到,有两处虚拟地址不是直接映射的:
      • trampoline 页:它是用户态-内核态跳板,既被映射到内核虚拟空间的顶端,页被映射到用户空间的同样位置。
      • kernel stack:每个进程都有自己的内核栈,它被映射到高位,这样在它下面可以保留一个未映射的守护页。
  • proc 的 user pagetable

    • 上图是xv6的用户程序虚拟地址空间分布。其代码实现在kernel/exec.c中,exec使用proc_pagetable分配了TRAMPOLINE和TRAPFRAME的页表映射,然后用uvmalloc来为每个ELF段分配内存及页表映射,并用loadseg把每个ELF段载入内存。
    • trampoline :用户态-内核态跳板。
    • trapframe:用来存放每个进程的用户寄存器的内存空间。如果你想查看xv6在trapframe page中存放了什么,详见proc.h的trapframe结构体。
    • heap :堆。程序初始化时堆没有分配任何空间。用户程序可以通过sbrk系统调用调整堆分配的空间,这会把新内存映射到页表中,也可以从页表中移除映射,释放内存。
    • stack:用户栈。xv6的用户栈只分配了一个页(PAGESIZE),放置在比堆更低的位置。通常操作系统会把用户栈放置在比堆更高的位置,这也是xv6和常见的操作系统做法不一样的地方。
    • guard page:守护页,用来保护Stack。如果stack耗尽了,它会溢出到Guard page,但是因为Guard page的PTE中Valid标志位未设置,会导致立即触发page fault,这样的结果好过内存越界之后造成的数据混乱。
    • data:用户程序的数据段。
    • text:用户程序的代码段。
  • proc的kernel pgtbl

    • 与全局kernel最重要的不同在于 只维护了自己kernel stack的地址映射,以及多维护了用户态的地址空间。

vm.c

工具函数

  • pte_t * walk(pagetable_t pagetable, uint64 va, int alloc)

    • 遍历前两级页表,找到第三级页表,返回va对应的第三级页表的叶子PTE , 这个PTE有可能全0
    • alloc == 1
      • 创建遍历过程中需要遍历到的但不存在的pagetable,最后返回va对应的第三级页表的叶子PTE.如果这个pte原本是不存在的话,那么实际上返回的pte是0
      • success : return pte的地址
      • fail : return 0 创建失败
    • alloc == 0
      • 不必创建遍历过程中需要遍历到的但不存在的pagetable,如果遍历到的pte还不存在或者需要遍历的页表不存在,那么直接返回0即可。
      • success : return pte的地址
      • fail : return 0 所需遍历的页表不存在
    • 只有当遍历到的三级页表在遍历前就全部存在,且第三级页表中的va对应的pte也之前就建立了对物理页的映射,才会得到返回有效的指向物理页的pte。
    • 目前来看只有通过walk接口,才能创建三级页表。
  • uint64 walkaddr(pagetable_t pagetable, uint64 va)

    • 根据va 去 pagetable里面查找相应的PTE , 进而得到pa。并不新增pte。也不会改变已有pte。对于pgtbl建立的映射关系没有影响。
    • success : 如果查到的PTE无效 / 无权限,返回0
    • fail : 如果查到的PTE有效,返回pa
      • 返回的pa也只是physical page的起始addr,因为舍弃了va的低12位。
  • int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)

    • 在pagetable上(所代表的三级页表),建立从va到pa的映射。一个page一个page的建立映射,perm是指定的权限位。mappages结束后,维护va->pa的所需的页表以及pte都已经创建和初始化好。
      • 建立的映射关系:[va,va+size-1] -> [pa,pa+size-1]。 使用的虚拟地址是连续的,传入的物理内存也是连续的
      • 建立映射:*pte = PA2PTE(pa) | perm | PTE_V
    • success : return 0;
    • fail : return -1;
      • 由于walk函数在创建pagetable自身的时候失败。
    • mappages 不会kalloc要映射到的物理页,要映射到的物理页时调用mappages之前就准备好的。是传入的pa;但是mappages可能会kalloc所需的pagetables。
  • int test_pagetable()

    • 检验当前使用的pagetable是全局的kernel pagetable 还是其他pagetable
      • 通过$satp比较
  • void freepgtblonly(pagetable_t pgtbl)

    • 释放以pgtbl为根节点的所有pgtbl,并将pgtbl的所有pte清0,但不释放映射到的物理内存
  • void freewalk(pagetable_t pagetable)

    • 释放所有存在的pagetable,并且检测叶子page table上面的叶子 PTE 是否已经没有对dram的映射,如果害有对dram的映射,那么panic
      • 在此之前应当先销毁整个pgtbl所建立的映射关系。(如调用 uvmunmap使得pte清0,释放映射的物理内存)

关于全局 kernel pgtbl

  • 关于全局 kernel pagetable的

  • void kvminit()

    • 建立好direct-map kernel_pagetable 但并没有启动pgtbl
    • 建立了KERNBASE之下 IO设备的映射以及kernel data . kernel text 以及trampoline的映射
  • void kvminithart()

    • 设置cpu当前使用页表为kernel_pagetable
  • kvmmap(uint64 va, uint64 pa, uint64 sz, int perm)

    • 在kernel_pagetable上,建立从va到pa映射,大小为sz。建立失败则panic
  • uint64 kvmpa(uint64 va)

    • 在kernel pagetable中 查找va对应的pa
    • success : return pa (PPN + offset)
    • fail : panic
      • 一般在va对应的物理页并不存在或者没有建立映射时(即根据va查询到的pte所指向的物理页无效 PTE_V = 0)
    • 仅仅对kernel stack的va的对应的pa进行查询时有效。因为kernel pagetable只有kernel stack不是直接映射。(其实还有trampoline,不过那是pc指向的地址了)

kernel 和 user 交互

kernel 和 user之间的数据交互 通过kernel 和user的pgtbl 以下是做lab之前

  • int copyout(pagetable_t user_pagetable, uint64 user_dstva, char *kernel_src, uint64 len)

    • 从kernel pagetable维护的src 的内容拷贝 len bytes 到 user pagetable维护的dst
    • success : return 0
    • fail : return -1
    • 核心代码如下,去掉了一些页对齐的代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      // Copy from kernel to user.
      // Copy len bytes from src to virtual address dstva in a given page table.
      // Return 0 on success, -1 on error.
      // 从kernel pagetable维护的src 的内容拷贝 len bytes 到 user pagetable维护的dst
      // success : return 0
      // fail : return -1
      copyout(pagetable_t user_pagetable, uint64 user_dstva, char *kernel_src, uint64 len)
      {
      // src是全局 kpgtbl维护的虚拟地址,其和物理地址是恒等映射的关系。因此使用src实际上就是在使用物理地址
      uint64 n, va0, pa0;

      while(len > 0){
      va0 = PGROUNDDOWN(dstva);
      // 查找user pgtbl 将va映射到的pa
      pa0 = walkaddr(pagetable, va0);
      // 将pa src的内容复制到pa处
      memmove(pa0, src, n);
      // continue
      }
      return 0;
      }
  • int copyin(pagetable_t user_pagetable, char *kernel_dst, uint64 user_srcva, uint64 len)

    • 从user pagetable维护的srcva 的内容拷贝 len bytes 到 kernel pagetable维护的dst
    • success : return 0
    • fail : return -1 . 查询srcva对应pa失败
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      // Copy from user to kernel.
      // Copy len bytes to dst from virtual address srcva in a given page table.
      // Return 0 on success, -1 on error.
      int
      copyin(pagetable_t user_pagetable, char *kernel_dst, uint64 user_srcva, uint64 len)
      {
      while(len > 0){
      // 页对齐
      va0 = PGROUNDDOWN(srcva);
      // user的va翻译成了物理地址pa
      pa0 = walkaddr(pagetable, va0);
      // 这里可以成功运行 也不过是因为kernel_pagetable 在这个部分对 dram是恒等映射
      // 因为cpu还是会将pa当作kernel pagetable的va来进行翻译。翻译成pa。
      // 将user pgtbl维护的映射到pa0的内容 拷贝到kernel pgtbl维护的dst指向的地址的物理内存
      memmove(dst, pa0 , n);
      // continue
      }
      }
  • int copyinstr(pagetable_t user_pagetable, char *kernel_dst, uint64 user_srcva, uint64 max)

    • 将user pagetable维护的srcva的地址的内容,拷贝kernel pagetable维护的dst。直到遇到’\0’

完成part3之后

  • int copyin(pagetable_t user_pagetable, char *kernel_dst, uint64 user_srcva, uint64 len)
    • 从proc user pagetable维护的srcva 的内容拷贝 len bytes 到 proc kernel pagetable维护的dst
    • 此时proc的kpgtbl 也维护了srcva的虚拟地址,故不必去walkaddr,而是直接使用srcva
    • success : return 0
    • fail : return -1 .
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // Copy from user to kernel.
      // Copy len bytes to dst from virtual address srcva in a given page table.
      // Return 0 on success, -1 on error.
      int
      copyin_new(pagetable_t user_pagetable, char *kernel_dst, uint64 user_srcva, uint64 len)
      {
      struct proc *p = myproc();
      // 直接使用user的srcva 而不必先去walkaddr得到pa!
      memmove((void *) kernel_dst, (void *)user_srcva, len);
      stats.ncopyin++; // XXX lock
      return 0;
      }

(part 3)proc的kernel pagetable

part 3 : proc的kernel pagetable

  • pagetable_t kvminitproc()
    • 建立proc的kpgtable。仿照kvminit。不同之处在于没有映射CLINT。防止合并user页表时发生重合。
  • void kvmmapproc(pagetable_t proc_kernel_pagetable, uint64 va, uint64 pa, uint64 sz, int perm)
    • 在proc_kernel_pagetable上,建立从va到pa映射,大小为sz。仅仅建立映射,而不负责kalloc映射到的physical page。失败则panic
  • void kvminithartproc(pagetable_t kpgtbl)
    • 设置当前cpu使用页表为proc的kpgtbl
  • int u2kvmcopymappingonly(pagetable_t kpgtbl,pagetable_t pgtbl,uint64 start,uint64 end)
    • 将user pgtbl 的pte拷贝给user kernel pagetable [start,end)范围最大为[0,PLIC)。仅仅拷贝映射关系而已。而不拷贝映射的物理内存。
      • 当user的pgtbl 有任何改动(增添或删除)的时候 将改动的pte拷贝给kernel pgtbl
  • uint64 kvmdeallocpgtblonly(pagetable_t kpgtbl, uint64 oldsz, uint64 newsz)
    • 解除proc 的 kernel pgtbl 的va从 [newsz,oldsz) 到pa的映射(即将相应pte清0),但不释放physical memory
      • [newsz , oldsz) 应当包含于 [0,PLIC)

关于proc的user pagetable

关于proc user pagetable

  • pagetable_t uvmcreate()

    • 创建一个PGSIZE大小的empty的user的pagetable(通过kalloc)
    • success :return pagetable
    • fail :return 0
  • void uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)

    • 解除va到pa的映射(pa由pagetable查找)(即使得va对应的pte清0)
    • 并释放pa对应的物理页(if dofree = 1)。
    • 总共解除对npages个物理页的映射
    • fail : panic。查询pte失败 | te指向的物理页无效 | 查询到的pte不是个叶子 ?
  • void uvminit(pagetable_t pagetable, uchar *src, uint sz)

    • 在(user的)pagetable上,从虚拟地址的0开始,建立从 virtual [0,PGSIZE-1) 到 physical [mem,mem+PGSIZE-1) 的映射
    • 只是为了第一个进程,第一个进程需要运行initcode的text。src这段内存中装的是initcode的代码
  • uint64 uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)

    • 为user pagetable 建立从virtual addr oldsz到virtual addr newsz的虚拟地址空间。为其申请物理内存并建立映射。
    • 传入的oldsz 和 newsz 都是pgtbl的虚拟地址
    • success : return newsz
      • 返回pagetable维护的最大的虚拟地址
    • fail : return 0; 当
      • kalloc申请物理内存失败 / 建立mappages映射失败
  • uint64 uvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)

    • pagetable维护的虚拟地址空间,从oldsz减到newsz。解除va到pa的映射 并释放physical page
      • 传入的oldsz和newsz都是pagetable上的虚拟地址
    • return pgtbl当前维护的最大vaddr
  • void uvmfree(pagetable_t pagetable, uint64 sz)

    • 释放user page table 所建立的 虚拟地址 到 物理地址的映射,并释放其所映射的物理内存
      • uvmunmap
    • 释放user page table 自身
      • freewalk(pagetable);
  • int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)

    • 所谓uvmcopy 就是将old pgtbl所建立的映射关系以及物理内存 拷贝给 new pgtbl
      • 本函数令 new pgtbl 也像old pgtbl 一样,建立从[0,sz-1]到physical memory的映射.
      • 传入的old pgtbl 所维护的映射是从[0,sz-1]到physical memory的映射
      • 将old(parent) page table 所维护的va到pa的映射中physical memory的内容拷贝给new pagetable 所建立的va所对应的physical memory.
    • success : return 0
    • fail : return -1
    • 调用场合:Given a parent process’s page table, copy its memory into a child’s page table.
  • void uvmclear(pagetable_t pagetable, uint64 va)

    • 将va在user pagetable对应的PTE标记为user无权限

琐碎问题

  1. 刚切换回scheduler的时候就需要换回全局 kernel pgtbl吗?. 是的
  2. user pagetable 一定都在0xC00000之下吗? 怎么确定的 ? 我们实现时人为控制的.
  3. scheduler:怎么switch之后 直接就运行了另一个进程呢?:通过swtch.S . 见后面的thread实验
  4. uvmcopy 是不是只拷贝了text到heap的这段连续的。trapframe和trampoline不管。:对的
  • 胡咧咧几句
    • 可以看到,相较于Linux,即便我们完成了本实验,xv6实现的虚拟内存还是较为简易的。
      • Linux里面process的virtual address不见得都是连续的. 一段段连续的va的起始地址记录于task_struct中的vm_area_struct中.

      • 但是在xv6中,user process的virtual address是连续的. 除了顶部的trampoline和trapframe,就是下部的heap stack等. 换句话说,va只分成了两段(见下面的exec.c可以证明)
        • 顶部的 trampoline , trapframe
        • 下部的 heap , stack , guard page , data , text

        • 当然heap段一开始是空的,并没有va对应的pte记录在pgtable中. 需要uvmalloc
        • stack , guard page , data , text 一开始就有pte在pgtable中. 见exec()
      • 那么,对于user传入kernel的va
        • linux 显然会先从vm_area_struct中,寻找,看该va是否属于某一区域. 不属于,则是非法va. 若属于,再看该va对应的pte的权限是否合法.
        • xv6 则简化很多,由于va连续,则kernel只需要检查va是否 < sz && > 0即可. 不属于,则非法va. 属于,则再看该va对应的pte的权限是否合法.
          • 见copy on write实验
  • 由exec.c可以看出 xv6的user processtext datat stack heap 的 va是连续的
    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
    int
    exec(char *path, char **argv)
    {
    struct proc *p = myproc();
    // create a user pagetable
    // 0. 创建 pagetable
    // 1. 建立 trampoline 和 trapframe 的映射
    pagetable = proc_pagetable(p);

    // 2. Load program into memory.
    // 建立了 data 段 和 text 段 的 va到pa(申请了phymem)
    for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
    sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz);
    loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz);
    }

    // Allocate two pages at the next page boundary.
    // Use the second as the user stack.
    // 3. 建立了 stack 段 和 guard page段 的 va到pa的映射 (申请了phymem)
    sz = uvmalloc(pagetable, sz, sz + 2*PGSIZE);
    uvmclear(pagetable, sz-2*PGSIZE);
    sp = sz;
    stackbase = sp - PGSIZE;
    // int main(int argc,char *argv[])
    // 将 argv压入 stack
    // Push argument strings, prepare rest of stack in ustack.
    for(argc) {
    sp -= strlen(argv[argc]) + 1;
    sp -= sp % 16; // riscv sp must be 16-byte aligned
    }
    // 记住main一开始的栈指针
    p->trapframe->a1 = sp;

    // 4. 解除kpgtbl的user部分的旧映射
    kvmdeallocpgtblonly(p->kpagetable,p->sz,0);

    // 5. 将新的user pgtbl 同步到 kernel pgtbl上
    if(u2kvmcopymappingonly(p->kpagetable,pagetable,0,sz)<0)
    goto bad;

    // Commit to the user image.
    p->pagetable = pagetable;
    p->sz = sz;
    p->trapframe->sp = sp; // initial stack pointer
    // 6. 释放旧的user pgtbl
    proc_freepagetable(oldpagetable, oldsz);

    return argc; // this ends up in a0, the first argument to main(argc, argv)
    }