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?要明确return
和exit
的区别:
本质上的区别:
return
是函数返回,返回后释放栈帧资源,把控制权交给调用函数exit
是进程的结束,系统级别的,直接退出整个进程,它将参数返回给OS,把控制权交给操作系统
exit
函数在调用时会先后执行三步:- 调用退出处理程序
atexit(func1)
,on_exit( fun1 , "退出")
,执行由atexit()
函数登记的函数。 - 做一些自身的处理工作,如刷新输出缓存,关闭IO流等等
- 调用_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时,堆栈已经不存在,出现段错误,也是由于多次释放同一地址空间导致的。
至于为什么会再次运行程序,大家可以在进行深入的探究。
总结
创建一个子进程可以考虑使用
fork
和vfork
。fork
创建出来的子进程,有自己独立的地址空间,但是在引入“写时拷贝”进行优化后,父子进程开始共用同一空间,无论那一方想要进行数据的“写操作”,会先进行地址空间的拷贝,并修改原来的操作权限。vfork()
创建出来的子进程和父进程共用同一个地址空间,子进程首先运行,直到调用exit
或者exec
后父进程才开始运行。要注意使用
vfork()
创建出来的子进程,不能使用return关键字。
以上便是Linux操作系统下的fork和vfork的分析,由于本人能力有限,如果出现什么地方不正确或者讲的不清楚的,可以提出,我会进行改正!