浅谈进程的创建函数fork及vfork

在Linux中,当我们需要创建一个进程的时候,常常会用到fork()函数,以及它的姐妹函数vfork(),下面我们就来谈一下这两个函数分别是在干什么。

fork函数
fork函数是最常用的进程创建的函数,它从一个已知的进程中创建一个新的进程,新进程即为子进程,原来的进程即为父进程。函数基本的用法如下

pid_t pid=fork();
if(pid < 0)
{
    //进程创建失败
    perror("fork");
    return -1;
}
else if(pid == 0)
{
    //pid==0为子进程
    printf("child");
}
else
{
    //pid>0为父进程,此时的pid表示的是从返回到父进程的子进程的pid
    printf("parent");
}       

对于fork来说,关于返回值的问题上面的代码已经说明清楚了。需要注意的是如果fork调用失败,可能是有两种原因

  • 内存不够多了
  • 系统中的进程数量已经达到上限

接下来我们来看一下它在内存中是如何表示的。首先需要说明一下的是,fork出来的父子进程,共享一份代码,但各自有一份数据,代码和数据是写时拷贝的。
这里写图片描述
每一个进程都有各自的PCB(在Linux下即为task_struct),每一个PCB中都有一个指向页表的指针,页表再对应映射在物理地址上。关于fork出来的进程,父子进程虽然代码一样,但是他们各自的页表对应的是不同的物理地址,所以对于数据而言各有一份,并且这些数据和代码是写时拷贝的。

什么是写时拷贝
也就是说,fork出来的子进程,父进程希望将代码和数据原封不动的拷贝过去,这可能是一个很耗时的事情,如果子进程只执行了一件事,比如说进入子进程立即调用exec函数进行程序替换,那么之前拷贝的所有代码和数据都是白费工夫。所以引入了写时拷贝,即在子进程创建的时候,并不立即拷贝父进程中所有的内容,而是在要用的时候才回去拷贝,这样就大大提高了效率。

怎么理解数据是各有一份的
先来看看下面的代码

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

int glob=100;

int main()
{
    pid_t pid=fork();
    if(pid > 0)//parent
    {
        printf("parent glob %d\n",glob);
    }
    else if(pid == 0)//child
    {
        glob=200;
        printf("child glob %d\n",glob);
    }
    else//调用失败
    {
        perror("fork");
        exit(1);
    }
    return 0;
}

这里我们有一个全局变量glob,在父进程中我们直接输出它,在子进程我们变动了glob的值并输出它,那么我们来看一下输出结果
这里写图片描述
子进程输出的是200,父进程输出的100,那么既然是全局变量,为何两者输出的值会是不同的呢?那是因为他们的数据是各自有一份的,子进程修改了自己的数据,并不影响父进程的数据的值,因为他们对应的是不同的物理地址空间。

关于父子进程谁先执行的问题
父子进程谁先执行取决于操作系统调度器,每个人的系统不一样可能就不一样,这并不是固定的。

子进程继承了父进程的PC指针,从fork的地方继续执行
关于这个问题,我们可以来看下面的代码

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    int i;
    for(i=0;i<2;++i)
    {
        int pid=fork();
        if(pid>0)
        {
            printf("father=%d,childpid=%d||",getpid(),pid);
        }
        else if(pid==0)
        {
            printf("child=%d||",getpid());
        }
        else
        {
            perror("fork");
        }
    }
    printf("END\n");
    return 0;
}

我们先来看下结果再来说明问题
这里写图片描述

具体地怎么执行怎么输出的如下图
这里写图片描述

再通过一段代码来简单地看一下上段代码中有’\n’和没有’\n\的区别。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    int i;
    for(i=0;i<2;++i)
    {
        int pid=fork();
        if(pid>0)
        {
            printf("#");
        }
        else if(pid==0)
        {
            printf("@");
        }
        else
        {
            perror("fork");
        }
    }
    return 0;
}

让父进程输出#,让子进程输出@,结果如下
这里写图片描述
这个是没有’\n’的测试代码和结果,总共输出了4个#,4个@。下面来看下有’\n’的代码和结果

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    int i;
    for(i=0;i<2;++i)
    {
        int pid=fork();
        if(pid>0)
        {
            printf("#\n");
        }
        else if(pid==0)
        {
            printf("@\n");
        }
        else
        {
            perror("fork");
        }
    }
    return 0;
}

这里写图片描述
这就只有3个#,3个@了,具体的原因之前的那张分析图中都有讲解到。

vfork函数
关于vfork函数,实际上用的不多。它相对于fork函数主要有几点不同

  • vfork出的子进程一定必父进程先执行,在子进程被调用exec或exit之后父进程才有可能被执行
  • vfork出的子进程和父进程共享地址空间,而fork的子进程具有独立的地址空间。

关于vfork,需要注意几个地方

  • 子进程不应该用return返回,否则会产生逻辑混乱的重复vfork
  • vfork诞生的原因是因为它没有给子进程开辟新的地址空间,而是直接共享了父进程的,当然不是希望子进程做和父进程一样的事,所以vfork出的子进程一般来说创建后立即执行exec函数进行程序替换,在子进程退出或开始新进程之前,内核保证父进程处于阻塞状态。

    关于vfork共享地址空间和fork重新创建一块地址空间,用之前的代码来解释一下,只不过用的是 vfork

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

int glob=100;

int main()
{
    pid_t pid=vfork();
    if(pid > 0)//parent
    {
        printf("parent glob %d\n",glob);
    }
    else if(pid == 0)
    {
        glob=200;
        printf("child glob %d\n",glob);
        exit(0);
    }
    else
    {
        perror("vfork");
        exit(1);
    }
    return 0;
}

在子进程中修改全局变量glob的值,看到如下结果
这里写图片描述
父进程中的glob的值也变成了子进程中修改的值,这是因为父子进程是共享一块地址空间的,这与fork显然是不同的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值