xv6操作系统中和进程相关的内存分配
在 xv6 里要分清两种“内存”:
- 物理页:
kalloc()分出来的真实 4KB 页,最后要 kfree() 还给空闲链表。 - 页表映射:虚拟地址 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 指向的物理页归当前地址空间所有,并且不会再被其他地方使用。
- 只摘映射:物理页不归当前地址空间所有、已经由别处负责释放、或者仍被共享使用。
大体的内存分配过程

-
进程回收的过程中,父进程调用
wait(),发现某个子进程已经是ZOMBIE状态(即子进程已exit但还没被回收),会调用这个函数,将该子进程还给进程池。 ↩︎
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)