本篇文章主要介绍了与进程创建有关的两个函数fork和vfork,以及他们之间的区别
再使用fork和vfork函数时需要添加头文件:
#include<unistd.h> //fork和vfrok函数需要的
#include<sys/types.h> //pid_t 需要的
(1)fork函数
-
函数原型:
pid_t fork()
。该函数是通过复制父进程来创建子进程。 -
返回值:
调用一次fork函数却能够返回两个值。 在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
-
fork出错可能的原因
(1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
(2)系统内存不足,这时errno的值被设置为ENOMEM。 -
写时拷贝技术
为什么要用使用写时拷贝技术?
在说写时拷贝技术之前,我先来分析fork函数。上面我说过,fork函数是通过复制父进程的方式来创建子进程的,也就是说,子进程和父进程是完全相同的。换句话说,就是子进程的pcb与父进程的pcb是一样的,那么他们也就具备相同的虚拟地址空间,这代表他们所映射的内存空间也是一样的,这也就意味着两个进程的数据是完全相同的。那么问题来了,如果我们在子进程中对一些数据进行修改,是不是也会影响到父进程,而一旦影响到父进程势必就会破坏进程的独立性。那么大佬们是如何解决这个问题的呢?答案就是利用了写时拷贝技术
什么是写时拷贝技术?
开始时,两进程的虚拟地址空间相同,但是当子进程想要修改数据时,操作系统会在物理内存中重新开辟一个新的空间,并且将原来的数据拷贝到新空间中,那么此时子进程修改数据就修改的是这个新空间的数据,修改完后,会同时更新页表信息,这就意味着虚拟地址映射的物理地址发生改变,原来的虚拟地址不在对应原来的物理地址,而是对应一个新的物理地址。这样子进程修改的就是自己的数据,而不会波及到父进程,从而保证了进程之间的独立性。这就是写时拷贝技术。
写时拷贝的作用
目的就是提高进程的创建效率。按理说,每个进程都应该有独立的空间,但是若是在子进程创建时就为其开辟空间,这就需要将原来的数据拷贝过去,那么创建过程就很慢。其次,如果将数据拷贝过来,但是子进程就一直没有修改的意思,那么是不是就浪费了。所以开始时,两个进程用同一个空间的数据,只有当子进程要修改某个数据时,才会为其开辟空间。
(2)vfork函数
-
函数原型:
pid_t vfork()
创建一个子进程,并且阻塞父进程,直到子进程exit退出或者程序替换之后,父进程才开始运行。
-
返回值:
成功: 子进程中返回 0,父进程中返回子进程 ID。
失败: 返回 -1。
-
特点:
- vfork 的创建效率较高,因为它创建的子进程与父进程共用同一块虚拟地址空间。
- 共用同一块虚拟地址空间意味着,共用相同的数据段、代码段,这样的话,父子进程同时运行会造成调用栈混乱,因此必须子进程运行,父进程阻塞;直到子进程退出,所有函数出栈才行。
- vfork创建的子进程不能再main函数中使用return退出,因为main中return会释放掉进程资源。你把资源释放掉了,那父进程还咋玩。
-
解释父子进程同时运行会造成调用栈混乱
因为是同一个虚拟地址空间,所以用的栈相同。我们知道执行函数时,必须先入栈。假如父进程先执行fun1函数,将其入栈,但是还没有执行完毕,此时进程切换。于是子进程开始执行fun2函数,再将函数入栈,同样此时没有执行完毕发生进程切换,轮到父进程。但是此时栈中最上面一层是fun2函数,而不是父进程想要执行的fun1函数,所以着就造成调用栈混乱。
(3)fork函数与vfork函数的区别
- fork函数是子进程拷贝父进程的虚拟地址空间,也就是说,子进程会新建立一个,只不过内容和父进程一样;vfork函数是与父进程共享同一个虚拟地址空间。
- fork函数创建的子进程,如果对其修改的话,并不会改变父进程的值;但是vfork函数创建的子进程,如果对其修改的话,会影响到父进程
- fork函数创建的子进程可以和父进程同时运行,只不过运行的顺序不确定(这与操作系统的异步性有关);vfork函数只能由子进程先运行,直到子进程exit退出或者程序替换之后,父进程才开始运行。