在 xv6 里要分清两种“内存”:

  1. 物理页:kalloc() 分出来的真实 4KB 页,最后要 kfree() 还给空闲链表。
  2. 页表映射:虚拟地址 VA 到物理地址 PA 的 PTE 记录。删映射只是把 PTE 清掉,不一定释放 PA。

核心判断标准:

如果这块物理页是这个映射“拥有”的,删映射时就可以 kfree();
如果这块物理页不是这个映射拥有的,或者还会被别处使用,就只能摘掉映射,不能释放物理页。


分配内存

进程分配用户内存时,正常流程是:

  sbrk/growproc
    -> uvmalloc() 
    -> kalloc() 分配物理页
    -> mappages() 建立 VA -> PA 映射

uvmalloc() 为从 oldsz 到 newsz 的进程分配 PTE 和物理内存,这些内存无需对齐到页面,返回新大小,或在出错时返回 0。此函数中,每扩一页都会:

  mem = kalloc();
  memset(mem, 0, PGSIZE);
  mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U);

这里 mem 是新分配的用户物理页,属于当前进程地址空间。所以以后进程缩小内存或退出时,可以释放。

释放内存

释放用户内存时走:

  uvmdealloc()
    -> uvmunmap(..., do_free = 1)
    -> kfree(pa)
    -> 清掉 PTE

uvmunmap() 的逻辑本身很直接:

  if(do_free){
    uint64 pa = PTE2PA(*pte);
    kfree((void*)pa);
  }
  *pte = 0;

所以关键不在 uvmunmap(),而在调用者传不传 do_free = 1,do_free = 1 的意思就是:删映射的同时释放物理页。


但是有些映射只能摘掉,不能释放。典型例子在 proc_freepagetable():

  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  uvmunmap(pagetable, TRAPFRAME, 1, 0);
  uvmfree(pagetable, sz);

这里 TRAMPOLINE 和 TRAPFRAME 都是 do_free = 0。

原因不同:

  • TRAMPOLINE 映射的是内核里的 trampoline 代码,不是这个进程分配出来的用户页,不能 kfree()。
  • TRAPFRAME 映射的是 p->trapframe,但它在 1freeproc() 里已经单独 kfree(p->trapframe) 了,所以这里只能删页表映射,不能再释放一次,否则
    double free。

所以uvmunmap(..., do_free = 0);表示,我只是不想让这个页表再映射它,不代表我要回收那块物理内存。


几个常见场景:

进程退出
  exit()
    -> 进程变 ZOMBIE
    -> 父进程 wait()
    -> freeproc()
    -> proc_freepagetable()
    -> uvmfree()

真正释放地址空间是在父进程 wait() 里调用 freeproc(),不是 exit() 立刻释放。因为退出进程还要保留 struct proc 和退出状态,等父进程收尸。

proc_freepagetable()函数

// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  uvmunmap(pagetable, TRAPFRAME, 1, 0);
  uvmfree(pagetable, sz);
}

首先摘掉这两个特殊页在页表中的映射,TRAPFRAME对应的物理页已经在freeproc里释放了

uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);

然后,用户页会被 uvmfree() 释放:

uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);
freewalk(pagetable);

这里 do_free = 1,释放用户物理页。然后 freewalk() 释放页表页本身。

exec 替换地址空间

exec() 会先创建一套新页表,把新程序装进去。成功后才:

  oldpagetable = p->pagetable;
  p->pagetable = pagetable;
  p->sz = sz;
  proc_freepagetable(oldpagetable, oldsz);

旧地址空间不再用了,所以把所有的旧用户页释放掉,TRAPFRAME和TRAMPOLINE不释放。

fork

普通 xv6 里 fork()uvmcopy(),给子进程重新 kalloc() 物理页,再 memmove() 复制内容。父子进程用户页是两份不同物理页,所以以后各自退出时,都能释放自己的页。

如果是 COW fork,那就不同了:父子进程会临时共享同一物理页。这时删某个进程的映射时不能无脑 kfree(),必须看引用计数。引用计数减到 0 才能真正释放物理页。


lazy allocation 风格中, sbrk() 只扩大 p->sz,不会立刻 kalloc(),真正物理页通常要等访问缺页时再分配。

因此在 lazy allocation 版本里要额外注意:

  • p->sz 变大,不代表对应虚拟地址已经有物理页。
  • 释放/缩小时,可能有些虚拟页根本没映射过。
  • uvmunmap() 原版要求映射必须存在,遇到没映射会 panic;lazy lab 通常要改成:没映射就跳过。

最终记住这条就够了:

  • 释放物理页:这个 PTE 指向的物理页归当前地址空间所有,并且不会再被其他地方使用。
  • 只摘映射:物理页不归当前地址空间所有、已经由别处负责释放、或者仍被共享使用。

大体的内存分配过程

在这里插入图片描述


  1. 进程回收的过程中,父进程调用 wait(),发现某个子进程已经是 ZOMBIE 状态(即子进程已 exit 但还没被回收),会调用这个函数,将该子进程还给进程池。 ↩︎

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐