xv6源码阅读——虚拟内存

目录

说明

内核地址空间

进程地址空间

kernel/main.c

kinit()函数

freerange()

kvminit()函数

kalloc()函数

kvmmap()函数

proc_mapstacks()函数

kvminithart()函数

procinit()函数


说明

阅读的代码是xv6-riscv版本的

内核地址空间

Xv6为每个进程维护一个页表,用以描述每个进程的用户地址空间,外加一个单独描述内核地址空间的页表。内核配置其地址空间的布局,以允许自己以可预测的虚拟地址访问物理内存和各种硬件资源。下图显示了这种布局如何将内核虚拟地址映射到物理地址。文件(kernel/memlayout.h) 声明了xv6内核内存布局的常量。

xv6源码阅读——虚拟内存_第1张图片

QEMU模拟了一台计算机,它包括从物理地址0x80000000开始并至少到0x86400000结束的RAM(物理内存),xv6称结束地址为PHYSTOP。QEMU模拟还包括I/O设备,如磁盘接口。QEMU将设备接口作为内存映射控制寄存器暴露给软件,这些寄存器位于物理地址空间0x80000000以下。内核可以通过读取/写入这些特殊的物理地址与设备交互;这种读取和写入与设备硬件而不是RAM通信。

进程地址空间

每个进程都有一个单独的页表,当xv6在进程之间切换时,也会更改页表。如图2.3所示,一个进程的用户内存从虚拟地址零开始,可以增长到MAXVA (kernel/riscv.h:348),原则上允许一个进程内存寻址空间为256G。

xv6源码阅读——虚拟内存_第2张图片  

当进程向xv6请求更多的用户内存时,xv6首先使用kalloc来分配物理页面。然后,它将PTE添加到进程的页表中,指向新的物理页面。Xv6在这些PTE中设置PTE_WPTE_XPTE_RPTE_UPTE_V标志。大多数进程不使用整个用户地址空间;xv6在未使用的PTE中留空PTE_V

我们在这里看到了一些使用页表的很好的例子。首先,不同进程的页表将用户地址转换为物理内存的不同页面,这样每个进程都拥有私有内存。第二,每个进程看到的自己的内存空间都是以0地址起始的连续虚拟地址,而进程的物理内存可以是非连续的。第三,内核在用户地址空间的顶部映射一个带有蹦床(trampoline)代码的页面,这样在所有地址空间都可以看到一个单独的物理内存页面。

kernel/main.c

当启动xv6时,在main.c当中会对页表进行初始化,创建,启用等操作

// start() jumps here in supervisor mode on all CPUs.
void
main()
{
    、、、、、、、、、、、、//省略
    kinit();         // 初始化物理页
    kvminit();       // 创建内核页表
    kvminithart();   // 打开分页机制
    procinit();      //为每个进程分配一个内核栈
    、、、、、、、、、、、、//省略
  scheduler();        
}

kinit()函数

在该函数当中会调用freerange()函数将范围内的物理地址进行分页

void
kinit()
{
  initlock(&kmem.lock, "kmem");  //初始化锁
  freerange(end, (void*)PHYSTOP);//将end----PHYSTOP范围内的地址进行分页
}

分配出来的页是拿链表的形式保存下来,每当需要分配页表时,只需要从这个链表中获取就可以,

释放页表后,再将页表插入到该链表当中。结构体声明如下

struct run {
  struct run *next;
};

struct {
  struct spinlock lock;
  struct run *freelist;
} kmem;

freerange()

下来我们看一下freerange()函数是怎样操作的

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    kfree(p);
}

一个页的大小为PGSIZE(1024),我们通过循环拿到范围内每个页表的首地址

然后调用kfree()函数

// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc().  (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
  struct run *r;

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  // Fill with junk to catch dangling refs.
  memset(pa, 1, PGSIZE);

  r = (struct run*)pa;

  acquire(&kmem.lock);
  r->next = kmem.freelist;
  kmem.freelist = r;
  release(&kmem.lock);
}
  1. 在kfree()函数当中先查看,改首地址是否内存对齐
  2. 将内存中的每一个字节设置为1。这将导致使用释放后的内存的代码(使用“悬空引用”)读取到垃圾信息而不是旧的有效内容,从而希望这样的代码更快崩溃。

  3. 然后kfree将页面前置(头插法)到空闲列表中:它将pa转换为一个指向struct run的指针r,在r->next中记录空闲列表的旧开始,并将空闲列表设置为等于r

注意:在进行此类操作时,要上锁,防止同时访问

kvminit()函数

在kvminit中使用 kvmmake (kernel/vm.c:20) 创建内核的页表。此调用发生在 xv6 启用 RISC-V 上的分页之前,因此地址直接引用物理内存。

// Initialize the one kernel_pagetable
void kvminit(void)
{
  kernel_pagetable = kvmmake();
}

我们看一下kvmmake()函数

// Make a direct-map page table for the kernel.
pagetable_t
kvmmake(void)
{
  pagetable_t kpgtbl;

  kpgtbl = (pagetable_t)kalloc();
  memset(kpgtbl, 0, PGSIZE);

  // uart registers
  kvmmap(kpgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

  // virtio mmio disk interface
  kvmmap(kpgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

  // PLIC
  kvmmap(kpgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

  // map kernel text executable and read-only.
  kvmmap(kpgtbl, KERNBASE, KERNBASE, (uint64)etext - KERNBASE, PTE_R | PTE_X);

  // map kernel data and the physical RAM we'll make use of.
  kvmmap(kpgtbl, (uint64)etext, (uint64)etext, PHYSTOP - (uint64)etext, PTE_R | PTE_W);

  // map the trampoline for trap entry/exit to
  // the highest virtual address in the kernel.
  kvmmap(kpgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);

  // map kernel stacks
  proc_mapstacks(kpgtbl);

  return kpgtbl;
}

  1. kvmmake 首先调用kalloc()函数分配一个物理内存页来保存根页表页。
  2. 然后调用kvmmap()函数构建映射关系。包括内核的指令和数据、物理内存的上限到 PHYSTOP,并包括实际上是设备的内存。
  3. Proc_mapstacks (kernel/proc.c:33) 为每个进程分配页表,并且完成映射。

下面我们在深入看看

kalloc()函数

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
  struct run *r;

  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r)
    kmem.freelist = r->next;
  release(&kmem.lock);

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}

该函数作用就是分配一个空闲页表,从kmem空闲链表中拿取

kvmmap()函数

// add a mapping to the kernel page table.
// only used when booting.
// does not flush TLB or enable paging.
void kvmmap(pagetable_t kpgtbl, uint64 va, uint64 pa, uint64 sz, int perm)
{
  if (mappages(kpgtbl, va, sz, pa, perm) != 0)
    panic("kvmmap");
}

kvmmap(kernel/vm.c:127)调用mappages(kernel/vm.c:138),mappages将范围虚拟地址到同等范围物理地址的映射装载到一个页表中。它以页面大小为间隔,为范围内的每个虚拟地址单独执行此操作。对于要映射的每个虚拟地址,mappages调用walk来查找该地址的PTE地址。然后,它初始化PTE以保存相关的物理页号、所需权限(PTE_WPTE_X和/或PTE_R)以及用于标记PTE有效的PTE_V(kernel/vm.c:153)。

// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned. Returns 0 on success, -1 if walk() couldn't
// allocate a needed page-table page.
int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
  uint64 a, last;
  pte_t *pte;

  if (size == 0)
    panic("mappages: size");

  a = PGROUNDDOWN(va);
  last = PGROUNDDOWN(va + size - 1);
  for (;;)
  {
    if ((pte = walk(pagetable, a, 1)) == 0)
      return -1;
    if (*pte & PTE_V)
      panic("mappages: remap");
    *pte = PA2PTE(pa) | perm | PTE_V;
    if (a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}

proc_mapstacks()函数

// Allocate a page for each process's kernel stack.
// Map it high in memory, followed by an invalid
// guard page.
void proc_mapstacks(pagetable_t kpgtbl)
{
  struct proc *p;

  for (p = proc; p < &proc[NPROC]; p++)
  {
    char *pa = kalloc();
    if (pa == 0)
      panic("kalloc");
    uint64 va = KSTACK((int)(p - proc));
    kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
  }
}

这个函数作用就是给每一个进程分配个页表,然后将内核栈映射到高地址,保护页不进行映射

kvminithart()函数

// 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();

}

kvminithart (kernel/vm.c:53)来安装内核页表。它将根页表页的物理地址写入寄存器satp。之后,CPU将使用内核页表转换地址。由于内核使用标识映射,下一条指令的当前虚拟地址将映射到正确的物理内存地址。

procinit()函数

// initialize the proc table at boot time.
void procinit(void)
{
  struct proc *p;

  initlock(&pid_lock, "nextpid");
  initlock(&wait_lock, "wait_lock");
  for (p = proc; p < &proc[NPROC]; p++)
  {
    initlock(&p->lock, "proc");
    p->kstack = KSTACK((int)(p - proc));
  }
}

将p->kstack指向之前映射好的页面

你可能感兴趣的:(mit6.s081学习笔记,链表,数据结构,mit6.s081,操作系统,MIT)