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节

  进程控制

一个简单的例子

  简单示例程序如下:

 #include <unistd.h>
#include <stdio.h> int main()
{
pid_t fpid;
fpid = fork(); if(fpid < )
{
printf("error in fork!");
}
else if(fpid == )
{
printf("\n=================================================\n");
printf("I am the child process, my process ID is %d\n", getpid());
printf("My parent process ID is %d\n", getppid());
printf("=================================================\n");
}
else
{
printf("\n=================================================\n");
printf("I am the parent process, my process ID is %d\n", getpid());
printf("=================================================\n");
sleep();
} return ;
}

  程序的运行结果是:

  

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

  

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

关于fork面试题

题目及答案  

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

 #include <stdio.h>
#include <sys/types.h>
#include <unistd.h> int main()
{
int i;
for(i = ; i < ; i++)
{
fork();
printf("-");
} sleep();
return ;
}

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

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

不考虑printf函数缓存效用

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

 #include <stdio.h>
#include <sys/types.h>
#include <unistd.h> int main()
{
int i;
for(i = ; i < ; i++)
{
fork();
printf("-\n");
} sleep();
return ;
}

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

  

  

更直观的解析

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

 #include <stdio.h>
#include <sys/types.h>
#include <unistd.h> int main()
{
int i;
for(i = ; i < ; i++)
{
fork();
printf("ppid=%d, pid=%d, i=%d \n", getppid(), getpid(), i);
} sleep(); //让进程停留十秒,这样我们可以用pstree查看一下进程树
return ;
}

  输出结果如下:

  

  陈皓还给出了图示解释:

  

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

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

  

其他可参考资料

  Forking vs Threading

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

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

  经典的 Fork 炸弹解析

本文参考资料

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

  一个fork的面试题

Unix/Linux中的fork函数的更多相关文章

  1. [fork]Linux中的fork函数详解

    ---------------------------------------------------------------------------------------------------- ...

  2. 深入解析Linux中的fork函数

    1.定义 #include <unistd.h> #include<sys/types.h> pid_t fork( void ); pid_t 是一个宏定义,其实质是int, ...

  3. linux中的fork()函数以及标准I/O缓冲

    1. fork()创建的新进程成为子进程.一次调用,两次返回,子进程的返回值是0,而父进程的返回值是新子进程的进程ID,如果出现错误,fork返回一个负值. 2. 可以通过fork返回的值来判断当前进 ...

  4. linux中的fork函数的基本用法

    代码: #include <iostream> #include <string> #include <cstdio> #include <unistd.h& ...

  5. Linux编程之fork函数

    在Linux中,fork函数的功能就是在一个进程中创建一个新的进程,当前调用fork函数的进程就是产生的新进程的父进程,新进程在以下也称为子进程.在新进程生成之后就会在系统中开始执行. 函数原型:pi ...

  6. [转]unix/linux中的dup()系统调用

    [转]unix/linux中的dup()系统调用    在linux纷繁复杂的内核代码中,sys_dup()的代码也许称得上是最简单的之一了,但是就是这么一个简单的系统调用,却成就了unix/linu ...

  7. 【Linux 进程】fork函数详解

    一.fork入门知识 一个进程,包括代码.数据和分配给进程的资源.fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同, ...

  8. 浅析linux中的fork、vfork和clone

    各种大神的混合,做个笔记. http://blog.sina.com.cn/s/blog_7598036901019fcg.html http://blog.csdn.net/kennyrose/ar ...

  9. Linux 环境下 fork 函数和 exec 函数族的使用

    前言 接触 Linux 已经有几个月了,以前在网上看各路大神均表示 Windows 是最烂的开发平台,我总是不以为然,但是经过这段时间琢磨,确实觉得 Linux 开发给我带来不少的便利.下面总结一下学 ...

随机推荐

  1. Dynamics CRM2016 Update or Create parentcustomerid in Contact using web api

    联系人实体中有个特殊的字段parentcustomerid 在通过web api创建或更新记录时,如果在给这个字段赋值时当做查找字段对待的话,那你就会遇到问题了,报错信息如下 正确的赋值方式如下

  2. Ubuntu14下安装svn仓库,以及权限配置

    sudo apt-get update 接下来安装svn apt-get install subversionapt-get install libapache2-svn 检查svn是否安装成功了: ...

  3. foxit pdf强制页面视图所有情况都为'合适宽度'

    在左边的书签点击时,有时明明已经设置为合适宽度,但foxit会自动给你变为'合适页面'.真是莫名其妙的设置.好在可以这样更改:

  4. Android中Snackbar的介绍以及使用

    Android中Snackbar的介绍以及使用 介绍 Snackbar可以说是Toast的升级版,不仅有显示信息的功能,还可以添加一个Action,实现点击功能,可以右滑删除. 效果图 Snackba ...

  5. 仿爱奇艺视频,腾讯视频,搜狐视频首页推荐位轮播图(二)之SuperIndicator源码分析

    转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼:http://blog.csdn.net/hejjunlin/article/details/52510431 背景:仿爱奇艺视频,腾讯视频 ...

  6. Android 在 SElinux下 如何获得对一个内核节点的访问权限

    点击打开链接 Android 5.0下,因为采取了SEAndroid/SElinux的安全机制,即使拥有root权限,或者对某内核节点设置为777的权限,仍然无法在JNI层访问. 本文将以用户自定义的 ...

  7. SQL Server 执行计划操作符详解(3)——计算标量(Compute Scalar)

    接上文:SQL Server 执行计划操作符详解(2)--串联(Concatenation ) 前言: 前面两篇文章介绍了关于串联(Concatenation)和断言(Assert)操作符,本文介绍第 ...

  8. SQL Server 索引维护(1)——如何获取索引使用情况

    前言: 在前面一文中,已经提到了三类常见的索引问题,那么问题来了,当系统出现这些问题时,该如何应对? 简单而言,需要分析现有系统的行为,然后针对性地对索引进行处理: 对于索引不足的情况:检查缺少索引的 ...

  9. Java集合-----java集合框架常见问题

    1什么是Java集合API Java集合框架API是用来表示和操作集合的统一框架,它包含接口.实现类.以及帮助程序员完成一些编程的算法. 简言之,API在上层完成以下几件事: ● 编程更加省力,提高城 ...

  10. java虚拟机参数设置 jvm参数设置

    java进程命令行使用方式如下: java [-options] class [args...] -options 表示虚拟机的启动参数, class为带有main()函数的java类的全名称 arg ...