由于涉及到system的回收和attack的创建,内存页表情况相对复杂,因此单独记录梳理。
TL;NR
因为是FILO的,只用关心发生在secret回收内存中,比heap更晚进入全局链表的内存页和fork,exec过程中的内存页即可。 同时fork过程中属于父进程的要归还也不用关心(除了trapframe,但可以视为exec阶段分配的,对本问题无影响)
因此:
- 晚于heap回收的,secret的三张页表页和attack的三张页表页抵消
- pipe需要一页,attack中的data,text,guard page,stack,trapframe 各1页,共6页
- 22-6 = 16,因此在第17页,跳过16页即可。
进程销毁
回收主要由父进程的wait触发,如果父进程父进程不在某个时刻调用wait/waitpid等,子进程就会一直停留在 ZOMBIE 状态(exit标记的),其残留的内存页表项和内核栈也不会被释放,甚至被init接管后才能回收。
int wait(uint64 addr) {
//...
for(pp = proc; pp < &proc[NPROC]; pp++){
if(pp->parent == p){
// 遍历所有子进程
//...
if(pp->state == ZOMBIE){
pid = pp->pid;
if(addr != 0 && copyout(p->pagetable, addr, (char *)&pp->xstate,
sizeof(pp->xstate)) < 0) {
release(&pp->lock);
release(&wait_lock);
return -1;
}
// 实际free
freeproc(pp);
release(&pp->lock);
release(&wait_lock);
return pid;
}
release(&pp->lock);
}
//...
}
static void
freeproc(struct proc *p)
{
if(p->trapframe)// 每个进程在内核栈上预留的一块内存,用来在用户态 ↔ 内核态 切换时保存和恢复该进程的全部 CPU 寄存器状态。
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
// ...
}
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
//删除PTE项,不实际free, do_free = 0
uvmunmap(pagetable, TRAMPOLINE, 1, 0);//只读的内核代码页,无需free
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, sz); // 把从0到sz,该进程所持有的页都回收
}
void
uvmfree(pagetable_t pagetable, uint64 sz)
{
if(sz > 0)
// 虚拟空间地址从低到高回收页面
uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);
freewalk(pagetable); //dfs 回收所有页表
}
进程创建
进程创建基本分为两个步骤,fork和exec阶段,每个阶段都需要建立一个用户态页表。
int
fork(void)
{
// 分配一个 proc 结构和内核栈、trapframe、pagetable
if((np = allocproc()) == 0){
return -1;
}
// 复制父进程的用户页表和所有用户物理页
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;
np->trace_mask = p->trace_mask;
//...
return pid;
}
static struct proc*
allocproc(void)
{
//...
// trapframe 页
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// 根页表
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
//...
}
int
exec(char *path, char **argv)
{
//...
// 分配根页表
if((pagetable = proc_pagetable(p)) == 0)
goto bad;
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
// 分配程序段页表, data, text
if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
goto bad;
sz = sz1;
// ...
}
// ...
p = myproc();
uint64 oldsz = p->sz;
// 分配guard page+stack size, 共2页
sz = PGROUNDUP(sz);
uint64 sz1;
if((sz1 = uvmalloc(pagetable, sz, sz + (USERSTACK+1)*PGSIZE, PTE_W)) == 0)
//...
//归还内存页和页表页,沿用fork创建的内核页
proc_freepagetable(oldpagetable, oldsz);
//...
}
分析
secret在heap之后归还的页仅有用户页表页 x3fork阶段不用考虑,因为拿到的所有page仍要返回,trapframe 看作是exec阶段创建的。exectrapframe x1, 一级页表页 x1, 二级三级页表页 x2, data+text x2, guard+stack x2
注意因为管理内存页是FILO的,因此原本应该是0-31中的9,跳过9页,现在需要反过来跳过31-9=22页,再加上trapframe,data+text, guard+stack (页表页抵消了), 22-5=17.
此外父进程pipe还需要1页,综上应该是跳过16页。
#include "kernel/types.h"
#include "kernel/fcntl.h"
#include "user/user.h"
#include "kernel/riscv.h"
int
main(int argc, char *argv[])
{
int X=16;
char *end = sbrk(PGSIZE*(X+1));
write(2, end+X*PGSIZE+32, 8);
exit(0);
}