00_前言

本文主要是理解xv6内核源码对虚拟内存部分的描述。主要是阅读文件:kernel/vm.c,接下来是对剩下的操纵用户态的函数作用的描述。

01_代码

8.walkaddr函数

walkaddr函数是walk函数的一层封装,专门用来查找用户页表中特定虚拟地址va所对应的物理地址。所以需要注意:

  • 它只用来查找用户页表
  • 返回的是物理地址,而非像walk函数那样只返回最终层的PTE

源码:

// Look up a virtual address, return the physical address,
// or 0 if not mapped.
// Can only be used to look up user pages.
uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
  pte_t *pte;
  uint64 pa;

  // 如果虚拟地址大于最大虚拟地址,返回0
  // 物理地址为0的地方是未被使用的地址空间
  // Question: 为什么不像walk函数一样直接陷入panic?
  if(va >= MAXVA)
    return 0;

  // 调用walk函数,直接在用户页表中找到最低一级的PTE
  pte = walk(pagetable, va, 0);
  // 如果此PTE不存在,或者无效,或者用户无权访问
  // 都统统返回0(为什么不陷入panic?)
  if(pte == 0)
    return 0;
  if((*pte & PTE_V) == 0)
    return 0;
  if((*pte & PTE_U) == 0)
    return 0;

  // 从PTE中截取下来物理地址页号字段,直接返回
  pa = PTE2PA(*pte);
  return pa;
}

调用这个函数的函数主要是:

  • copyin
  • copyout
  • copyinstr

这三个函数是vm.c的核心,专门负责内核态和用户态之间数据拷贝的

9.freewalk函数

这个函数的作用就是专门用来回收页表页的内存的,因为页表是多级的结构,所以此函数的实现用到了递归,调用这个函数时应该保证叶子级别页表的映射关系全部解除并释放(这将会由后面的uvmunmap函数负责),因为此函数专门用来回收页表页。

uvmunmap函数和freewalk函数结合,成功实现了页表页、物理页的全面释放。

源码:

// Recursively free page-table pages.
// All leaf mappings must already have been removed.
// 译:递归地释放页表页,所有的叶级别页表映射关系必须已经被解除
void
freewalk(pagetable_t pagetable)
{
  // there are 2^9 = 512 PTEs in a page table.
  // 每一个页表都正好有512个页表项PTE,所以要遍历它们并尝试逐个释放
  for(int i = 0; i < 512; i++){
    // 取得对应的PTE
    pte_t pte = pagetable[i];
    // 注意,这里通过标志位的设置来判断是否到达了叶级页表
    // 如果有效位为1,且读位、写位、可执行位都是0
    // 说明这是一个高级别(非叶级)页表项,且此项未被释放,应该去递归地释放
    if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
      // this PTE points to a lower-level page table.
      uint64 child = PTE2PA(pte);
      // 去递归地释放下一级页表
      freewalk((pagetable_t)child);
      // 释放完毕之后,将原有的PTE全部清空,表示已经完全释放
      pagetable[i] = 0;
    // 如果有效位为1,且读位、写位、可执行位有一位为1
    // 表示这是一个叶级PTE,且未经释放,这不符合本函数调用条件,会陷入一个panic
    } else if(pte & PTE_V){
      panic("freewalk: leaf");
    }
    // 这里隐藏了一个逻辑,即if(pte & PTE_V == 0)
    // 这说明当前PTE已经被释放,不用再次释放了,直接遍历下一个PTE
  }
  // 最后释放页表本身占用的内存,回收,回到上一层递归
  kfree((void*)pagetable);
}
10.uvmcreate函数

这个函数的作用很简单,就是为用户进程分配一个页表页并返回指向此页的指针。

源码:

// create an empty user page table.
// returns 0 if out of memory.
// 译:创建一个空的用户页表,当内存耗尽时返回空指针
pagetable_t
uvmcreate()
{
  pagetable_t pagetable;
  // 分配一个内存页
  pagetable = (pagetable_t) kalloc();
  if(pagetable == 0)
    return 0;
  // 将此页表的每一个PTE完全清空
  memset(pagetable, 0, PGSIZE);
  return pagetable;
}
11.uvminit函数

这个函数的作用是将initcode加载到用户页表的0地址上,initcode是启动第一个进程时所需要的一些代码。这个函数的作用就是将initcode映射到用户地址空间中。首先给出xv6中用户地址空间的设计,可以看到从虚拟地址0开始存放的是进程的代码段,对于操作系统启动的第一个进程而言,这个位置放置的就是initcode代码。

// Load the user initcode into address 0 of pagetable,
// for the very first process.
// sz must be less than a page.
// 译:将用户的initcode加载到页表的0地址
// 仅为第一个进程而服务
// 代码的尺寸必须小于一个页(4096 bytes)
void
uvminit(pagetable_t pagetable, uchar *src, uint sz)
{
  // mem虽然是一个指针,但是因为内核地址空间中虚拟地址和物理地址
  // 在RAM上是直接映射的,所以它其实也就等于物理地址
  char *mem;

  // 如果要求分配的大小大于一个页面,则陷入panic
  if(sz >= PGSIZE)
    panic("inituvm: more than a page");
  // 分配一页物理内存作为initcode的存放处,memset用来将当前页清空
  mem = kalloc();
  memset(mem, 0, PGSIZE);

  // 在页表中加入一条虚拟地址0 <-> mem的映射,相当于将initcode成功映射到了虚拟地址0
  mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
  // 将initcode的代码一个字节一个字节地搬运到mem地址
  memmove(mem, src, sz);
}

这段函数中调用了memmove函数,它被定义在kernel/string.c中,实现的功能是从src地址拷贝n个字节到dst地址,并返回指向目的地址的指针。

memmove函数的实现:

void*
memmove(void *dst, const void *src, uint n)
{
 // 声明两个活动的指针,用来实时更新拷贝的过程
  const char *s;
  char *d;

  // 如果n等于0,说明不需要拷贝字符,直接返回dst
  if(n == 0)
    return dst;

  s = src;
  d = dst;
  // case1:src与dst字符串部分重叠时,倒序对字节进行复制,这样可以避免覆盖问题
  if(s < d && s + n > d){
    // 调整指针到字符串尾部,准备开始倒序复制
    s += n; 
    d += n;
    while(n-- > 0)
      *--d = *--s;
  } else
    // 否则直接正序复制即可
    while(n-- > 0)
      *d++ = *s++;

  return dst;
}

memmove函数的实现中最重要的就是分类讨论,分类讨论是为了分析潜在的Src和Dst的地址重叠问题,重叠时反向复制(从终点开始复制到起点),否则正向复制(从起点复制到终点)。

12.uvmunmap函数
(1)函数的作用

这个函数的作用是取消用户进程页表中指定范围的映射关系。当调用这个函数时,系统就知道从指定的虚拟地址va开始,释放一段连续的虚拟地址范围的映射关系,该范围由npages个页面组成。由于页表的管理是以页面为单位的,因此va必须是一个页对齐的地址,即它必须是页大小的整数倍,以确保内存管理系统能够正确处理这些映射关系。

(2)具体代码
// Remove npages of mappings starting from va. va must be
// page-aligned. The mappings must exist.
// Optionally free the physical memory.
// 译:从虚拟地址va开始移除npages个页面的映射关系
// va必须是页对齐的,映射必须存在
// 释放物理内存是可选的
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
  uint64 a;
  pte_t *pte;

  // va不是页对齐的,陷入panic  
  if((va % PGSIZE) != 0)
    panic("uvmunmap: not aligned");
   // 通过遍历释放npages * PGSIZE大小的内存
  for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
    // 如果虚拟地址在索引过程中对应的中间页表页不存在,陷入panic
    // 回顾一下,walk函数返回0,只有一种情况,那就是某一级页表页在查询时发现不存在
    if((pte = walk(pagetable, a, 0)) == 0)
      panic("uvmunmap: walk");

    // 查找成功,但发现此PTE不存在,陷入panic
    if((*pte & PTE_V) == 0)
      panic("uvmunmap: not mapped");

    // 查找成功,但发现此PTE除了valid位有效外,其他位均为0
    // 这暗示这个PTE原本不应该出现在叶级页表(奇怪的错误),陷入panic
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");

    // 否则这是一个合法的,应该被释放的PTE
    // 如果do_free被置位,那么还要释放掉PTE对应的物理内存
    if(do_free){
      uint64 pa = PTE2PA(*pte);
      kfree((void*)pa);
    }
    // 最后将PTE本身全部清空,成功解除了映射关系
    *pte = 0;
  }
}
13.uvmdealloc函数
(1)函数的作用

这个函数的主要目的是调整用户进程的虚拟地址空间大小,它可以通过回收不再需要的页面来缩小地址空间,也可以通过增加新的页面来扩展地址空间。oldsz表示当前的空间大小,而newsz是调整后的目标大小。函数返回调整后的新地址空间大小。

关键点

这个函数既可以扩展地址空间(如果newsz大于oldsz),也可以缩小地址空间(如果newsz小于oldsz),因此不一定会导致地址空间缩小。

(2)具体代码
// Deallocate user pages to bring the process size from oldsz to
// newsz.  oldsz and newsz need not be page-aligned, nor does newsz
// need to be less than oldsz.  oldsz can be larger than the actual
// process size.  Returns the new process size.
// 译:回收用户页,使得进程的内存大小从oldsz变为newsz。oldsz和newsz不一定要是
// 页对齐的,newsz也不一定要大于oldsz。oldsz可以比当前实际所占用的内存大小更大。
// 函数返回进程新占用的内存大小
uint64
uvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{
  // 如果新的内存大小比原先内存还要大,那么什么也不用做,直接返回oldsz即可
  if(newsz >= oldsz)
    return oldsz;

  // 如果newsz经过圆整后占据的页面数小于oldsz
  // PGROUNDUP宏定义的讲解见上一篇博客
  if(PGROUNDUP(newsz) < PGROUNDUP(oldsz)){
    // 计算出来要释放的页面数量
    int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
    // 调用uvmunmap,清空叶级页表的PTE并释放物理内存
    // 因为我们使用了PGROUNDUP来取整页面数量,所以这里可以保证va是页对齐的
    // 因为用户地址空间是从地址0开始紧密排布的, 所以PGROUNDUP(newsz)对应着新内存大小的结束位置
    // 注意do_free置为1,表示一并回收物理内存
    uvmunmap(pagetable, PGROUNDUP(newsz), npages, 1);
  }

  return newsz;
}
14.uvmalloc函数
(1)函数的作用

uvmallocuvmdealloc分别用于为用户进程分配内存和回收内存。

  • uvmalloc申请新的内存并将其映射到用户进程的虚拟地址空间
  • uvmdealloc则释放不再需要的内存。
(2)具体代码
// Allocate PTEs and physical memory to grow process from oldsz to
// newsz, which need not be page aligned.  Returns new size or 0 on error.
// 译:分配PTE和物理内存来将分配给用户的内存大小从oldsz提升到newsz
// oldsz和newsz不必是页对齐的
// 成功时返回新的内存大小,出错时返回0
uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{

  char *mem;
  uint64 a;

  // 如果新的内存大小更小,不用分配,直接返回旧内存大小
  if(newsz < oldsz)
    return oldsz;

  // 计算原先内存大小需要至少多少页,因为进程地址空间紧密排列
  // 所以这里oldsz指向的其实是原先已经使用内存的下一页,崭新的一页
  oldsz = PGROUNDUP(oldsz);
  // 开始进行新内存的分配
  for(a = oldsz; a < newsz; a += PGSIZE){
    // 获取一页新的内存
    mem = kalloc();
    // 如果mem为空指针,表示内存耗尽
    // 释放之前分配的所有内存,返回0表示出错
    if(mem == 0){
      uvmdealloc(pagetable, a, oldsz);
      return 0;
    }
    // 如果分配成功,则将新分配的页面全部清空
    memset(mem, 0, PGSIZE);
    // 并在当前页表项中建立起来到新分配页表的映射
    // mappages函数的讲解见完全解析系列博客(1)
    if(mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
      // 如果mappages函数调用返回值不为0,表明在调用walk函数时索引到的PTE无效
      // 释放之前分配的所有内存,返回0表示出错
      kfree(mem);
      uvmdealloc(pagetable, a, oldsz);
      return 0;
    }
  }
  // 如果成功跳出循环,表示执行成功,返回新的内存空间大小
  return newsz;
}
15.uvmcopy函数
(1)函数的作用

uvmcopy函数是为fork系统调用服务的,它会将父进程的整个地址空间全部复制到子进程中,这包括页表本身和页表指向的物理内存中的数据。

uvmcopy会复制父进程的页表结构和页表所指向的物理内存内容,这种复制通常包括堆(heap)、栈(stack)、数据段、代码段,这样子进程在初始阶段能够与父进程共享内存。

16.uvmclear函数
(1)函数的作用

uvmclear函数专门用来清除一个PTE的用户使用权限,用于exec函数来设置守护页。

17.uvmfree函数

uvmunmap用于取消叶级页表的映射关系,freewalk用于释放页表页,两者结合可以完全释放内存空间吗。uvmfree函数就是两者的一个简单结合和封装,用于完全释放用户的地址空间。


踏上取经路,比抵达灵山更重要