精选文章 Fork函数的解析(一)

Fork函数的解析(一)

作者:安弦 时间: 2019-11-07 07:41:40
安弦 2019-11-07 07:41:40

Fork函数的运用:父进程通过调用Fork函数来创建一个新的子进程。

那么,什么是进程呢?进程的经典定义就是一个执行中程序的实例,即指程序的一次运行过程。程序是指按某种方式组合形成的代码和数据集合,代码即是机器指令序列,因而程序是一个静态的概念。不同于程序,进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态的含义。

1.同一个程序处理不同的数据就是不同的进程。

2.进程是操作系统对cpu执行的程序的运行过程的一种抽象。进程会有自己的生命周期,它由于任务的启动而创建,随着任务的完成或终止而消亡,它所占用的资源也随着进程的终止而释放。

3.一个可执行目标文件即程序可被加载执行多次,也即,一个程序可能对应多个不同的进程。

引入进程是为了给应用程序提供以下两方面的抽象:

1.一个独立的逻辑控制流。每个进程拥有一个独立的逻辑控制流,使得程序员以为自己的程序在执行过程中独占使用处理器。

2.一个私有的虚拟地址空间。每个进程拥有一个私有的虚拟地址空间,使得程序员以为自己的程序在执行过程中独占使用存储器。

进程的引入简化了程序员的编程以及语言处理系统的处理,即简化了编程、编译、链接、共享和加载等整个过程。

以上就是对进程的解释,下面回到Fork函数:

新创建的子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用Fork函数时,子进程可以读写父进程中打开的任何文件。父进程和子进程最大的区别在于他们有不同的PID(PID是指process  id 即进程标识符)

那如何分辨程序是在父进程还是子进程中进行呢?

Fork函数只被一次调用却会有两次返回:一次是在调用父进程中,一次是在新创建的子进程中。在父进程中,Fork函数返回子进程的PID(大于0的数);在子进程中,Fork函数返回0。

如果你是第一次学习Fork函数,可以通过画进程图来帮助理解。进程图是刻画程序语句的偏序的一种简单的前趋图。每个顶点a对应于一条程序语句的执行。有向边a->b表示语句a发生在语句b之前。边上可以标注一些信息,例如一个变量的当前值。对应于printf语句的顶点可以标记上printf的输出(这里输出的都是hello便没有标记)。每张图从一个顶点开始,对应于调用main的父进程。这个顶点没有入边并且只有一个出边。每个进程顶点序列结束于一个对应exit调用的顶点。这个顶点只有一条入边没有出边。

下面用两个程序及运行结果来理解Fork函数:

#include 
#include 
#include 

int main()
{
    int x = 1;

    if (fork() == 0)
        printf("p1: x=%d\n",++x);

    printf("p2: x=%d\n",--x);
    exit(0);

}

进程图如下:

Fork函数的解析(一)1

程序的运行结果如下:

Fork函数的解析(一)2

此时子进程的输出是:p1:x=2                                                                                                                                                                                              p2:x=1

父进程的输出是:p2:x=0

#include 
#include 
#include 

int main()
{
	int x = 3;
	if (fork() != 0)
		printf("x=%d\n",++x);
	printf("x=%d\n",--x);
	exit(0);
}

进程图如下:

Fork函数的解析(一)3

运行结果如下:

Fork函数的解析(一)4

注意到上图两次都是同样的代码,运行的结果却不一样,原因如下:父进程和子进程是并发独立进程,内核能够以任何方式交替执行他们的逻辑控制流中的指令。作为程序员,我们绝不能对不同进程中指令的交替执行做任何的假设。但是可以干预它们执行的顺序,可用sleep函数让父进程或子进程休眠一会或者用wait函数(下面会介绍)。

入手题目之前要先知道return与exit的区别

1. exit用于结束正在运行的整个程序,它将参数返回给OS,把控制权交给操作系统;而return 是退出当前函数,返回函数值,把控制权交给调用函数。
2. exit是系统调用级别,它表示一个进程的结束;而return 是语言级别的,它表示调用堆栈的返回。
3. 在main函数结束时,会隐式地调用exit函数,所以一般程序执行到main()结尾时,则结束主进程。exit将删除进程使用的内存空间,同时把错误信息返回给父进程。
4. void exit(int status); 一般status为0,表示正常退出,非0表示非正常退出。

此区别转载于下面的链接,更多详情可参考下面的链接:
链接:https://blog.csdn.net/firefly_2002/article/details/7960595

以下还有几个题目判断会有几个“hello”的输出:

#include 
#include 
#include 

int main()
{
	int i;
	for(i=0;i<2;i++)
		fork();
	printf("hello\n");
	exit(0);	
}

进程图如下:

Fork函数的解析(一)5

运行结果如下:

Fork函数的解析(一)6

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

#include 
#include 
#include 

void doit()
{
	if(fork() == 0)
	{
		fork();
		printf("hello\n");
		return;
	}
	return;
}

int main()
{
	doit();
	printf("hello\n");
	exit(0);	
}

进程图如下:

Fork函数的解析(一)7

运行结果如下:

Fork函数的解析(一)8

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

#include 
#include 
#include 

void doit()
{
	if(fork() == 0)
	{
		fork();
		printf("hello\n");
		exit(0);
	}
	return;
}

int main()
{
	doit();
	printf("hello\n");
	exit(0);	
}

进程图如下:

Fork函数的解析(一)9

运行结果如下:

Fork函数的解析(一)10

 

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

#include 
#include 
#include 

void doit()
{
	fork();
	fork();
	printf("hello\n");
	return;
}

int main()
{
	doit();
	printf("hello\n");
	exit(0);	
}

进程图如下:

Fork函数的解析(一)11

运行结果如下:

Fork函数的解析(一)12

 

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

void fork1()
{
    int x = 1;
    pid_t pid = fork();

    if (pid == 0) {
	printf("Child has x = %d\n", ++x);
    } 
    else {
	printf("Parent has x = %d\n", --x);
    }
    printf("Bye from process %d with x = %d\n", getpid(), x);
}

进程图如下:

Fork函数的解析(一)13

运行结果如下:

Fork函数的解析(一)14

getpid()函数的作用是返回调用进程的PID,getppid()函数的作用是返回它父进程的PID(创建调用进程的进程),在这个函数中能够很好地验证当fork()==0时,子进程会执行if里面的语句,用这些函数能够帮助我们理解在程序中父进程和子进程的运行过程。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

int main()
{
    printf("L0\n");
    if (fork() != 0) {  //父进程会执行里面的语句
	printf("L1\n");    
	if (fork() != 0) {  
	    printf("L2\n");
	}
    }
    printf("Bye\n");    //所有进程都会执行
	return 0;
}

进程图如下:

Fork函数的解析(一)15

运行结果如下:

Fork函数的解析(一)16

 

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

int main()
{
    printf("L0\n");
    if (fork() == 0) {   //与上面程序相反这只有子进程会执行
	printf("L1\n");    
	if (fork() == 0) {
	    printf("L2\n");
	}
    }
    printf("Bye\n");
	return 0;
}

进程图如下:

Fork函数的解析(一)17

运行结果如下:

Fork函数的解析(一)18

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

void cleanup(void) {
    printf("Cleaning up\n");
}
int main()
{
    atexit(cleanup); //atexit注册了cleanup函数
    fork();      
    exit(0);
}

 

运行结果如下:

Fork函数的解析(一)19

atexit()函数是用来调用终止函数的,在此程序中执行到atexit()函数时并不会立即输出cleanup,而是会先执行fork函数,然后进程与父进程都会输出clesnup语句(先注册后调用)。

更多的相关atexit()函数的信息可参考下面博客:

链接:https://blog.csdn.net/chan0311/article/details/71023454

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

int main()
{
     if (fork() == 0) {
	/* Child */
	printf("Terminating Child, PID = %d\n", getpid());
	exit(0);
    } else {
	printf("Running Parent, PID = %d\n", getpid());
	while (1)
	    ; /* Infinite loop */     //父进程陷入死循环
    }
}

 

运行部分结果如下(实际上这个程序不会终止,可用Ctrl+c强行终止,Ctrl+z将其挂起在后台运行):

Fork函数的解析(一)20

 

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

int main()
{
     if (fork() == 0) {
	/* Child */
	printf("Running Child, PID = %d\n",
	       getpid());
	while (1)
	    ; /* Infinite loop */    //子进程陷入死循环
    } else {
	printf("Terminating Parent, PID = %d\n",
	       getpid());
	exit(0);
    }
}

运行结果如下(这个程序会终止):

Fork函数的解析(一)21

用ps命令可以查看进程的状态

以上两个程序差异性的原因:父进程是shell main创建的,上面的程序,父进程在死循环中出不来故一直不能出现shell命令行;下面的程序中父进程结束自然回到shell命令行中。

 

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

int main()
{
    int child_status;

    if (fork() == 0) {
		printf("HC: hello from child\n");
        exit(0);
    }
	else {
		printf("HP: hello from parent\n");
		wait(&child_status);  //调用wait函数,等到子进程终止了就执行下一条语句
		printf("CT: child has terminated\n");  //这条语句一定是在HC的后面出现
    }
    printf("Bye\n");
}

 

运行结果如下:

Fork函数的解析(一)22

一个进程可以通过调用waitpid()函数来等待它的子进程终止或者停止。

pid_t waitpid(pid_t pid,int *statusp,int options); 当options=0时,waitpid会挂起调用进程的执行,直到它的等待集合中一个子进程终止,若等待集合中的一个子进程在刚被调用就已经终止了,该函数就会立即返回已终止进程的PID。此时,已终止进程已经被回收。其中,等待集合的成员是由第一个参数pid来确定的,若pid>0,则等待集合就是一个单独的子进程,他的进程ID等于pid;若pid=-1,那么等待集合就是由其父进程所有的子进程组成的。

pid_t wait(int *statusp);是waitpid函数的简化版本,调用wait(&status)等价于调用waitpid(-1,&statusp,0)

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

int main()
{
    pid_t pid[N];
    int i, child_status;

    for (i = 0; i < N; i++)
	if ((pid[i] = fork()) == 0) {
	    exit(100+i); /* Child */
	}
    for (i = 0; i < N; i++) { /* Parent */
	pid_t wpid = wait(&child_status);
	if (WIFEXITED(child_status))
	    printf("Child %d terminated with exit status %d\n",
		   wpid, WEXITSTATUS(child_status));
	else
	    printf("Child %d terminate abnormally\n", wpid);
    }
}

 

运行结果如下:

Fork函数的解析(一)23

程序使用了waitpid函数,不按照特定的顺序等他N个子进程终止,在第一个for循环里面,父进程创建N个子进程,里面的子进程都以唯一的退出状态退出。在后一个循环里面,父进程调用wait函数,在每个子进程终止的时候,对wait函数的调用都会返回。在N次循环里面对检查N个子进程退出状态,若子进程是调用exit函数终止的,那么父进程提取出退出状态并把它输到stdout(标准输出设备上)。调用wait函数相当于调用第一个参数为-1的waitpid函数,它是等待所有里子进程最早终止。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

int main()
{
    pid_t pid[N];
    int i;
    int child_status;

    for (i = 0; i < N; i++)
	if ((pid[i] = fork()) == 0)
	    exit(100+i); /* Child */   //非正常退出
    for (i = N-1; i >= 0; i--) {
	pid_t wpid = waitpid(pid[i], &child_status, 0);
	if (WIFEXITED(child_status))
	    printf("Child %d terminated with exit status %d\n",
		   wpid, WEXITSTATUS(child_status));
	else
	    printf("Child %d terminate abnormally\n", wpid);
    }
}

 

运行结果如下:

Fork函数的解析(一)24

这个程序与上面程序的不同在于waitpid参数不同,下面这个程序会等进程ID等于pid的子进程正常退出然后输出语句。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

下面是一道企业面试题:

int main(void)
{
   int i;
   for(i=0; i<2; i++){
      fork();
      printf("*");
   }
 
   return 0;
}

 

这一题若是简单的按照上面画进程图的步骤很容易得出会输出六颗星,但是结合fork函数创建子进程的过程及输出的概念,就能知道这道实际上会输出八颗星。

运行结果如下:

Fork函数的解析(一)25

printf若没有遇到\n(换行符)就会将输出存储在缓冲区,前面提到用fork函数创建子进程时,子进程能够得到父进程的代码和数据段、堆、共享库以及用户栈。所以把缓冲区里的数据也复制到子进程里,下面附图帮助理解:

要想要解决缓冲区这个问题 :                                                                                                                                                            1.加上换行符\n,换行符能够输出行冲区里的数据并刷新缓冲区;                                                                                                      2.使用fflush函数去清空缓冲区;

下面试验第二种方法:

int main(void)
{
   int i;
   for(i=0; i<2; i++){
      fork();
      printf("*");
      fflush(stdout);//此函数在stdio.h头文件里面,stdout是标准输出设备
   }
 
   return 0;
}

运行结果如下(只有六颗星):

Fork函数的解析(一)26

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

最后上一道稍难的题目:假设下面这个程序编译后名字是main,请问这个程序执行后,系统总共会出现多少个main进程?

pid_t Fork(void){  //为了便于观察结果,我将fork()封装为Fork()函数,每个进程都将输出一颗星
	pid_t pid;
	
	if((pid=fork())>0) printf("*\n");
	return pid;
}
int main(int argc,char *argv[])
{
    Fork();
	Fork()&&Fork()||Fork();
	//A&&B||c
	Fork();
}

 

运行结果(每颗星代表一个进程,共19颗星):

Fork函数的解析(一)27

弄明白这道题首先要先能理解A&&B||C:若A是true则接着看B,若B是true,则跳过C

                                                                 若A是true则接着看B,若B是false,则接着看C

                                                                 若A是false则跳过B,接着看C

这里所说的true或false分别对应题目中的父进程和子进程。

为了区分五个Fork函数,第一个和最后一个分别用1和5表示,中间三个用分别用A、B、C表示,父进程图如下(共19个Fork):

Fork函数的解析(一)28

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

水平有限,若有不足,欢迎指正!

 

 

勿删,copyright占位
分享文章到微博
分享文章到朋友圈

上一篇:头条不够,补次条!

下一篇:艾永亮:企业发展道路的下半场,如何用超级产品获得可持续增长

您可能感兴趣

  • 前端路 - Webpack

    概述 本质 JavaScript 应用程序的静态模块打包器 核心 加载器(Loader)机制 工作流程 配置初始化 webpack 会首先读取配置文件,执行默认配置 编译前准备 webpack 会实例化 compiler,注册 plugins、resolverFactory、hooks。 reslove 前准备 webpack 实例化 compilation、NormalModuleFact...

  • Java异常面试题(2020最新版)

    文章目录 Java异常架构与异常关键字 Java异常简介 Java异常架构 1. Throwable 2. Error(错误) 3. Exception(异常) 运行时异常 编译时异常 4. 受检异常与非受检异常 受检异常 非受检异常 Java异常关键字 Java异常处理 声明异常 抛出异常 捕获异常 如何选择异常类型 常见异常处理方式 直接抛出异常 封装异常再抛出 捕获异常 自定义异常 t...

  • SpringBoot 使用过滤器、拦截器、切面(AOP),及其之间的区别和执行顺序

    先上代码,下面的demo中包含多个拦截器、过滤器,以及切面的前置通知/后置通知/环绕通知: https://gitee.com/xiaorenwu_dashije/filter_interceptor.git 下面总结一下相关原理: 首先了解一下SpringMVC的执行流程 具体流程如下 用户发起请求到前端控制器(Controller) 前端控制器没有处理业务逻辑的能力,需要找到具体的模型对...

  • Webview.apk —— Google 官方的私有插件化方案

    简介: 在 Android 跨入 5.0 版本之后,我们在使用 Android 手机的过程中,可能会发现一个奇特的现象,就是手机里的 WebView 是可以在应用商店升级,而不需要跟随系统. 在 Android 跨入 5.0 版本之后,我们在使用 Android 手机的过程中,可能会发现一个奇特的现象,就是手机里的 WebView 是可以在应用商店升级,而不需要跟随系统的。 这一点在 iOS...

  • 前端实习生面试题分析(一)

    最近面试拿了很多公司的实习offer,只要是面试的都通过了。 接下来就分析下面试题,也能给自己一个提升吧,以便后续的面试更轻车熟路些,题目没什么顺序,想起什么写什么,还有我面试过程中的一些小套路。 估计要写好多,每天写几道题,而且有些经验性的东西也不容易写出来 这篇写的都是类似一些概念性的理论东西。 1.JS防抖和节流 先说为什么要做防抖和节流,针对一些会频繁触发的事件,像scroll、re...

  • 【计算机网络】第七章:网络安全

    【计算机网络】第七章:网络安全 目录 网络安全问题概述 1.1.计算机网络面临的安全性威胁 1.2.被动攻击和主动攻击 1.3.计算机网络通信安全的目标 1.4.恶意程序(rogue program) 1.5.计算机网络安全的内容 1.6.一般的数据加密模型 两类密码体制 一、对称密钥密码体制 1.1.对称密钥的缺点与优点 1.2.数据加密标准 DES 1.3.DES 的保密性 二、公钥密码...

  • 开启CAN通信学习(二)——基于Kvaser的CAN通信案例

    1 案例硬件介绍 Kvaser是瑞典的一家专门提供CAN和LIN总线分析仪及数据记录仪的公司,在CAN产品开发领域已经有近30年的经验,本案例选择的CAN通信硬件型号是Kvaser Leaf Light v2,产品如下图所示: 【公众号dotNet工控上位机:thinger_swj】 为了配套该产品,还准备了一个威柏电子(Westpac)提供的模拟控制器RL78 CAN ECU来进行功能测试...

  • Yolo系列详解

    零、图像基本概念 图像表示为二维的矩阵  灰阶图像 0-255,0表示黑色,255表示白色,其余表示灰色。 图像的坐标轴   彩色图像  注意:颜色信息对于任务有时候有用,有时候没用。 一、什么是目标检测 目标检测是计算机视觉中的经典的原子问题,即通过输入的图像来完成物体的检测,它需要解决两个问题: 物体在哪里 物体是什么 目标检测算法的传统实现 sift hog 等算法。这些算法的...

华为云40多款云服务产品0元试用活动

免费套餐,马上领取!
CSDN

CSDN

中国开发者社区CSDN (Chinese Software Developer Network) 创立于1999年,致力为中国开发者提供知识传播、在线学习、职业发展等全生命周期服务。