duduru

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

posted on 2024-01-24 15:58  duduru  阅读(0)  评论(0)    收藏  举报  来源

导航