xv6源码剖析 009

xv6源码剖析 009

我们继续昨天的内容。proc.hproc.c

growproc(int n)

int
growproc(int n)
{
  uint sz;
  struct proc *p = myproc();

  sz = p->sz;
  if(n > 0){
    if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
  } else if(n < 0){
    sz = uvmdealloc(p->pagetable, sz, sz + n);
  }
  p->sz = sz;
  return 0;
}

当我们的进程使用sbrk()系统调用申请或者释放内存时,内核会在底层调用这个函数。

fork(void)

我们重点看看这个函数,我们在写代码的时候也会常常用到这个函数,有些时候我们会和另一个系统调用exec一起调用。

int
fork(void)
{
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();

  // Allocate process.
  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->parent = p;
  
  // 复制父进程trapframe
  // trapframe保存着父进程的寄存器的状态
  *(np->trapframe) = *(p->trapframe);

  // Cause fork to return 0 in the child.
  // 设置返回值
  np->trapframe->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;
  // 释放子进程的进程锁
  release(&np->lock);
  // 从父进程中返回子进程的进程id
  return pid;
}

有几个点值得我们注意的:

  1. 一般情况下,我们是这样使用fork的

    int main()
    {
        do_something();
        int pid;
        if ((pid = fork()) == 0)
        {
            do_something_child();
        }
        else if (pid > 0)
        {
            do_something_parent();
        }
        else
        {
            exit(0);
        }
    }
    

    从上面的fork的代码我们可以很好地理解为什么要这样使用,而不是像线程一样直接使用就好了。

    子进程和父进程是相互独立的,它们并不共享相同的资源,这是由操作系统的隔离机制决定的,但是线程不一样,线程是运行在进程中的,而且一个进程中的不同进程是共享一部分进程的内存的;但是不同进程之间的线程就像进程一样,它们的交互是通过进程间通信(ipc)的。

    从代码中,我们知道,当调用fork的时候,内核会完全复制父进程的页表,进程地址空间,trapframe page和相应的文件描述符,等等,唯一不同的就是,内核会直接将父进程的返回值返回(在代码中),而对于子进程则是将返回值保存在一个寄存器中;其实本质上,它们都是相同的,只是在代码中的体现有所不同,因为这个调用是由父进程引起的。

  2. 增减描述符的引用计数

    这个小小的细节也值得我们注意。看下面的ipc通信的小例子。

    void test()
    {
        int pipe[2];
        // 初始化一个匿名管道
        ::pipe(pipe);
        char buff[512];
        
        int pid;
        if ((pid = fork()) == 0)
        {
            memset(buff, 0, sizeof(buff));
            read(pipe[0], buff, sizeof(buff));
            do_something(buff);
        }
        else if (pid > 0)
        {
            write(pipe[1], buff, sizeof(buff));
            do_something();
        }
        else 
        {
            exit(1);
        }
    }
    

    上面是一个常规匿名管道的应用。我们可以思考一下,为什么可以通过一个pipe来进行进程间的通信,明明一个进程在fork的时候,会将父进程的所有内容都进行拷贝?

    文件描述符是文件系统(file system)的一个高级抽象,它的底层是不同类型的数据存储对象,我们在后面文件系统的时候会讲到。现在我们只需要将文件描述符看成一个c语言中的指针,虽然进行了拷贝,但是父进程和子进程指向的内容是一样的,并且操作系统还会在内部维护一个指向该文件的引用计数(reference count),每当用户进程调用close()的时候,就会将这个引用计数减一,当计数为零并且连接数(link)为零时就会关闭这个文件。

  3. copy-on write

    这个机制也属于xv6实验的范畴,所以我只简单讲述一下,

    在内核拷贝父进程的页表时,函数uvmcopy()需要递归地遍历整个页表,以复制整个父进程的进程地址空间。当父进程的空间过大的,扫描就非常耗时,所以,我们一般是直接拷贝父进程页表中的映射(mapping),相当于拷贝一个指针到子进程中,并将每一个pte都设置为只读(read only),当父进程或子进程尝试写这个页面的时候,就会引发缺页中断(page fault)从而陷入(trap)内核,内核将一部分的内存进行复制并重新建立映射关系。然后返回用户空间,重新执行代码。

今晚时间比较少,写的可能少了点,sorry!!!

  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值