其他分享
首页 > 其他分享> > MIT6.S081 LAB3 pagetable & virtual memory

MIT6.S081 LAB3 pagetable & virtual memory

作者:互联网

预备知识(理解相关代码)

1. 地址空间

为什么需要地址空间(address space)?(1)进程之间的内存隔离性;(2)实现了抽象性,为了对内存进行更好的管理。

2. 页表

2.1 页表(page table)在一个物理内存上创建不同的地址空间。页表在硬件中通过内存管理单元(MMU)实现,MMU将虚拟地址翻译称物理地址,物理地址用以索引物理内存。pagetable中的每一个条目(entry)都是一个page的索引,所以一次的物理地址翻译对应的是一个page的虚拟地址到物理地址的翻译,page中的字节由offset索引,offset直接继承自虚拟地址的最后12位(索引一个page中的4096个字节)。

2.2 一条虚拟地址由64bit(25+27+12)表示(RISC-V的寄存器是64bit的),其中前25位保留,通过启用这25位,可以扩展可用的虚拟内存的大小;中间27位是一个page的虚拟地址,称为index,MMU通过将这27位page虚拟地址转化为page物理地址来访问page;最后12位是offset,表示要访问的字节在page中的偏移量。

2.3 一条物理地址由56bit(44+12)表示(56bit是由硬件设计人员决定的,如果要更改,还需要更大的物理内存和主板上更宽的地址总线),其中44bit是物理page号(PPN),剩下的12位是offset。

2.4 MMU通过分级索引的方式将虚拟地址中的index转化为PPN。虚拟地址中的index的27bit实际上分为3层索引(L2,L1,L0),每层9个bit,也称为3个pagedirectory,每一个pagedirectory都是一个page大小,因此也可以视为一个page。L2的9个bit索引从最高级(L2级)pagetable中索引得到L1级的pagedirectory所在的物理地址,L1的9个bit从L1级pagedirectory中索引得到L0级pagetable的物理地址,L0的9个bit从L0级pagetable中得到page的物理地址。若中间索引的结果为空,MMU将会提出pagefault,而不会创建新的page(ps.无法直接指定虚拟地址来使用物理内存)。这样总共能索引到的物理地址有(2**9个L1级pagetable)* (2**9个L0级pagetable)*(2**9个物理页)。利用分级页表,可以索引到同样多的2**27个PTE,但是在单级索引的时候,需要在pagedirectory中准备2**27个PTE提供虚拟地址和物理地址的一一对应以供索引,这将需要一个庞大的pagetable,会占用很多内存;在三级索引的时候,需要3x(2**9)个PTE就可以

// extract the three 9-bit page table indices from a virtual address.
#define PGSHIFT 12  // bits of offset within a page
#define PXMASK          0x1FF // 9 bits
#define PXSHIFT(level)  (PGSHIFT+(9*(level)))
#define PX(level, va) ((((uint64) (va)) >> PXSHIFT(level)) & PXMASK)\

// shift a physical address to the right place for a PTE.
#define PA2PTE(pa) ((((uint64)pa) >> 12) << 10)

#define PTE2PA(pte) (((pte) >> 10) << 12) //>>L0: split flags in pte; <<L2: form a page head addr.

#define PTE_FLAGS(pte) ((pte) & 0x3FF)

// MMU: from va get the final [ppn-flag] in L0 pagetable, in which ppn is the final ppn of data.
pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)
{
  if(va >= MAXVA)
    panic("walk");

  for(int level = 2; level > 0; level--) {// level2 and level1 pagetable
    pte_t *pte = &pagetable[PX(level, va)]; // PX(level, va): get 9 bits of level "level" from va as index of pagetable.
    if(*pte & PTE_V) {
      pagetable = (pagetable_t)PTE2PA(*pte); // update pagetable to level-"level"-pagetable.
    } else {  //pte is not valid, if alloc==1, allocate a new page and init that with zero to be *pte.
      if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
        return 0;
      memset(pagetable, 0, PGSIZE);
      *pte = PA2PTE(pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)];//level0 pagetable
}
// walkaddr(pt, va): 
// pte = walk(pt,va);  
// return PTE2PA(*pte).

2.5 pagedirectory中的一个条目称为PTE(page table entry),是64bit(寄存器大小)。PTE由三部分组成(10+44+10),前10bit是保留字段;中间44bit是PPN,也就是物理page号,指向了一个page(或pagetable)的物理地址;最后10bit是flag,标志着指向的物理page的属性(读、写、有效...)。

3. 页表缓存

TLB快表(translation lookaside buffer)提供了【虚拟地址,物理地址】对,这样近期访问的条目的缓存。然而,当切换pagetable的时候,虚拟地址和物理地址的对应将失效,之前的缓存不再有用,因此会清空TLB。在RISC-V中,清空TLB的指令是sfence_vma。

// flush the TLB.
static inline void
sfence_vma()
{
  // the zero, zero means flush all TLB entries.
  asm volatile("sfence.vma zero, zero");
}

// Switch h/w page table register to the kernel's page table,
// and enable paging.
void
kvminithart()
{
  w_satp(MAKE_SATP(kernel_pagetable));
  sfence_vma();
}

4. kernal kage table

下图是虚拟地址和物理地址的对应关系,在物理地址中,只有RAM部分属于内存,在RAM以下的部分属于各种外部设备到物理地址的映射,RAM的以上有一块未使用的区域,这是由于物理地址由56bit表示,但可能主板上没有接那么多DRAM芯片,于是没有那么大的实际内存空间。

对于不同的进程,有不同的kernel stack。

虚拟地址中的free memory对应了物理内存中的一段地址,XV6使用这段free memory来存放用户进程的pagetable, text和data,用户进程的虚拟地址空间本质上和内核的虚拟地址空间大小是一样的,内核为进程放弃了一些自己的内存。下面两图分别是【内核pagetable】和【进程pagetable】。

5. 乱七八糟

memlayout.h定义了一系列的地址量,与【pagetable】两图一一对应。

SATP寄存器包含了需要使用的地址转换表的内存地址。所以ls有自己的地址转换表,cat也有自己的地址转换表。每个进程都有完全属于自己的地址转换表。每个cpu有自己的satp寄存器,指向自己的根页表页,由于最终的物理内存不同,因而可以运行不同的进程。

walk的函数,它在软件中实现了MMU硬件相同的功能。walk函数设置了最初的page table。

在XV6中,内核有它自己的page table,用户进程也有自己的page table,用户进程指向sys_info结构体的指针存在于用户空间的page table,但是内核需要将这个指针翻译成一个自己可以读写的物理地址。如果你查看copy_in,copy_out,你可以发现内核会通过用户进程的page table,将用户的虚拟地址翻译得到物理地址,这样内核可以读写相应的物理内存地址。这就是为什么在XV6中需要有walk函数的一些原因。

它有由内核设置好的,专属于进程的page table来完成地址翻译。

每个CPU核只有一个SATP寄存器,但是在每个proc结构体,如果你查看proc.h,里面有一个指向page table的指针,这对应了进程的根page table物理内存地址。

trampoline和trapframe。

procinit是一开始的时候,系统初始化时调用的,对所有的proc初始化其lock和kernel stack(此时尚且没有用户进程,所以这些proc都是内核进程)。

// start() jumps here in supervisor mode on all CPUs.
void
main()
{
  if(cpuid() == 0){
    consoleinit();
#if defined(LAB_PGTBL) || defined(LAB_LOCK)
    statsinit();
#endif
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    kinit();         // physical page allocator: init kmem lock, and free pa from end to PHYSTOP(set to 0).
    kvminit();       // alloc and create kernel page table: and map virtural address in kernel pagetable to address in pysical memory.
    kvminithart();   // turn on paging: set satp to kernel pagetable inited in kvminit, and fush TLB.
    procinit();      // process table: init locks for procs, alloc & map kernel stack for each proc
    trapinit();      // trap vectors: 
    trapinithart();  // install kernel trap vector
    plicinit();      // set up interrupt controller
    plicinithart();  // ask PLIC for device interrupts
    binit();         // buffer cache
    iinit();         // inode cache
    fileinit();      // file table
    virtio_disk_init(); // emulated hard disk
#ifdef LAB_NET
    pci_init();
    sockinit();
#endif    
    userinit();      // first user process
    __sync_synchronize();
    started = 1;
  } else {
    while(started == 0)
      ;
    __sync_synchronize();
    printf("hart %d starting\n", cpuid());
    kvminithart();    // turn on paging
    trapinithart();   // install kernel trap vector
    plicinithart();   // ask PLIC for device interrupts
  }

  scheduler();        
}

scheduler中没有satp寄存器的切换,是因为进程的切换总是在内核态进行,而所有进程共用一个kernel pagetable(不同进程有不同的kernel stack)。

LAB 3 PGTBL

实现打印pagetable的功能。用递归的方式遍历三层pagetable,若pte有效,则打印出这一条,并遍历其子树。

//vm.c
void
_vmprint(pagetable_t pagetable,int level)
{
    // there are 2^9 = 512 PTEs in a page table.
    if(level>=3) return;
    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);
            printf("..");
            for(int j = 0;j<level;j++) printf(" ..");
            printf("%d: pte %p pa %p\n", i,pte, PTE2PA(pte));
            _vmprint((pagetable_t)child, level+1);
        }
    }
}

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

//defs.h
void            vmprint(pagetable_t);

//exec.c
if(p->pid==1) vmprint(p->pagetable);
return argc; // this ends up in a0, the first argument to main(argc, argv)
A kernel page table per process(hard)

当前的xv6中,所有的process共用一个kernel pagetable,即,【内核pagetable】图中的结构是所有process共享的,每个process只有自己的kstack是独立的,这些kstack在同一个kernel pagetable中排列(由保护页分隔开)。

本实验要求实现每个process都有一个自己的kernel pagetable,且每个进程内核页表与现有的全局内核页表相同。

  1. 首先,在struct proc中添加一个字段,用以保存进程内核页表地址。
pagetable_t kpagetable;      // kernel page table
  1. 初始化一个进程的时候,不仅要初始化其user pagetable,也要初始化其kernel pagetable。现有的处理是在初始化进程的时候在全局kernel_pagetable中声明一页kstack作为进程独有的kstack,我们需要将其修改为声明一个进程独有的kernel pagetable,而不是仅仅声明一页kstack。
void
procinit(void)
{
  struct proc *p;
  
  initlock(&pid_lock, "nextpid");
  for(p = proc; p < &proc[NPROC]; p++) {
      initlock(&p->lock, "proc");

//      // Allocate a page for the process's kernel stack.
//      // Map it high in memory, followed by an invalid
//      // guard page.
//      char *pa = kalloc();
//      if(pa == 0)
//          panic("kalloc");
//      uint64 va = KSTACK((int) (p - proc));
//      kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
//      p->kstack = va;
  }
//  kvminithart(); //set satp tobe kernel_pagetable, here we don't use kernel_pagetable, but use p->kpagetable, so don't need.
}

// pagetable is not fixed, comparing to kvmmap.
void
uvmmap(pagetable_t pagetable,uint64 va, uint64 pa, uint64 sz, int perm)
{
    if(mappages(pagetable, va, sz, pa, perm) != 0)
        panic("kvmmap");
}

// simialr to kvminit, create a direct-map page table for the kernel and map that to p->kpagetable using uvmmap().
pagetable_t
proc_kpt_init()
{
    pagetable_t kpt;
    kpt = uvmcreate();
    if (kpt == 0) return 0;
    uvmmap(kpt, UART0, UART0, PGSIZE, PTE_R | PTE_W);
    uvmmap(kpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
    uvmmap(kpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
    uvmmap(kpt, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
    uvmmap(kpt, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
    uvmmap(kpt, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
    uvmmap(kpt, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
    return kpt;
}

static struct proc*
allocproc(void)
{
  ...

    p->kpagetable = proc_kpt_init(); 
    // Allocate a page for the process's kernel stack.
    // Map it high in memory, 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;

  ...
}

  1. 在scheduler函数中,设置相应的p->kpagetable作为satp寄存器的指向。注意,此处satp设置的是内核页表,因为所有的进程切换都是在内核中进行的。
void
scheduler(void)
{
  ...
      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;
          w_satp(MAKE_SATP(p->kpagetable)); // set satp tobe the process's kernel pagetable, 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();  // set satp beto kernel_pagetable, when there's no process running.
        c->proc = 0;

        found = 1;
      }
...
}
  1. 进程释放的时候,要把p->kapgetable一起释放,注意,p->kpagetable中直接映射到clint等外部设备的pte,只释放pte,不释放对应的物理地址,因此,这里真正释放物理内存的其实只有p->kstack中指向的内存。
void
freewalk_kproc(pagetable_t pagetable) {
    for(int i = 0; i < 512; i++){
        pte_t pte = pagetable[i];
        if((pte & PTE_V)){
            pagetable[i] = 0;
            if ((pte & (PTE_R|PTE_W|PTE_X)) == 0)
            {
                uint64 child = PTE2PA(pte);
                freewalk_kproc((pagetable_t)child);
            }
        }
    }
    kfree((void*)pagetable);
}

static void
freeproc(struct proc *p)
{
  ...
    if (p->kstack) //here free kernel stack, also its physical memory
    {
        pte_t* pte = walk(p->kpagetable, p->kstack, 0);
        if (pte == 0)
            panic("freeproc: walk");
        kfree((void*)PTE2PA(*pte));
    }
    p->kstack = 0;
    if (p->kpagetable)//just unmap, won't free physical meory as they are mapped with some devices as others.
        freewalk_kproc(p->kpagetable);
 ...
}
  1. 最后一个容易忽略的地方,是kvmpa()函数的修改。这个函数将kernel virtural address转化为physical address,所用的pagetable是全局的kernel_pagetable,要将其修改为p->kpagetable。提问:在scheduler函数中不是已经替换了kernel pagetable吗,为什么这里还要替换一次?回答:main函数在一系列系统初始化函数之后,最后运行的scheduler(),难道系统初始化的时候的那些序列进程就不需要用自己的p->kpagetable了吗?
// vm.c 
#include "spinlock.h" 
#include "proc.h"

//vm.c
// 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;
  
  pte = walk(myproc()->kpagetable, va, 0); // HERE
  if(pte == 0)
    panic("kvmpa");
  if((*pte & PTE_V) == 0)
    panic("kvmpa");
  pa = PTE2PA(*pte);
  return pa+off;
}
Simplify copyin/copyinstr(hard)

在xv6中,内核要访问用户内存的时候,首先根据虚拟地址,用user pagetable得到物理地址,再将物理地址指向的内容复制到某一指定的内存中。本实验要求将用户映射(user pagetable)加入到进程的kpagetable中,使得引用用户指针的时候,可以不用翻译地址,而只需要直接解引用。

  1. 首先写一个函数,实现,将user pagetable复制到kpagetable中,且不能超过PLIC的高度(系统初始化完成后,程序只能访问PLIC及以上的位置)(由于user pagetable总是从虚拟地址0开始的,因此将user pagetable复制到kpagetable上的时候,也将从虚拟地址0的位置开始覆盖kpagetable,好在kpagetable中只需要保留PLIC及以上的位置就可以满足程序运行需求)。遍历user pagetable中需要复制的pte,这里是从old_size到new_size范围内的page,
// copy the user page table to kernel page table
void
u2kvmcopy(pagetable_t pagetable, pagetable_t kpagetable, uint64 oldsz, uint64 newsz)
{
    pte_t *pte_from, *pte_to;
    uint64 a, pa;
    uint flags;

    if (newsz < oldsz)
        return;
    oldsz = PGROUNDUP(oldsz);  // page-alian
    for (a = oldsz; a < newsz; a += PGSIZE)
    {
        if ((pte_from = walk(pagetable, a, 0)) == 0) // one pte point to one page
            panic("u2kvmcopy: pte should exist");
        if ((pte_to = walk(kpagetable, a, 1)) == 0)  // do_alloc = 1
            panic("u2kvmcopy: walk fails");
        pa = PTE2PA(*pte_from);
        flags = (PTE_FLAGS(*pte_from) & (~PTE_U));  // ptes in kpagetable is not useable by user. 
        *pte_to = PA2PTE(pa) | flags;
    }
}

// defs.h 
void            u2kvmcopy(pagetable_t, pagetable_t, uint64, uint64);
  1. 在修改了user pagetable的地方,同时也同样地修改kpagetable。
 // proc.c : 

 // userinit()
  p->sz = PGSIZE;
    u2kvmcopy(p->pagetable, p->kpagetable, 0, p->sz);  // HERE
  // prepare for the very first "return" from kernel to user.
  p->trapframe->epc = 0;      // user program counter

 //growproc()
int
growproc(int n)
{
    uint sz;
    struct proc *p = myproc();

    sz = p->sz;

    if(n > 0){
        if (PGROUNDUP(sz + n) >= PLIC)
           return -1;
        if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
            return -1;
        }
        u2kvmcopy(p->pagetable, p->kpagetable, sz-n, sz); // HERE
    } else if(n < 0){  // here you should unmap p->kpagetable, but if you don't unmap, nothing will go wrong.
        sz = uvmdealloc(p->pagetable, sz, sz + n);
    }
    p->sz = sz;
    return 0;
}

// fork()
	np->cwd = idup(p->cwd);   //fork() don't need to see if sz>=PLIC, because parent's user pagetable sz is <PLIC
    u2kvmcopy(np->pagetable, np->kpagetable, 0, np->sz);  // HERE
	safestrcpy(np->name, p->name, sizeof(p->name));


// exec.c
    if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
      goto bad;
    if(sz1>=PLIC) // HERE
        goto bad;
    sz = sz1;
	...
        
    stackbase = sp - PGSIZE;
    u2kvmcopy(pagetable, p->kpagetable, 0, sz); // HERE
  	// Push argument strings, prepare rest of stack in ustack.
  	for(argc = 0; argv[argc]; argc++) {
  1. 最后把copyin和copyinstr替换成test文件中准备的对应new函数即可。
//defs.h
int    copyin_new(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len);
int    copyinstr_new(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max);


// 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 pagetable, char *dst, uint64 srcva, uint64 len)
{
  return copyin_new(pagetable,dst,srcva,len);
}

// Copy a null-terminated string from user to kernel.
// Copy bytes to dst from virtual address srcva in a given page table,
// until a '\0', or max.
// Return 0 on success, -1 on error.
int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
    return copyinstr_new(pagetable,dst,srcva,max);
}

【lab3.3这个实验debug了很久,拉黑了】

【两个txt文件不管了嗷】

标签:uint64,kernel,pagetable,PTE,MIT6,pte,virtual,page
来源: https://www.cnblogs.com/MiaoMiaoGarden/p/15655962.html