CSAPP(异常控制流)——fork()函数详解(附家庭作业解析)


    本博客的内容和代码是基于《深入理解计算机系统》的内容,用到了卡内基·梅隆大学关于此课程的一些代码以及csapp.h(一堆头文件的打包),在http://csapp.cs.cmu.edu/public/code.html 这里可以下载。

    在linux下,csapp.h和csapp.c要自己导入,放到 /usr/include的文件夹里面,并编辑csapp.h,在#end if前面加上一句#include <csapp.c>,因为那个头文件要把csapp.c文件包含进去 。
    因为csapp.c中有关于线程的头文件,在用gcc的时候最后要加上-lpthread

一.fork基础概念

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

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

二.进程的概念

附百度百科:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

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

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

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

三.辅助方法——进程图

接下来我给的例子我都会借助进程图来辅助说明:
进程图是刻画程序语句的偏序的一种简单的前趋图。每个顶点a对应于一条程序语句的执行。有向边a->b表示语句a发生在语句b之前。边上可以标注一些信息,例如一个变量的当前值。对应于printf语句的顶点可以标记上printf的输出(这里输出的都是hello便没有标记)。每张图从一个顶点开始,对应于调用main的父进程。这个顶点没有入边并且只有一个出边。每个进程顶点序列结束于一个对应exit调用的顶点。这个顶点只有一条入边没有出边。

四.两个简单的fork例子

例子1:书本p514——fork.c

#include "csapp.h"

/* $begin fork */
/* $begin wasidefork */
int main(int argc, char *argv[]) 
{
    pid_t pid;
    int x = 1;

    pid = Fork(); //line:ecf:forkreturn
    if (pid == 0) {  /* Child */
	printf("child : x=%d\n", ++x); //line:ecf:childprint
	fflush(stdout);
	return 0;
    }

    /* Parent */
    printf("parent: x=%d\n", --x); //line:ecf:parentprint
    fflush(stdout);
    return 0;
}
/* $end fork */
/* $end wasidefork */


运行命令:

gcc -o runfork fork.c csapp.c -lpthread
./runfork

书本514页的fork.c函数
进程图如下:
在这里插入图片描述
所以输出结果是:

parent: x=0
child : x=2

例子2:书本p551 8.13——forkprob3.c

#include "csapp.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    int x = 3;

    if (Fork() != 0)
	printf("x=%d\n", ++x);

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

运行命令:
gcc -o forkprob3 forkprob3.c csapp.c -lpthread

进程图:
forkprob3
结果:
在这里插入图片描述
根据父子进程并发运行的关系,再结合进程图,此程序还有一种结果(但是我一直没有跑出来,借用“安弦”的博客效果图)
在这里插入图片描述
对于有两个结果的原因:
    1.并发:交替运行
    2.并行:同一时刻,两个或多个都在执行
fork函数产生的子进程和父进程是并发运行的,内核能够以任何方式交替执行它们的逻辑控制流中的指令。我们不能对不同进程中指令的交替执行做任何的假设。但是可以干预它们执行的顺序,可用sleep函数让父进程或子进程休眠一会或者用wait函数

其它fork例子

  1. 8.11 forkprob1.c
    这个程序会输出多少个"hello"输出行?
#include "csapp.h"
int main()
{
	int i;
	
	for(i=0;i<2;++i)
		Fork();
	printf("hello\n");
	exit(0);
}

运行命令:

 gcc -o forkprob1 forkprob1.c csapp.c -lpthread

在这里插入图片描述
进程图:
在这里插入图片描述
2. 8.12 forkprob4.c

#include "csapp.h"
void doit()
{
    Fork();
    Fork();
    printf("hello\n");
    return;
}

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

运行命令:

gcc -o forkprob4 forkprob4.c csapp.c -lpthread

结果:
在这里插入图片描述
共有八个hello输出
进程图:
在这里插入图片描述
入手题目之前要先知道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
3. 8.14 forkprob5.c

/* $begin forkprob5 */
#include "csapp.h"

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

int main() 
{
    doit();
    printf("hello\n");
    exit(0);
}
/* $end forkprob5 */

运行命令:

gcc -o forkprob5 forkprob5.c csapp.c -lpthread

结果:
在这里插入图片描述
进程图:
在这里插入图片描述
4. 8.15 forkprob6.c

/* $begin forkprob6 */
#include "csapp.h"

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

int main() 
{
    doit();
    printf("hello\n");
    exit(0);
}
/* $end forkprob6 */

运行命令:

gcc -o forkprob6 forkprob6.c csapp.c -lpthread

结果:
在这里插入图片描述

进程图:在这里插入图片描述

5 8.16 forkprob7.c

/* $begin forkprob7 */
#include "csapp.h"
int counter = 1;

int main() 
{
    if (fork() == 0) {
	counter--;  
	exit(0);
    }
    else {
	Wait(NULL); 
	printf("counter = %d\n", ++counter);
    }
    exit(0);
}
/* $end forkprob7 */

运行命令

gcc -o forkprob7 forkprob7.c csapp.c -lpthread

结果:
在这里插入图片描述
进程图:
在这里插入图片描述
这里涉及到回收进程的问题:
引用:CSDN博主「马怡青」的原创文章
1).waitpid(pid_t pid,int *statusp,int options)

pid:
 pid>0时只等待进程ID为pid的子进程结束

 pid=-1时等待其所有的子进程中的任何一个(只要一个)结束

options:
 options=0(默认情况)时,挂起父进程,等待其子进程结束。返回子进程编号。

 options=WNOHANG时,父进程不挂起。如果一个子进程都没有结束的话,返回0;否则返回子进程编号。

如果调用函数的进程没有子进程,waitpid返回-1,errno设为ECHILD

2).wait(&status)

等价于waitpid(-1,&status,0)

3).WIFEXITED(status):

如果子进程是以exit或者return正常退出的,函数返回值就为true

4).WEXITSTATUS(status)

前提是WIFEXITED一定为true,此函数返回正常终止的子进程的退出状态,即exit的值

5)atexit()

在进程结束调用exit时,调用atexit()括号中的注册函数,注册几次就调用几次。并且它的调用顺序和登记顺序是相反的。与压栈顺序有关。
  1. 8.18 forkprob2.c 考虑下面程序可能有的输出
/* $begin forkprob2 */
#include "csapp.h"

void end(void) 
{
    printf("2"); fflush(stdout);
}

int main() 
{
    if (Fork() == 0) 
	atexit(end);
    if (Fork() == 0){
	printf("0");fflush(stdout);
}
    else{
	printf("1");fflush(stdout);
}
    exit(0);
}
/* $end forkprob2 */

结果:
在这里插入图片描述
可能输出就是拓扑排序,所以有上图给出的可能(但不止)
101202,112021
进程图:
在这里插入图片描述
(此进程图的输出应该实在fflush之后,故图中有错误)

7 关于atexit()函数

#include "csapp.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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

结果:
在这里插入图片描述
正如前面所说,atexit()函数是用来调用终止函数的,在此程序中执行到atexit()函数时并不会立即输出cleanup,因为这里还没有终止函数,所以会执行下一条语句,(fork函数),然后fork子进程与父进程都会输出clesnup语句(先注册后调用)。

再看一个atexit,理解一下先注册后调用

#include "csapp.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void func1(){
    printf("fun1\n");
}
void func2(){
    printf("fun2\n");
}
void func3(){
    printf("fun3\n");
}
void func4(){
    printf("fun4\n");
}
void func5(){
    printf("fun5\n");
}

int main(int argc,char *argv){
    atexit(func1);
    atexit(func2);
    atexit(func3);
    atexit(func4);
    atexit(func5);

    sleep(2);
    printf("main\n");
    return 0;
}

运行命令:

gcc -o cm_atexit cm_atexit.c -lpthread
./cm_atexit

结果:
在这里插入图片描述
当执行到return 0 时,exit会自动调用这些已注册过的函数,但是由于压栈过程中先入后出的原则,所以先注册的函数最后执行。

#include "csapp.h"
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 */     //父进程陷入死循环
    }
}

运行命令:

gcc -o fork9 fork9.c csapp.c -lpthread
./fork9

结果:
在这里插入图片描述
###当执行到while(1)时,程序陷入死循环,按Ctrl+z(挂起)或Ctrl+c(强行终止)
###可以用ps查看当前进程
在这里插入图片描述

10

#include "csapp.h"
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);
    }
}

这个函数与上面不同的点在于while(1)的位置不同,这里的父进程是在shell main创建的,父进程结束自然回到shell命令行中。上面的程序,父进程在死循环中出不来故一直不能出现shell命令行!
结果:
在这里插入图片描述

11.一道有关fork和缓冲区的企业面试题
1)代码一:

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

由前面给的代码,你可能认为这里还是输出6可*,其实不然,这里的输出语句,没有\n,printf若没有遇到\n(换行符),就会将输出存储在缓冲区,并没有实际的写到屏幕上,前面提到用fork函数创建子进程时,子进程能够得到父进程的代码和数据段、堆、共享库以及用户栈。所以把缓冲区里的数据也复制到子进程里
结果:
在这里插入图片描述

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

12.不算main自身这进程,这个程序到底创建了多少个进程

int main(int argc,char *argv[])
{
    Fork();
	Fork()&&Fork()||Fork();
	Fork();
}

先加一些辅助性的输出,跑一遍,看结果:

#include "csapp.h"
int main(int argc,char *argv[])
{
    Fork();
	Fork()&&Fork()||Fork();
	Fork();
    printf("*\n");
}

结果:在这里插入图片描述
加上main自身输出的*,一共有20个*,所以除去main本身,共产生了19个进程

分析:
关键之处在于Fork()&&Fork()||Fork();这个语句。
对于//A&&B||c这个格式的语句,有以下几种执行情况
对于&&连接的两个值,若前面为假,表达式的结果就为假,则不判断后面的真假情况,
对于||连接的两个值,若前面为真,则表达式就为真,则不判断后面的真假情况
所以有:
1)A为假时,B不执行,C执行
2)A为真,B为假,执行C
2)A为真,B为真,不执行C

进程图:

在这里插入图片描述

参考链接:
https://www.cnblogs.com/love-jelly-pig/p/8471206.html
https://blog.csdn.net/weixin_43329358/article/details/102932891
https://blog.csdn.net/weixin_44688476/article/details/102868173

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值