fork 与 vfork 区别详解及代码演示

# Man中的描述

fork() 函数

NAME
       fork - create a child process 
       // 创建一个子进程
SYNOPSIS
       #include 

       pid_t fork(void);

描述:

子进程是父进程的一份拷贝,它与父进程共享正文代码段,独占下面这些数据:

  • 数据空间

共享代码段说明,父进程和子进程在fork()之后的指令双方都能看到,所以需要根据fork的返回值控制父子进程的执行代码。值得一提的是为了提高效率,Linux中对子进程的数据采取写实拷贝的技术,当对数据有修改时,才会拷贝。

fork()函数执行成功时有两个返回值:

  • 在父进程返回新创建子进程的进程 ID
  • 在子进程中返回 0

执行失败时:

  • 父进程返回 -1
  • 子进程不会被创建,会设置errno

什么情况下需要使用 fork()

  1. fork()调用者希望有一个进程可以拥有与它同样的资源,而执行不同的操作时。比如,在网络编程中,父进程负责监听客户端的连接请求,当有客户端发起连接时,fork()出一个子进程,让子进程去和客户端进行交互,而父进程继续监听。

  2. 当一个进程要执行一个不同的程序时。比如在shell下,当我们输入一个程序的名字或一条命令的时候(当然这里的命令指外部命令,对于shell可以直接执行的内部命令,则不会fork出并程序替换),并不是shell直接去执行,而是通过fork()创建出一个与自己有相同资源的子进程,然后exec()程序替换,去替我们执行。

vfork() 函数

NAME
       vfork - create a child process and block parent
       // 创建一个子进程并阻塞父进程
SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>

       pid_t vfork(void);

调用方法和返回值同上。

vfork并不会完全复制父进程的地址空间,它的作用是创建一个新进程,而该新进程的目的是 exec 一个新程序

这块可能比较难理解,可以这样理解:如果当前进程需要另外一个进程帮忙做一件事情,也就是立马需要进程进行程序替换。如果我们使用fork的话,它在前期会做大量工作,从父进程拷贝数据空间(堆、栈、全局变量和静态变量等),从而降低效率。

vfork()就不会,在进行程序替换(调用exec())或调用exit()之前,由vfork产生的子进程就在父进程的地址空间执行,也就是说此时它与父进程共享数据,一旦在程序替换或结束之前对父进程的数据进行修改,则会影响到父进程。

fork与vfork不同点总结

  1. vfork调用产生的子进程中,子进程进行程序替换或者exit退出之前,父进程会被阻塞,也就是说保证子进程在父进程之前被执行。

  2. fork 出的子进程 和父进程,谁先执行并不确定,这完全依赖于系统当前负载状况,以及内核调度算法。就算在父进程中进入关键代码区之前采取sleep() 的方式,也不能完全可以保证子进程执行,如果此时系统负载特别重,在sleep 返回之前,子进程还没有得到CPU资源,当sleep()返回时,就不能保证谁先执行,这种情况比较少见,也叫做竞争条件。

  3. vfork不完全复制父进程的地址空间,在exec或exit之前,与父进程共享数据空间,而fork会拷贝一份。

代码演示

fork

观察:子进程和父进程数据独立,子进程修改数据是否会影响父进程。

#include 
#include 

int global = 10;


int main()
{
    int local = 20;
    pid_t id = fork();

    if(id < 0)
    {
        perror("fork fail");
        _exit(1);
    }
    else if(id == 0)
    {// child

        local++;
        global++a;
        printf("child ID is %d, global = %d, local = %d\n", getpid(), global, local);

        printf("in child process global address:%x, local address : %x\n", &global, &local);
    }
    else
    {// parent
        wait(NULL);
        printf("\n\nparent ID is %d, global = %d, local = %d \n", getpid(), global, local);
        printf("in parent process global address:%x, local address : %x\n", &global, &local);
    }


    return 0;
}

fork 与 vfork 区别详解及代码演示_第1张图片

结果:关于值的结果很明显。对于打印出来的地址确实一样的,刚开始有点不解,这不是写实拷贝吗为什么会一样,后来想到,因为有虚拟地址机制,各个进程之间互相独立,也就是说在父进程有一个地址 0X00001010,而同样在子进程也有0X00001010,它们的虚拟地址之所以相同,是因为它是从父进程拷贝过来的,这样理解写实拷贝:在子进程未对数据进程修改之前,子进程和父进程都是指向相同的物理地址,而当子进程需要对数据进行修改时,就会进行写实拷贝,此时分配不同于父进程的物理空间,然后改变页表中的指向,所以在程序中打印的是在自己所属的4GB空间中的虚拟地址,而具体到物理地址中需要根据页表进行虚实地址转换。

vfork

观察在exit之前修改数据,是否会影响父进程。

#include 
#include 
#include 
#include 

int global = 10;

int main()
{
    int local = 20;
    pid_t id = vfork();
    if(id  < 0)
    {
        perror("fork fail");
        exit(1);
    }
    else if (id == 0)
    {// child
        local++;
        global++;
        printf("child ID is %d, global = %d, local = %d\n", getpid(), global, local);
        exit(0);
    }
    else
    {
        printf("parent ID is %d, global = %d, local = %d\n", getpid(), global, local);
    }
    return 0;
}

这里写图片描述

值的提醒的是,在子进程退出的时候使用的是exit()而没有用exec。

注意:vfork 出的子进程必须被 exit或_exit终止或者程序替换,否则会出错(各平台实现不同,行为未定义),出错的原因是子进程如果没有exit,则它运行到 } 时,相当于return,而return 会清空当前函数栈帧,而此时父进程与子进程共享栈,当子进程return返回时,父进程恢复运行,看到我的栈帧怎么没了,一脸懵逼,所以此时栈帧情况不确定, 行为是未定义的,可能是段错误,也可能有别的错误,比如下面的错误。

我测试了在centos6.5上子进程中注释掉exit的情况:

  • 单纯取消掉wait(其实没必要加wait,因为vfork在exit或程序替换之父进程会阻塞,为了对比没有正常结束,所以加入wait):此时会无限制的fork子进程,然后直接报错fork fail: Resource temporarily unavailable程序直接挂掉,也可以说,此时父进程栈帧情形不确定是什么,它可能会“乱跳”。

你可能感兴趣的:(linux)