xv6中的fork()和exec()
状态机模型
在分析fork()和exec()之前,我们需要知道计算机的状态机模型。即从状态机的角度看程序的运行,理解进程和进程切换。
进程=状态机=内存+寄存器
换句话说,有了某一时刻的内存和寄存器值,我们就可以还原那一时刻的状态机状态
fork源码解析
fork()的作用是复制一个进程。所以在分析fork()的源码之前,我们先来想想如果要完完整整地fork()一个进程,需要复制哪些东西?

上图代表一个用户地址空间,一个进程就是有上图各部分组成的,包括代码段text,数据段data,堆区heap,栈区stack,trampoline page和trapframe page以及guard page。所以我们就是要原封不动地复制一个这样的用户地址空间。
下面的代码就是严格按照这张图编写的,配合理解效果最佳。
fork():
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// Allocate process.
// 分配一个proc结构体
if ((np = allocproc()) == 0)
{
return -1;
}
// Copy user memory from parent to child.
// 复制父进程的页表
if (uvmcopy(p->pagetable, np->pagetable, p->sz) < 0)
{
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;
np->parent = p; // 成为父子关系
// copy saved user registers.
*(np->tf) = *(p->tf); // tf的信息也要全部复制,这样才能保证复制后的进程同样正常运行
// Cause fork to return 0 in the child.
np->tf->a0 = 0; // 这一步是区分父进程与子进程的关键
// increment reference counts on open file descriptors.
// 复制已打开的文件
for (i = 0; i < NOFILE; i++)
if (p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
np->state = RUNNABLE; // 子进程可以被调度
np->trace_mask = p->trace_mask;
release(&np->lock);
return pid;
}
fork()函数做的事情有两件:
- 复制整个用户地址空间
- 初始化新的proc
要复制一个一模一样的用户地址空间很简单,uvmcopy()就能做到。原理就是复制整个(p->size)页表,使它们指向同一个物理地址(COW Fork是这样,如果是xv6原始的fork需要复制整个物理内存)。
而复制完用户地址空间后,其它部分的代码就是在初始化刚刚分配的结构体proc了。这里部分的结构体变量是直接复制父进程的,包括sz,tf,ofile,cwd等,而改变的有parent,pid,context等。我们主要从fork()的返回值入手,分析fork()的整体流程。
if(fork() > 0)
{
// 父进程
}
else if(fork() == 0)
{
// 子进程
}
else
{
// 失败
}
我们都知道fork()的返回值有两种情况:父进程返回子进程的pid,子进程返回0。fork()源码解释了为什么。父进程很好解释,因为当前整个fork()函数的执行都是在父进程里执行的,最后的return pid自然返回的也是子进程的pid。而对于子进程,只有在np->state = RUNNABLE后,直到release(&np->lock)那一刻,子进程才是可以被调度器调度的(关于调度器可以参考这篇文章:xv6的Scheduling)。
在明白了xv6调度器的工作原理后,我们知道进程在运行时会被定时器打断,流程类似于yield()->sched()->swtch()->scheduler()->swtch()->sched()->yield()。但对于一个还没有运行的进程,这样的流程显然是不行的,它需要一个初始化的程序,这个程序就是forkret(),forkret()会在allocproc()函数里被调用。
allocproc():
// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, return 0.
// 遍历然后分配一个UNUSED proc,并对结构体成员初始化
static struct proc *
allocproc(void)
{
struct proc *p;
for (p = proc; p < &proc[NPROC]; p++)
{
acquire(&p->lock);
if (p->state == UNUSED)
{
goto found;
}
else
{
release(&p->lock
浙公网安备 33010602011771号