Unix/Linux中的fork函数

fork函数介绍

  一个现有进程可以调用fork函数创建一个新进程。该函数定义如下:

#include <unistd.h>
pid_t fork(void);
// 返回:若成功则在子进程中返回0,在父进程中返回子进程ID,若出错则返回-1

  fork函数调用一次,返回两次。它在调用进程(称为父进程)中返回一次,返回值是新派生进程(称为子进程)的进程ID号;在子进程中返回一次,返回值为0。因此,返回值本身告知当前进程是子进程还是父进程。

  fork在子进程返回0而不是父进程的进程ID的原因在于:任何子进程只有一个父进程,而且子进程总是可以通过调用getppid取得父进程的进程ID。相反,父进程可以有很多子进程,而且无法获取各个子进程的进程ID。如果父进程想要跟踪所有子进程的进程ID,那么它必须记录每次调用fork的返回值。另外,进程ID 0总是由内核交换进程使用,所以一个子进程的进程ID不可能为0。

  子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。父、子进程并不共享这些存储空间部分。父、子进程共享代码段。

  由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、堆和栈的完全复制。作为替代,使用了写时复制(Copy-On-Write, COW)技术。这些区域由父、子进程共享,而且内核将它们的访问权限改变为只读的。如果父、子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统的一“页”。

  使fork失败的两个主要原因是:1)系统中已经有太多的进程(通常意味着某个方面出了问题);2)实际用户的进程总数超过了系统限制。

附:exec函数

  用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec函数只是用一个全新的程序替换了当前进程的正文、数据、堆和栈端。

  关于exec函数详细的请参考:

  APUE 8.10节

  进程控制

一个简单的例子

  简单示例程序如下:

 1 #include <unistd.h>
 2 #include <stdio.h>
 3 
 4 int main()
 5 {
 6     pid_t fpid;
 7     fpid = fork();
 8     
 9     if(fpid < 0)
10     {
11         printf("error in fork!");
12     }
13     else if(fpid == 0)
14     {
15         printf("\n=================================================\n");
16         printf("I am the child process, my process ID is %d\n", getpid());
17         printf("My parent process ID is %d\n", getppid());
18         printf("=================================================\n");
19     }
20     else
21     {
22         printf("\n=================================================\n");
23         printf("I am the parent process, my process ID is %d\n", getpid());
24         printf("=================================================\n");
25         sleep(5);
26     }
27 
28     return 0;
29 }

  程序的运行结果是:

  

  按照惯常,程序按顺序执行,最终输出应该只有if...else if...else中一个条件下的结果,但很明显我们这边输出了两个条件下的结果。具体原因在于通过fork函数创建的子进程也会复制父进程的存储空间(数据、堆、栈等,包括程序计数器),创建了属于自己的存储空间,并从fork函数后开始执行。利用pstree命令可以看到子进程(ID 18406)确实继承自父进程(ID 18405):

  

  一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。在上述程序中,父进程先执行,子进程在其之后执行。

关于fork面试题

题目及答案  

  下边的一道题摘自陈皓的博文

 1 #include <stdio.h>
 2 #include <sys/types.h>
 3 #include <unistd.h>
 4 
 5 int main()
 6 {
 7     int i;
 8     for(i = 0; i < 2; i++)
 9     {
10         fork();
11         printf("-");
12     }
13 
14     sleep(10);
15     return 0;
16 }

  请问上述程序会输出几个‘-’?6个还是8个?

  如果我们不考虑printf函数的缓存效果,程序的最终输出是6个‘-’。但因为printf函数有缓存的效用,最终导致输出了8个‘-’。具体原因可参照陈皓的博文

不考虑printf函数缓存效用

  为了去除printf函数缓存效用,我们稍微改动一下程序:

 1 #include <stdio.h>
 2 #include <sys/types.h>
 3 #include <unistd.h>
 4 
 5 int main()
 6 {
 7     int i;
 8     for(i = 0; i < 2; i++)
 9     {
10         fork();
11         printf("-\n");
12     }
13 
14     sleep(10);
15     return 0;
16 }

  这下输出就正确了。下边两图是程序输出结果和相应的进程(forktest3)树状图:

  

  

更直观的解析

  为了更直观,我们可以修改程序如下:

 1 #include <stdio.h>
 2 #include <sys/types.h>
 3 #include <unistd.h>
 4 
 5 int main()
 6 {
 7     int i;
 8     for(i = 0; i < 2; i++)
 9     {
10         fork();
11         printf("ppid=%d, pid=%d, i=%d \n", getppid(), getpid(), i);
12     }
13 
14     sleep(10);    //让进程停留十秒,这样我们可以用pstree查看一下进程树
15     return 0;
16 }

  输出结果如下:

  

  陈皓还给出了图示解释:

  

  上图中,相同颜色的是同一个进程。

  而对于printf("-");这个语句,我们就可以很清楚的知道,哪个子进程复制了父进程标准输出缓中区里的的内容,而导致了多次输出了。如下图所示,就是阴影并双边框了那两个子进程:

  

其他可参考资料

  Forking vs Threading

  孤儿进程与僵尸进程[总结]

  从一道面试题谈linux下fork的运行机制

  经典的 Fork 炸弹解析

本文参考资料

  《UNIX环境高级编程 第二版》

  一个fork的面试题

 

转载于:https://www.cnblogs.com/xiehongfeng100/p/4618684.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值