zhuzilin's Blog

about

6.828 笔记5

date: 2019-02-20
tags: OS  6.828  

这里会记录阅读6.828课程lecture note的我的个人笔记。可能会中英混杂,不是很适合外人阅读,也请见谅。

Lecture 6: Virtual Memory

Virual Memory Overview

  • 我们需要隔离开的address space

    每个进程都有自己的内存,耶只能读写自己的内存。

    挑战就在于如何能够完成multiplexing的同时保证isolation

  • pagine机制提供了一个addressing的抽象

    CPU -> MMU -> RAM
        VA     PA

    软件只能通过VA进行load/store,而不能通过PA。

    kernel告诉MMU该如何进行这个mapping

    • 本质上,MMU里有一个表,key是VA, value是PA,这个表也就被称为page table

    MMU还可以限制用户能够使用哪些虚拟地址。

  • x86的上述mapping的基本单元是4KB,这个单元被称为page

    同时,mapping是按照4KB对齐的,也就是说每个paging都是start on 4 KB boundary

    因为x86的内存为32位,所以后面的12位对应一个page内部的地址,上面提到的page table做的mapping就是用VA前20位对应到PA的前20位。

  • page table里面的这2^20个entry被称为2^20个page table entry(PTE),我们来看一下一个PTE中有什么:

    PTE的前面20位就是对应的PA(实际上是对应的PA中的那个page)的前20位,其被称为physical page number(PPN)。

    后面的12位都是flag,如PTEP表示是否存在,PTEW表示是否可写,PTE_U表示user program是否可以使用。

  • page table被存在哪里呢?

    被存在RAM中,MMU会读取或存储PTE

    操作系统可以读写PTE

  • 如果page table就仅仅是一个PTE的array,会出现什么问题呢?

    首先是太大了,2^20条,每条32bit,整个table就会是4 MB了,这对于早期的机器太大了。

    并且对于一个小的程序,它不需要那么多内存,可能只需要几百page,剩下的就浪费了。

  • 所以x86使用了一个"two-level page table"以节省空间

    除了在RAM中 分配PTE,还在内存中存一个叫page directory(PD)的东西。

    PD也是一个array,其每一个entry被称为PDE,我们来看一下这个PDE的结构,

    PDE的前20位也是一个PPN,其指向的page是一个用于存page table的page,存的每个page table会指向1024个PTE。

    在PD中有1024个PDE,所以就指向了2^20个PTE。

    刚刚提到了对于一个小程序,可能不需要那么多PTE,所以有的PDE可以是invalid,从而可以让address space变得很小。

  • MMU如何知道page table在RAM的哪里呢?

    %cr3存了PD的地址。PD里面(间接)存了PTE的PA,而这些PTE不一定是连续的。

  • x86 paging hardware是如何翻译VA的?

    首先通过%cr3找到PD的PA,从而可以加载PD;

    然后从VA的前10位找到对应的PT(page table)的PA,从而可以加载PT;

    然后用VA的之后10位找到PTE,PTE的前20位,也就是PPN加上VA的最后12位就得到了VA对应的PA。

  • PTE中的flag

    P, W, U

    xv6用U来防止用户使用kernel memory

  • 如果这些flag没有被set(没有设为1)会出现什么?

    会触发page fault,导致CPU存储寄存器,并强制转化到kernel(进入trap.c)。

    kernel可以选择produce error, kill process或者install a PTE, resume the process

  • 为什么选择mapping而不是其他的,如给一个上下界?

    mapping带来的这种indirection让paging hardware可以解决很多问题,如

    • avoid fragmentation
    • copy-on-write fork
    • lazy allocation

    与其他的很多技巧(这些方法都是啥。。。不知道之后的一讲会不会讲清楚。。。)

  • 为什么在kernel中使用VA?

    显然给user process一个page table是很合理的,但是为什么大多数kernel也这么做?

    • 还是要提一下,的确是有kernel直接跑在PA上的。
    • 有一些原因很好,一些很逊。
    • 硬件使得很难关闭page table ???
    • kernel用user address很方便,但是可能会导致poor isolation between kernel/application
    • 如果地址是连续的,也会很方便。比如kernel has both 4Kbyte objects and 64Kbyte objects(没看懂这是啥意思)
    • 如果没有page table,很容易有memory fragmentation(内存碎片?)

      比如先分配64K, 释放,之后分配4K,4K占据了64K的地方,之后再分配64K就没法弄了。

    • kernel可以跑在有不同physical memory layout的硬件上。

Case study: xv6 use of the x86 paging hardware

  • xv6的address space的一个概况:

    0x00000000:0x80000000 -- user addresses below KERNBASE
    0x80000000:0x80100000 -- map low 1MB devices (for kernel)
    0x80100000:?          -- kernel instructions/data
    ?         :0x8E000000 -- 224 MB of DRAM mapped here
    0xFE000000:0x00000000 -- more memory-mapped devices
  • 为什么用这种布局:

    user virtual addresses 从0开始

    • 注意不同user的0会map到不同的PA

    2GB for user heap to grow contiguously

    • 不过注意这2G不是连续的物理内存,所以就不会有fragmentation problem

    kernel和user都被map了

    • 为了方便进行system call或interrupt
    • 方便kernel读写用户内存

    kernel永远都被map在固定位置:

    • 方便切换进程

    kernel线性映射(pa x mapped at va x+0x80000000)

    • 方便kernel读写物理内存
  • 这种情况最大的进程能有:0x80000000,也就是2G内存

下面我们来看代码:

中间的哪些断点之类的东西可能是和现在的版本代码不一样?所以这里为了弄清楚,我来自由发挥一波。

首先在这里需要记录一下,为了方便查找函数出现的位置,经常会使用的指令是

$ grep -n funcname *.[chS]

main开始,

// Bootstrap processor starts running C code here.
// Allocate a real stack and switch to it, first
// doing some setup required for memory allocator to work.
int
main(void)
{
  kinit1(end, P2V(4*1024*1024)); // phys page allocator
  kvmalloc();      // kernel page table
  mpinit();        // detect other processors
  lapicinit();     // interrupt controller
  seginit();       // segment descriptors
  picinit();       // disable pic
  ioapicinit();    // another interrupt controller
  consoleinit();   // console hardware
  uartinit();      // serial port
  pinit();         // process table
  tvinit();        // trap vectors
  binit();         // buffer cache
  fileinit();      // file table
  ideinit();       // disk 
  startothers();   // start other processors
  kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()
  userinit();      // first user process
  mpmain();        // finish this processor's setup
}

main会先调用kinit1然后在entrypgdir的范围里对kernel进行初始化,然后调用kinit2,其会把剩下的从entrypgdirPHYSTOP + KERNBASE的虚拟内存分给kernel,这两个函数的具体在kalloc.c

// Initialization happens in two phases.
// 1. main() calls kinit1() while still using entrypgdir to place just
// the pages mapped by entrypgdir on free list.
// 2. main() calls kinit2() with the rest of the physical pages
// after installing a full page table that maps them on all cores.
void
kinit1(void *vstart, void *vend)
{
  initlock(&kmem.lock, "kmem");
  kmem.use_lock = 0;
  freerange(vstart, vend);  // 注意这里的freerange就是把这个范围里的physical memory free掉
}

void
kinit2(void *vstart, void *vend)
{
  freerange(vstart, vend);
  kmem.use_lock = 1;
}

之后的userinit是我们的重点,这个函数的目的就是设置第一个用户process,它在proc.c中。

void
userinit(void)
{
  struct proc *p;
  extern char _binary_initcode_start[], _binary_initcode_size[];

  p = allocproc();
  
  initproc = p;
  if((p->pgdir = setupkvm()) == 0)
    panic("userinit: out of memory?");
  inituvm(p->pgdir, _binary_initcode_start, (int)_binary_initcode_size);
  p->sz = PGSIZE;
  memset(p->tf, 0, sizeof(*p->tf));
  p->tf->cs = (SEG_UCODE << 3) | DPL_USER;
  p->tf->ds = (SEG_UDATA << 3) | DPL_USER;
  p->tf->es = p->tf->ds;
  p->tf->ss = p->tf->ds;
  p->tf->eflags = FL_IF;
  p->tf->esp = PGSIZE;
  p->tf->eip = 0;  // beginning of initcode.S

  safestrcpy(p->name, "initcode", sizeof(p->name));
  p->cwd = namei("/");

  // this assignment to p->state lets other cores
  // run this process. the acquire forces the above
  // writes to be visible, and the lock is also needed
  // because the assignment might not be atomic.
  acquire(&ptable.lock);

  p->state = RUNNABLE;

  release(&ptable.lock);
}

我们这次主要考虑page table相关的内容,所以主要有两个函数,setupkvm()inituvm。首先是setupkvm

// There is one page table per process, plus one that's used when
// a CPU is not running any process (kpgdir). The kernel uses the
// current process's page table during system calls and interrupts;
// page protection bits prevent user code from using the kernel's
// mappings.
//
// setupkvm() and exec() set up every page table like this:
//
//   0..KERNBASE: user memory (text+data+stack+heap), mapped to
//                phys memory allocated by the kernel
//   KERNBASE..KERNBASE+EXTMEM: mapped to 0..EXTMEM (for I/O space)
//   KERNBASE+EXTMEM..data: mapped to EXTMEM..V2P(data)
//                for the kernel's instructions and r/o data
//   data..KERNBASE+PHYSTOP: mapped to V2P(data)..PHYSTOP,
//                                  rw data + free physical memory
//   0xfe000000..0: mapped direct (devices such as ioapic)
//
// The kernel allocates physical memory for its heap and for user memory
// between V2P(end) and the end of physical memory (PHYSTOP)
// (directly addressable from end..P2V(PHYSTOP)).

// This table defines the kernel's mappings, which are present in
// every process's page table.
static struct kmap {
  void *virt;  // 这一段开始的虚拟地址
  uint phys_start;  // 实际物理地址起始
  uint phys_end;  // 实际物理地址结束
  int perm;
} kmap[] = {
 { (void*)KERNBASE, 0,             EXTMEM,    PTE_W}, // I/O space
 { (void*)KERNLINK, V2P(KERNLINK), V2P(data), 0},     // kern text+rodata
 { (void*)data,     V2P(data),     PHYSTOP,   PTE_W}, // kern data+memory
 { (void*)DEVSPACE, DEVSPACE,      0,         PTE_W}, // more devices
};

// Set up kernel part of a page table.
pde_t*
setupkvm(void)
{
  pde_t *pgdir;
  struct kmap *k;

  if((pgdir = (pde_t*)kalloc()) == 0)  // kalloc会分配4096B的物理内存
    return 0;
  memset(pgdir, 0, PGSIZE);
  if (P2V(PHYSTOP) > (void*)DEVSPACE)
    panic("PHYSTOP too high");
  for(k = kmap; k < &kmap[NELEM(kmap)]; k++)
    if(mappages(pgdir, k->virt, k->phys_end - k->phys_start,
                (uint)k->phys_start, k->perm) < 0) {
      freevm(pgdir);
      return 0;
    }
  return pgdir;
}

这里的重点函数就是mappages

// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned.
static int
mappages(pde_t *pgdir, void *va, uint size, uint pa, int perm)
{
  char *a, *last;
  pte_t *pte;

  a = (char*)PGROUNDDOWN((uint)va);
  last = (char*)PGROUNDDOWN(((uint)va) + size - 1);
  for(;;){
    if((pte = walkpgdir(pgdir, a, 1)) == 0)
      return -1;
    if(*pte & PTE_P)
      panic("remap");
    *pte = pa | perm | PTE_P;
    if(a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}

在解释mappages之前,需要先解释一下这里面的walkpgdir

// Return the address of the PTE in page table pgdir
// that corresponds to virtual address va.  If alloc!=0,
// create any required page table pages.
static pte_t *
walkpgdir(pde_t *pgdir, const void *va, int alloc)
{
  pde_t *pde;
  pte_t *pgtab;

  pde = &pgdir[PDX(va)];
  if(*pde & PTE_P){
    pgtab = (pte_t*)P2V(PTE_ADDR(*pde));
  } else {
    if(!alloc || (pgtab = (pte_t*)kalloc()) == 0)
      return 0;
    // Make sure all those PTE_P bits are zero.
    memset(pgtab, 0, PGSIZE);
    // The permissions here are overly generous, but they can
    // be further restricted by the permissions in the page table
    // entries, if necessary.
    *pde = V2P(pgtab) | PTE_P | PTE_W | PTE_U;
  }
  return &pgtab[PTX(va)];
}

walkpgdir是找寻va对应的物理地址。

  • 用va的前10位找到va对应的PDE。
  • 如果PDE不是空指针,同时其存在(PTE_P)被设置了,则再把PDE的物理地址转化为虚拟地址

    • 找这个PDE对应的page table在哪里
  • 如果PDE是空的,同时alloc被设置为1了,说明需要分配新的page table了

    • 分配除一个4096B作为page table
    • 然后把这个对应的page table设置好权限,然后赋值给之前找到的PDE
  • 用va的中间10位来从page table中找到对应的PTE的物理地址

    注意PTE的地址指向的值的前20位加上va的后12位就是va对应的pa了。

说完walkpgdir就可以说回mappagessetupvm,其主要就是把kernel的va对应的地址都分配了。

说完setupvm就说inituvm,就是把_binary_initcode_start存在pgdir的最开始的一个page中。

除去最开始的初始化,在执行一个新的进程的时候也会分配新的内存的,有兴趣可以看一下exec.c中的代码。