理解vfork函数

来源:互联网 发布:iphone7plus无移动数据 编辑:程序博客网 时间:2024/06/03 23:38

前言

vfork这个函数,起初是在fork函数没有实现写实拷贝机制的时候出现的一个东西。然而现在的fork的函数早已经支持写实拷贝。
提到写实拷贝就多说几句,在操作系统中,创建一个子进程你就得给他分配进程所需的资源,如果每次创建子进程后都要拷贝父进程的资源有点太降低效率了。就是在这个环境下衍生出了vfork这个函数,父子进程同时使用同一份内存空间,虽然省去了重新开辟和拷贝的动作,但却非常危险。

写实拷贝

在后来的发展中,fork实现了写实拷贝的机制,即创建一个子进程后先不给它分配资源,直到父进程或者子进程要进行数据的读写时再进行内存的开辟和拷贝父进程代码段和数据段。

vfork函数

#include<sys/types.h>#include<unistd.h>pid_d vork(void);

函数特点

  • 子进程一定会先于父进程运行
  • 在子进程调用_exit/exit或者exec程序替换后,父进程才有可能运行

本篇文章主要探究,为什么要有这两条规则?

最主要的原因就是父子进程共用同一份地址空间,也就是说如果不加以控制就会引发未知的错误,那么就有下面两种可能:

  1. 父进程影响子进程:对于这一点vfork函数在底层实现,就确保子进程一定先与父进程运行,所以父进程影响子进程这一点,操作系统帮助我们搞定了。
  2. 子进程影响父进程:既然保证了子进程先运行,那么子进程所做的一切都会影响到父进程,因为父子共用同一个地址空间。

如何防止子进程影响到父进程

vfork函数的设计初衷就是创建一个子进程,为了提高效率不给它开辟空间,父子共用,然后子进程直接调用exec函数族去做其他事,因为一旦调用exec程序替换,子进程就去执行别的函数,不会再影响父进程。

为什么必须时_exit/exit函数而不能是return呢?

_exit/exit与return的区别

exit在最后也会调用_exit函数,只不过会做一些清理工作,比如刷新缓冲区。调用这两个函数是直接将进程终止掉。
而return的话是释放局部变量,释放栈帧,回到上一个函数调用的位置。
那么看一下下面这段代码:

#include<stdio.h>#include<unistd.h>#include<stdlib.h>int g_val = 100;int main(){    pid_t id = vfork();    if(id > 0)    {        //father        printf("father id = %d,g_val = %d,address = %p\n",getpid(),g_val,&g_val);    }    else if(id == 0)    {        //child        g_val = 200;printf("child id = %d,g_val = %d,address = %p\n",getpid(),g_val,&g_val);        exit(1);        //return 1;    }    else    {        perror("vfork");        exit(1);    }    return 0;}

这里写图片描述

子进程修改了全局变量的值,然后通过exit退出,可以看到父子进程打印出来的g_val都是被修改过的,可见父子进程共用一个地址控制空间,通过exit函数退出,程序正常。

然后试着让子进程通过return退出,看下效果。
这里写图片描述

父子进程也都打印了结果,但是最后却报了段错误,想想为什么?
调用return返回时,会调整ebp和exp两个寄存器,释放堆栈空间,释放局部变量。因为g_val是一个全局变量,保存在静态全局区,所以父进程最后打印的是被子进程修改过的值。

如果子进程修改的是局部变量的值并且return呢?

#include<stdio.h>#include<unistd.h>#include<stdlib.h>int g_val = 100;int main(){    pid_t id = vfork();    int p_val = 1;    if(id > 0)    {        //father        printf("father id = %d,g_val = %d,address = %p\n",getpid(),g_val,&g_val);        printf("father p_val = %d,address = %p\n",p_val,&p_val);    }    else if(id == 0)    {        //child        g_val = 200;        printf("child id = %d,g_val = %d,address = %p\n",getpid(),g_val,&g_val);        p_val = 2;        printf("father p_val = %d,address = %p\n",p_val,&p_val);        //exit(1);        return 1;    }    else    {        perror("vfork");        exit(1);    }   // printf("process~\n");    //exit(0);    return 0;}

这里写图片描述

因为子进程先运行,将p_val修改为2然后打印,最后调用return返回,当父进程执行时,对应的地址空间处就像什么都没发生一样,所以它还是打印了1。

解释了这个问题,那么就继续说为什么调用return会崩溃,子进程返回时修改了ebp,exp两个寄存器,然后将main函数返回,当父进程运行完return 0时,两个寄存器已经被子进程修改了,而main()函数是被其他函数调用的,父进程的东西被你子进程修改了,所以报出了段错误。

我将main函数最后的return 0 换成exit(0)是不会报错的,因为exit并不会去释放栈帧。

以上就是我关于vfork函数的一些理解。

由于当前的fork()函数具有写实拷贝的机制,所以,尽量可以不使用vfork()就别使用它,因为你一旦使用,你就得时刻提防子进程对父进程的影响。
如果使用的话,切记让子进程直接exec程序替换,并且exit返回。


进程的控制:http://blog.csdn.net/qq_36528114/article/details/78764238

原创粉丝点击