在Linux中,当我们需要创建一个进程的时候,常常会用到fork()函数,以及它的姐妹函数vfork(),下面我们就来谈一下这两个函数分别是在干什么。
fork函数
fork函数是最常用的进程创建的函数,它从一个已知的进程中创建一个新的进程,新进程即为子进程,原来的进程即为父进程。函数基本的用法如下
pid_t pid=fork();
if(pid < 0)
{
//进程创建失败
perror("fork");
return -1;
}
else if(pid == 0)
{
//pid==0为子进程
printf("child");
}
else
{
//pid>0为父进程,此时的pid表示的是从返回到父进程的子进程的pid
printf("parent");
}
对于fork来说,关于返回值的问题上面的代码已经说明清楚了。需要注意的是如果fork调用失败,可能是有两种原因
- 内存不够多了
- 系统中的进程数量已经达到上限
接下来我们来看一下它在内存中是如何表示的。首先需要说明一下的是,fork出来的父子进程,共享一份代码,但各自有一份数据,代码和数据是写时拷贝的。
每一个进程都有各自的PCB(在Linux下即为task_struct),每一个PCB中都有一个指向页表的指针,页表再对应映射在物理地址上。关于fork出来的进程,父子进程虽然代码一样,但是他们各自的页表对应的是不同的物理地址,所以对于数据而言各有一份,并且这些数据和代码是写时拷贝的。
什么是写时拷贝
也就是说,fork出来的子进程,父进程希望将代码和数据原封不动的拷贝过去,这可能是一个很耗时的事情,如果子进程只执行了一件事,比如说进入子进程立即调用exec函数进行程序替换,那么之前拷贝的所有代码和数据都是白费工夫。所以引入了写时拷贝,即在子进程创建的时候,并不立即拷贝父进程中所有的内容,而是在要用的时候才回去拷贝,这样就大大提高了效率。
怎么理解数据是各有一份的
先来看看下面的代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int glob=100;
int main()
{
pid_t pid=fork();
if(pid > 0)//parent
{
printf("parent glob %d\n",glob);
}
else if(pid == 0)//child
{
glob=200;
printf("child glob %d\n",glob);
}
else//调用失败
{
perror("fork");
exit(1);
}
return 0;
}
这里我们有一个全局变量glob,在父进程中我们直接输出它,在子进程我们变动了glob的值并输出它,那么我们来看一下输出结果
子进程输出的是200,父进程输出的100,那么既然是全局变量,为何两者输出的值会是不同的呢?那是因为他们的数据是各自有一份的,子进程修改了自己的数据,并不影响父进程的数据的值,因为他们对应的是不同的物理地址空间。
关于父子进程谁先执行的问题
父子进程谁先执行取决于操作系统调度器,每个人的系统不一样可能就不一样,这并不是固定的。
子进程继承了父进程的PC指针,从fork的地方继续执行
关于这个问题,我们可以来看下面的代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int i;
for(i=0;i<2;++i)
{
int pid=fork();
if(pid>0)
{
printf("father=%d,childpid=%d||",getpid(),pid);
}
else if(pid==0)
{
printf("child=%d||",getpid());
}
else
{
perror("fork");
}
}
printf("END\n");
return 0;
}
我们先来看下结果再来说明问题
具体地怎么执行怎么输出的如下图
再通过一段代码来简单地看一下上段代码中有’\n’和没有’\n\的区别。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int i;
for(i=0;i<2;++i)
{
int pid=fork();
if(pid>0)
{
printf("#");
}
else if(pid==0)
{
printf("@");
}
else
{
perror("fork");
}
}
return 0;
}
让父进程输出#,让子进程输出@,结果如下
这个是没有’\n’的测试代码和结果,总共输出了4个#,4个@。下面来看下有’\n’的代码和结果
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int i;
for(i=0;i<2;++i)
{
int pid=fork();
if(pid>0)
{
printf("#\n");
}
else if(pid==0)
{
printf("@\n");
}
else
{
perror("fork");
}
}
return 0;
}
这就只有3个#,3个@了,具体的原因之前的那张分析图中都有讲解到。
vfork函数
关于vfork函数,实际上用的不多。它相对于fork函数主要有几点不同
- vfork出的子进程一定必父进程先执行,在子进程被调用exec或exit之后父进程才有可能被执行
- vfork出的子进程和父进程共享地址空间,而fork的子进程具有独立的地址空间。
关于vfork,需要注意几个地方
- 子进程不应该用return返回,否则会产生逻辑混乱的重复vfork
vfork诞生的原因是因为它没有给子进程开辟新的地址空间,而是直接共享了父进程的,当然不是希望子进程做和父进程一样的事,所以vfork出的子进程一般来说创建后立即执行exec函数进行程序替换,在子进程退出或开始新进程之前,内核保证父进程处于阻塞状态。
关于vfork共享地址空间和fork重新创建一块地址空间,用之前的代码来解释一下,只不过用的是 vfork
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int glob=100;
int main()
{
pid_t pid=vfork();
if(pid > 0)//parent
{
printf("parent glob %d\n",glob);
}
else if(pid == 0)
{
glob=200;
printf("child glob %d\n",glob);
exit(0);
}
else
{
perror("vfork");
exit(1);
}
return 0;
}
在子进程中修改全局变量glob的值,看到如下结果
父进程中的glob的值也变成了子进程中修改的值,这是因为父子进程是共享一块地址空间的,这与fork显然是不同的。