xv6操作系统源码阅读之页表

硬件支持

在这里插入图片描述
如上图,对于x86硬件来说,其在寻址时(保护模式下),会通过CR3寄存器找到页目录表的地址,接着虚拟地址的高十位会作为页目录表的索引,从而找到对应的页表,接着虚拟地址的中间10个bit会被作为页表中的索引,从而找到对应的页的物理地址,最后虚拟地址的低12位被用来指示其在页中的偏移,由于偏移量占了12位,也就意味着每一个页的大小为4k个字节。
页目录表和页表都有1024条记录,这些记录的高20位对应的就是分配的物理地址,低12位为一些标志位,比如P标志位指示了当前该虚拟地址是否分配了物理地址,如果为0,则会产生错误(陷入中断),再比如U标志位指示了用户进程是否允许访问该页表,如果为0,则该页表项只能由内核访问。由于页目录表中和页表中每一项占32bit,所以每个页目录表和页表的大小也就为4096个字节。

进程地址空间

在这里插入图片描述

如上图,进程的地址空间从0地址一直到KERNBASE,再往上就是内核空间。上图中的各个宏的定义代码如下。

// Memory layout

#define EXTMEM  0x100000            // Start of extended memory
#define PHYSTOP 0xE000000           // Top physical memory
#define DEVSPACE 0xFE000000         // Other devices are at high addresses

// Key addresses for address space layout (see kmap in vm.c for layout)
#define KERNBASE 0x80000000         // First kernel virtual address
#define KERNLINK (KERNBASE+EXTMEM)  // Address where kernel is linked

根据KERNBASE的值可以判断出每个进程所能使用的空间有2GB。
从上图中可以看出内核把自己映射到了0到PHYSTOP的物理地址,PHYSTOP代表物理地址的上限,但是由于内核部分的虚拟空间只有2GB,这也使得即使PHYSTOP大于2GB,也就是说物理内存大于2GB,内核也使用不了,因为物理空间超过了其虚拟空间的大小。
一些内存映射IO设备的物理地址从0xFE000000开始,所以虚拟地址直接映射过去就可以了。
另外内核部分的页表的U标志是清零的,这也代表内核空间是不允许用户进程访问的。
由于每个进程的地址空间都包含了内核和用户两部分,这也使得从用户空间切换到内核空间不需要进行页表的切换。

创建地址空间源码分析

在之前的源码分析系列文章中我们看到了给内核分配地址空间的是kvmalloc函数,其实也就是建立KERNBASE以上虚拟地址的映射,代码如下。

// Allocate one page table for the machine for the kernel address
// space for scheduler processes.
void
kvmalloc(void)
{
  kpgdir = setupkvm();
  switchkvm();
}

可以看到其主要是调用了setupkvm函数,代码如下。

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

  if((pgdir = (pde_t*)kalloc()) == 0)
    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;
}

setupkvm函数主要是对内核部分的进程地址空间进行映射,代码定义如下。

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
};

看到它首先调用kalloc函数分配了一个页目录的地址,kalloc的作用是分配一个4096个字节大小的物理页并返回其指针。接着将其清零,主要进行映射工作的是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;
}

可以看到其中主要调用了walkpgdir函数来得到PTE,其代码如下。

// 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函数主要是在页目录表里面找到对应的表项,如果该表项未被分配页表,则为其分配页表,最后返回pte到mappages,而在mappages里面会对返回的pte进行设置,主要也就是设置相应的物理地址。

物理内存分配

xv6系统分配的物理内存是在内核所使用的最大内存到PHYSTOP之间,也就是上图中的end到PHYSTOP之间的内存。
最开始的时候,系统刚刚切换到保护模式时,使用的是一个临时页表,定义如下。

// The boot page table used in entry.S and entryother.S.
// Page directories (and page tables) must start on page boundaries,
// hence the __aligned__ attribute.
// PTE_PS in a page directory entry enables 4Mbyte pages.

__attribute__((__aligned__(PGSIZE)))
pde_t entrypgdir[NPDENTRIES] = {
  // Map VA's [0, 4MB) to PA's [0, 4MB)
  [0] = (0) | PTE_P | PTE_W | PTE_PS,
  // Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
  [KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
};

在main函数中可以看到调用了两个函数kinit1和kinit2,这两个函数都是初始化xv6的物理内存分配器的,xv6使用一个物理内存分配器的数据结构来管理可分配的物理内存。其中kinit1初始化了从kernel end到4M之间的物理内存,而kinit2初始化了从4M到PHYSTOP之间的物理内存。之所以使用两个初始化函数,是因为在main函数中,其大部分的工作(kinit1和kinit2之间的函数调用)都不能使用锁也不能使用超过4M的内存。
其中物理内存分配器的数据结构定义如下。

struct run {
  struct run *next;
};

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

kinit1函数定义如下。

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

可以看到在kinit1中lock被禁用了,而且物理内存的初始化主要借助了freerange函数,该函数定义如下。

freerange(void *vstart, void *vend)
{
  char *p;
  p = (char*)PGROUNDUP((uint)vstart);
  for(; p + PGSIZE <= (char*)vend; p += PGSIZE)
    kfree(p);
}

在此函数中可以看到是对每一个页都调用kfree来将其加入到物理内存分配器中,kfree函数定义如下。

//PAGEBREAK: 21
// 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(char *v)
{
  struct run *r;

  if((uint)v % PGSIZE || v < end || V2P(v) >= PHYSTOP)
    panic("kfree");

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

  if(kmem.use_lock)
    acquire(&kmem.lock);
  r = (struct run*)v;
  r->next = kmem.freelist;
  kmem.freelist = r;
  if(kmem.use_lock)
    release(&kmem.lock);
}

在kfree函数中,主要是将要free的页的虚拟地址加入struct run的链表中,还有一个操作是将该虚拟地址对应的内存页memset成1,这是为了防止进程引用被释放后的内存单元,因为1是一个无意义的值,进程引用的话会产生错误。

用户侧地址空间


一个进程用户端的地址空间如上图所示,可见主要有堆,栈,数据段和代码段,其中栈中存放了初始的exec的参数,比如argc和argv这些main函数的参数。

sbrk系统调用

当一个进程需要增加或者收缩自己的地址空间的时候,就可以使用sbrk系统调用来实现,在xv6代码里主要是growproc这个函数,代码如下。

// Grow current process's memory by n bytes.
// Return 0 on success, -1 on failure.
int
growproc(int n)
{
  uint sz;
  struct proc *curproc = myproc();

  sz = curproc->sz;
  if(n > 0){
    if((sz = allocuvm(curproc->pgdir, sz, sz + n)) == 0)
      return -1;
  } else if(n < 0){
    if((sz = deallocuvm(curproc->pgdir, sz, sz + n)) == 0)
      return -1;
  }
  curproc->sz = sz;
  switchuvm(curproc);
  return 0;
}

可见这个函数既可以增加,也可以收缩进程的地址空间,主要实现方式就是通过修改进程的页表,经过之前的分析可见进程用户侧所能使用的最大地址就是KERNBASE,对应于内核的起始地址,所以进程的地址扩张时是不能超过此界限的。

exec过程

exec的过程就是xv6中的exec函数。该函数首先打开一个二进制ELF文件。

  if((ip = namei(path)) == 0){
    end_op();
    cprintf("exec: fail\n");
    return -1;
  }

接着对该ELF文件做检查。

  // Check ELF header
  if(readi(ip, (char*)&elf, 0, sizeof(elf)) != sizeof(elf))
    goto bad;
  if(elf.magic != ELF_MAGIC)
    goto bad;

接着为该进程创建内核页表。

  if((pgdir = setupkvm()) == 0)
    goto bad;

接着对ELF中的每个段分配页表(allocuvm)并将其加载进内存(loaduvm)中。

  // Load program into memory.
  sz = 0;
  for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
    if(readi(ip, (char*)&ph, off, sizeof(ph)) != sizeof(ph))
      goto bad;
    if(ph.type != ELF_PROG_LOAD)
      continue;
    if(ph.memsz < ph.filesz)
      goto bad;
    if(ph.vaddr + ph.memsz < ph.vaddr)
      goto bad;
    if((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0)
      goto bad;
    if(ph.vaddr % PGSIZE != 0)
      goto bad;
    if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0)
      goto bad;
  }

接着创建用户的栈(如上一节中的进程侧地址空间图,即用来存放argc和argv)。

  // Allocate two pages at the next page boundary.
  // Make the first inaccessible.  Use the second as the user stack.
  sz = PGROUNDUP(sz);
  if((sz = allocuvm(pgdir, sz, sz + 2*PGSIZE)) == 0)
    goto bad;
  clearpteu(pgdir, (char*)(sz - 2*PGSIZE));
  sp = sz;

  // Push argument strings, prepare rest of stack in ustack.
  for(argc = 0; argv[argc]; argc++) {
    if(argc >= MAXARG)
      goto bad;
    sp = (sp - (strlen(argv[argc]) + 1)) & ~3;
    if(copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
      goto bad;
    ustack[3+argc] = sp;
  }
  ustack[3+argc] = 0;

  ustack[0] = 0xffffffff;  // fake return PC
  ustack[1] = argc;
  ustack[2] = sp - (argc+1)*4;  // argv pointer

  sp -= (3+argc+1) * 4;
  if(copyout(pgdir, sp, ustack, (3+argc+1)*4) < 0)
    goto bad;
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值