Linux创建子进程- fork()和vfork()

fork

fork()是Linux中在一个进程中创建一个子进程的系统调用。

函数原型

#include <unistd.h>

pid_t fork(void);

进程创建的一般过程

1.给新进程分配一个标识符,在内核中分配一个PCB

2.复制父进程的环境(不复制地址空间)

3.分配资源(程序,数据,堆栈等)

4.复制父进程的地址空间的内容

5.将进程置成就绪状态,放入就绪队列


从父进程继承了什么东西?

地址空间(写时拷贝)

进程上下文

文件描述符

环境

当前路径

根目录

进程堆栈

信号量

控制终端

进程调度优先级


不会从父进程继承什么?

父进程的pid子进程不继承

父进程的未决信号子进程不继承

父进程的锁子进程不继承


理论上子进程有独立的进程ID号和地址空间,但是在考虑到效率问题后,Linux引入了写时拷贝技术(COW):

即fork出来的子进程完全复制父进程的内存空间(包括代码段,数据段,堆空间,栈空间),也同时复制了页表,但是没有复制物理页面,所以这时子进程的虚拟地址和物理地址同父进程完全相同,但是会把父子进程共享的页面标记为”只读”,如果父子进程中的任何一个进程要对共享区域进行“写操作”,那么内核会复制一个物理页面给这个进程使用,同时修改页表,使虚拟进程映射到新的物理页面。而留下的只读页面被改写为“可读写”,留给另外一个进程使用。


vfork

vfork()的作用同fork()相同,都是创建一个子进程,但是也与fork()有所不同

特点

1.vfork()创建的子进程和父进程共享地址空间,而fork()的子进程具有独立的地址空间。

2.vfork()保证子进程先运行,在他调用exec或 exit()之后父进程才可能被调度运行。

注意exit()和return:

1.使用vfork创建的子进程中千万不能使用return,在进程运行结束后,想要退出时一定要用exit()来结束进程

至于为什么不能使用return?要明确returnexit的区别

  • 本质上的区别:

    1. return是函数返回,返回后释放栈帧资源,把控制权交给调用函数

    2. exit是进程的结束,系统级别的,直接退出整个进程,它将参数返回给OS,把控制权交给操作系统

  • exit函数在调用时会先后执行三步:

    1. 调用退出处理程序atexit(func1)on_exit( fun1 , "退出") ,执行由atexit()函数登记的函数。
    2. 做一些自身的处理工作,如刷新输出缓存,关闭IO流等等
    3. 调用_exit(),退出程序(如果用_exit()退出程序,则它不关闭任何文件,不清除任何缓冲器、也不调用任何终止函数!)
  • 对于单独的进程exit的返回值是返回给操作系统的,但如果是多进程,则是返回给父进程的。


vfork测试

测试代码

  • 测试环境:Linux Centos7
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>

int main()
{
  int a = 180;//栈空间
  int* p = (int *)malloc(sizeof(int));//堆空间
  *p = 999;

  printf("#################  开始  #####################\n");
  pid_t pid = vfork();
  if(pid == -1){ 
    perror("fork");
    return 1; 
  }

//父进程
  if(pid > 0){
   sleep(1);
   //printf("父进程[%d]:a的地址:[%p]   a的值[%d]  p的值[%p]  *p的值:[%d] \n" ,getpid(), &a, a, p, *p);
   printf("父进程[%d]:a的地址:[%p]   a的值[%d]  \n" ,getpid(), &a, a);
    //return 0;
    //exit(0);
  }else {//子进程
    sleep(1);
    //printf("子进程[%d]:a的地址:[%p]   a的值[%d]  p的值[%p]  *p的值[%d]  他的父进程:[%d]\n",getpid(), &a, a, p, *p, getppid() );
    printf("子进程[%d]:a的地址:[%p]   a的值[%d]  他的父进程:[%d]\n",getpid(), &a, a, getppid() );

    //return 0;
    //exit(0);
  }

  printf(" main:[%d]\n",getpid());
  //return 0;
  //exit(0);
}

1. 第一次不使用return 和 exit(报错)

这里写图片描述

打印结果:
这里写图片描述

vfork() 创建一个子进程,和父进程共用一个地址空间,即共用同一个堆栈,vfork是子进程先执行的,因此执行过程如下:

  • 先执行子进程,在子进程结束时,main函数退出调用默认的return,释放堆栈。

  • 后执行父进程,在父进程结束时,main函数退出调用默认的return,释放堆栈,可是由于父子进程共用同一个堆栈,且该堆栈空间已经被子进程释放,因此在释放堆栈时,重复释放,第二次释放的是一个不存在的空间,报出段错误。(可以参考free重复释放同一空间)

至于a的值改变也是因为已有的堆栈空间被释放所导致的。


2. 第二次子进程使用return ,父进程使用exit (无报错)
这里写图片描述
打印结果:
这里写图片描述

子进程return,释放堆栈,结束进程。父进程没用调用return,不会重复释放堆栈,直接调用exit()进程退出。因此不会报错,至于a的值发生改变,同样是因为a所在的地址空间已经被子进程释放,不存在


3. 第三次子进程使用exit ,父进程使用return (正常)
这里写图片描述
打印结果:
这里写图片描述

这个是便是正常结果,子进程没有调用return,不会释放堆栈,自己退出后,不会影响父进程的操作,父进程之后的操作,也不会受子进程的影响。


4. 第四次父子进程都使用exit(无报错)
这里写图片描述
打印结果:
这里写图片描述
父子进程都没有return,只要是由于子进程没有释放堆栈,兑付进程没有影响。


5. 第五次父子进程都使用return
这里写图片描述
打印结果:
这里写图片描述

可以发现,如果父子进程都显式的调用return,可以看到在父子进程都return之后,程序又被执行了一次。但是在第二个子进程return时,堆栈已经不存在,出现段错误,也是由于多次释放同一地址空间导致的。

至于为什么会再次运行程序,大家可以在进行深入的探究。


总结

  • 创建一个子进程可以考虑使用forkvfork

  • fork创建出来的子进程,有自己独立的地址空间,但是在引入“写时拷贝”进行优化后,父子进程开始共用同一空间,无论那一方想要进行数据的“写操作”,会先进行地址空间的拷贝,并修改原来的操作权限。

  • vfork()创建出来的子进程和父进程共用同一个地址空间,子进程首先运行,直到调用exit或者exec后父进程才开始运行。

  • 要注意使用vfork()创建出来的子进程,不能使用return关键字。

以上便是Linux操作系统下的fork和vfork的分析,由于本人能力有限,如果出现什么地方不正确或者讲的不清楚的,可以提出,我会进行改正!

猜你喜欢

转载自blog.csdn.net/xiaozuo666/article/details/81033482