实验代码之fork

关于fork实例题目,从以下第一个开始

一:

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);
}

可以很清楚的画出其进程图如下:
在这里插入图片描述
运行结果如下所示:

zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 1
Parent has x = 0
Bye from process 18239 with x = 0
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ Child has x = 2
Bye from process 18240 with x = 2

其中process 18239和18240就分别是父进程和子进程的id,这是一目了然的,但是另一个问题是,为什么一次运行也就是./fork之后,它还会有一次shell命令行出现?而且是自动输出。

  1. 这个问题留待日后

二:

void fork2()
{
    printf("L0\n");
    fork();
    printf("L1\n");    
    fork();
    printf("Bye\n");
}

这一个代码块我运行了两次,发现一个有趣的现象,运行结果如下:

zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 2
L0
L1
Bye
Bye
L1
Bye
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ Bye
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 2
L0
L1
Bye
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ Bye
L1
Bye
Bye

很显然,这两次的输出顺序并不完全一致,我们再来看一下它的流程图:
在这里插入图片描述

其实从这张图以及这两个运行结果,完全可以猜测出上一个题目最后所留下的那个疑问,为什么一次运行之后会自动进行一次shell命令行输出?

原理暂时还不太清楚,但是可以根据以上进行猜想,自动输出shell命令行的条件是,父进程结束。为什么是这样?

我们看上面两次结果对比,根据流程图也可知道,第一个输出肯定是L0,这显然是毫无疑问的,但是接下来输出的L1呢?它其实既有可能是父进程输出的L1,也可能是子进程输出的L1,这也就是我们所说的关于fork父子进程执行顺序不可确定性。这样理解之后其实就很好解决两次输出不一致以及命令行出现位置的问题了。显然第一次运行结果中新的命令行之前的L1和Bye就是父进程中的L1和Bye,而第二次运行时最开始三次输出包括L0在内全市父进程的输出结果。至于为什么会有这样洋溢个新的命令行机制的出现以及实现,这个问题以后了解之后再细说。

三:

void fork4()
{
    printf("L0\n");
    if (fork() != 0) {
	printf("L1\n");    
	if (fork() != 0) {
	    printf("L2\n");
	}
    }
    printf("Bye\n");
}
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 4
L0
L1
L2
Bye
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ Bye
Bye

看一下进程图:
在这里插入图片描述这道题主要是限制条件对fork函数的常规使用,可以看到,因为源代码中if语句的使用,将所有子进程的输出语句都限制在了一句。

四:

void cleanup(void) {
    printf("Cleaning up\n");
}
void fork6()
{
    atexit(cleanup);
    fork();
    exit(0);
}
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 6
Cleaning up
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ Cleaning up

这个代码块是强调atexit()系统函数的相关作用,在查看相关博客看到这样一段话:

atexit函数是一个特殊的函数,它是在正常程序退出时调用的函数,我们把他叫为登记函数(函数原型:int atexit (void (*)(void))):
⼀个进程可以登记若⼲个(具体⾃⼰验证⼀下)个函数,这些函数由exit⾃动调⽤,这些函数被称为终⽌处理函数, atexit函数可以登记这些函数。 exit调⽤终⽌处理函数的顺序和atexit登记的顺序相反(网上很多说造成顺序相反的原因是参数压栈造成的,参数的压栈是先进后出,和函数的栈帧相同),如果⼀个函数被多次登记,也会被多次调⽤。
————————————————
版权声明:本文为CSDN博主「ShawnLeex」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/leex_brave/article/details/51813962

exit()函数在结束进程时会调用终止处理函数,也就是atexit()函数中所记录的登记函数,而且调用这些函数的顺序还是逆序的,详情请参照上段引用的原文。

五:

void fork7()
{
    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 */
    }
}

这段源代码中在父进程最后加了一句while(1)循环,这意味着父进程永远不会结束,而子进程却会经过exit函数结束进程,我们来看看运行结果怎样。

zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 7
Running Parent, PID = 7388
Terminating Child, PID = 7389

当父进程和子进程的输出语句全部出现时,该程序段还未曾结束,这里根据其父子进程进行的顺序也可佐证上面所猜想的新命令行在父进程结束之后才会出现。

此时因为程序未曾结束,按下Ctrl+z,会发生有趣的事情:

^Z
[1]+  已停止               ./fork 7
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ps
   PID TTY          TIME CMD
  6390 pts/0    00:00:00 bash
  7388 pts/0    00:01:53 fork
  7389 pts/0    00:00:00 fork <defunct>
  7429 pts/0    00:00:00 ps
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ kill -9 7388
[1]+  已杀死               ./fork 7
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ps
   PID TTY          TIME CMD
  6390 pts/0    00:00:00 bash
  7625 pts/0    00:00:00 ps

程序在此刻停止运行,但是通过ps命令我们发现,该父进程和子进程都还占据着空间!

我们知道,父进程在结束的时候会将已经结束的子进程杀死并回收空间,如果父进程没有结束,那就意味着该父进程的进程组中的进程都会处于“僵死”状态,这对于内存空间是极大的危害!此时只能使用kill函数发送信号将其父进程为名的整个进程组都杀死回收。

六:

void fork8()
{
    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);
    }
}

上一个题目是父进程一直在循环没有结束,这一个题目就是子进程一直循环的情况。先来看一下运行结果:

zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 8
Terminating Parent, PID = 7917
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ Running Child, PID = 7918
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ps
   PID TTY          TIME CMD
  6390 pts/0    00:00:00 bash
  7918 pts/0    00:00:36 fork
  7930 pts/0    00:00:00 ps

这个程序运行有一个新问题,因为是父进程结束而子进程未结束,而在终端窗口中却显示和全都结束一样的结果(这个问题亟待解决),即此时如果不用ps命令查看进程情况,我们是发现不了子进程未被回收的状况的,这就很容易导致子进程“僵死”状态占据内存损害内存!

这种情况非常危险!一般的解决方法是通过在父进程结束语句之前加上一条wait()指令等待子进程全都结束之后再结束,这样就能使得程序员更加警醒,因为一旦父进程都没有结束的话那就意味着程序出现了一些很危险的问题。

来看增加wait()之后的程序:

void fork9()
{
    int child_status;

    if (fork() == 0) {
	printf("HC: hello from child\n");
        exit(0);
    } else {
	printf("HP: hello from parent\n");
	wait(&child_status);
	printf("CT: child has terminated\n");
    }
    printf("Bye\n");
}

运行结果如下:

zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 9
HP: hello from parent
HC: hello from child
CT: child has terminated
Bye

七:

void fork10()
{
    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);
    }
}
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 10
Child 8676 terminated with exit status 104
Child 8675 terminated with exit status 103
Child 8674 terminated with exit status 102
Child 8673 terminated with exit status 101
Child 8672 terminated with exit status 100

这个程序使用了WIFEXITED()等相关宏定义用于判定子进程的结束状态,因为子进程都是通过exit()正常结束,所以在WIFEXITED()返回值为true的情况下可以再次进行WEXITSTATUS()调用,则可以返回其子进程退出时的状态,也就是exit()函数所带的参数设置。

比较一下三个程序的差异:

void fork11()
{
    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);
    }
}
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 11
Child 9069 terminated with exit status 104
Child 9068 terminated with exit status 103
Child 9067 terminated with exit status 102
Child 9066 terminated with exit status 101
Child 9065 terminated with exit status 100
void fork12()
{
    pid_t pid[N];
    int i;
    int child_status;

    for (i = 0; i < N; i++)
	if ((pid[i] = fork()) == 0) {
	    /* Child: Infinite Loop */
	    while(1)
		;
	}
    for (i = 0; i < N; i++) {
	printf("Killing process %d\n", pid[i]);
	kill(pid[i], SIGINT);
    }

    for (i = 0; i < N; i++) {
	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 terminated abnormally\n", wpid);
    }
}
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 12
Killing process 9111
Killing process 9112
Killing process 9113
Killing process 9114
Killing process 9115
Child 9115 terminated abnormally
Child 9114 terminated abnormally
Child 9113 terminated abnormally
Child 9112 terminated abnormally
Child 9111 terminated abnormally

fork11是使用更为复杂的waitpid函数,实际上调用wait(&status)等价于调用waitpid(-1,&status,0), waitpid()在实际效用上可以让父进程等待任意一个子进程或者所有子进程结束,还可以通过设置参数options实现更为复杂的功能,这里不多说。至于for循环中i从0开始和从N-1开始,其实并无一点影响,都只是作为一个循环大小来看的,其实际顺序还是按照子进程创建的顺序运行的。

而在fork12中,是将子进程的exit()结束语句全部变成了一个while(1)完全循环语句,这就是将子进程永久挂起在那儿,此时在父进程中WIFEXITED()宏定义返回的值就是false,因此并不会进入该If语句中,从而输出abnormally这一个异常提示。kill(pid[i], SIGINT)语句是父进程调用kill函数给pid[i]的子进程传递信号SIGINT,表示终止子进程进行。

接下来这个代码块使用了一条 signal(SIGINT, int_handler)这样的语句,又更进一步解决了fork12的问题:

void int_handler(int sig)
{
    printf("Process %d received signal %d\n", getpid(), sig); /* Unsafe */
    exit(0);
}
void fork13()
{
    pid_t pid[N];
    int i;
    int child_status;

    signal(SIGINT, int_handler);
    for (i = 0; i < N; i++)
	if ((pid[i] = fork()) == 0) {
	    /* Child: Infinite Loop */
	    while(1);
	}

    for (i = 0; i < N; i++) {
	printf("Killing process %d\n", pid[i]);
	kill(pid[i], SIGINT);
    }

    for (i = 0; i < N; i++) {
	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 terminated abnormally\n", wpid);
    }
}
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 13
Killing process 9508
Killing process 9509
Killing process 9510
Killing process 9511
Killing process 9512
Process 9512 received signal 2
Child 9512 terminated with exit status 0
Process 9511 received signal 2
Child 9511 terminated with exit status 0
Process 9510 received signal 2
Child 9510 terminated with exit status 0
Process 9509 received signal 2
Child 9509 terminated with exit status 0
Process 9508 received signal 2
Child 9508 terminated with exit status 0

其中 signal(SIGINT, int_handler)语句的作用是修改默认设置,通过设置信号处理函数int_handler,接受到信号SIGINT时就自动调用该信号处理函数,所以党在for循环中调用kill函数传递SIGINT信号给子进程时,就转而执行自定义的 int_handler()函数,于是就输出了Process 9512 received signal 2语句,并且在此后使用exit(0)结束了子进程,状态恢复正常。

八:

int ccount = 0;
void child_handler(int sig)
{
    int child_status;
    pid_t pid = wait(&child_status);
    ccount--;
    printf("Received SIGCHLD signal %d for process %d\n", sig, pid); /* Unsafe */
    fflush(stdout); /* Unsafe */
}
void fork14()
{
    pid_t pid[N];
    int i;
    ccount = N;
    signal(SIGCHLD, child_handler);

    for (i = 0; i < N; i++) {
	if ((pid[i] = fork()) == 0) {
	    sleep(1);
	    exit(0);  /* Child: Exit */
	}
    }
    while (ccount > 0)
	;
}
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 14
Received SIGCHLD signal 17 for process 2659
^Z
[1]+  已停止               ./fork 14
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ps
   PID TTY          TIME CMD
  2635 pts/0    00:00:00 bash
  2658 pts/0    00:00:01 fork
  2660 pts/0    00:00:00 fork <defunct>
  2661 pts/0    00:00:00 fork <defunct>
  2662 pts/0    00:00:00 fork <defunct>
  2663 pts/0    00:00:00 fork <defunct>
  2664 pts/0    00:00:00 ps
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ kill -9 2658
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ps
   PID TTY          TIME CMD
  2635 pts/0    00:00:00 bash
  2666 pts/0    00:00:00 ps
[1]+  已杀死               ./fork 14

当子进程终止或结束时会给父进程发送一个编号为17的SIGCHLD信号,而signal(SIGCHLD, child_handler)这条语句就将SIGCHLD信号的信号处理函数修改为了child_handler()函数,当子进程休眠之后再结束时就会发送信号给父进程,紧接着父进程调用信号处理函数,显示输出。但是这段代码问题就在于,在子进程结束之前使用了一个sleep()函数。

sleep函数的功能是将进程挂起一段时间,也就是“休眠”,在linux系统中其时间是以秒/s为单位的,在这样的情形下,这段代码执行过程中子进程就没有及时结束,导致父进程也一直在循环状态下,此时占用了极大的内存消耗!如果在实际操作中其sleep函数的参数是一个更大的数字,那么对于系统内存来说,同一时刻消耗过多的容量就会使得当前其它进程收到更大的影响,这一点在虚拟内存那一块尤其明显。

接下来我们看看如何解决这个问题:

void child_handler2(int sig)
{
    int child_status;
    pid_t pid;
    while ((pid = wait(&child_status)) > 0) {
	ccount--;
	printf("Received signal %d from process %d\n", sig, pid); /* Unsafe */
	fflush(stdout); /* Unsafe */
    }
}
void fork15()
{
    pid_t pid[N];
    int i;
    ccount = N;

    signal(SIGCHLD, child_handler2);

    for (i = 0; i < N; i++)
	if ((pid[i] = fork()) == 0) {
	    sleep(1);
	    exit(0); /* Child: Exit */

	}
    while (ccount > 0) {
	pause();
    }
}
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 15
Received signal 17 from process 3165
Received signal 17 from process 3164
Received signal 17 from process 3163
Received signal 17 from process 3162
Received signal 17 from process 3161

九:

void fork16() 
{
    if (fork() == 0) {
	printf("Child1: pid=%d pgrp=%d\n",
	       getpid(), getpgrp());
	if (fork() == 0)
	    printf("Child2: pid=%d pgrp=%d\n",
		   getpid(), getpgrp());
	while(1);
    }
} 
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 16
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ Child1: pid=9905 pgrp=9904
Child2: pid=9906 pgrp=9904
void fork17() 
{
    if (fork() == 0) {
	printf("Child: pid=%d pgrp=%d\n",
	       getpid(), getpgrp());
    }
    else {
	printf("Parent: pid=%d pgrp=%d\n",
	       getpid(), getpgrp());
    }
    while(1);
} 
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 17
Parent: pid=9953 pgrp=9953
Child: pid=9954 pgrp=9953
^Z
[1]+  已停止               ./fork 17
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ps
   PID TTY          TIME CMD
  6390 pts/0    00:00:00 bash
  9953 pts/0    00:00:01 fork
  9954 pts/0    00:00:01 fork
  9961 pts/0    00:00:00 ps
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ kill -9 9953
[1]+  已杀死               ./fork 17
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ps
   PID TTY          TIME CMD
  6390 pts/0    00:00:00 bash
  9974 pts/0    00:00:00 ps

这两段程序都是讲述进程组的问题,当子进程被创建时会自动将其分配至以其父进程为名的进程组名下,此时只要kill进程组id即可将进程组中所有子进程都删除回收。

十:

void fork18() 
{
    printf("*");
    fork();
    printf("*");
    exit(0);
} 
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 18
**zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ **

这一段代码是怎么回事呢?我们知道,如果按照常规思路理解,先进行一次打印输出语句,出现一个符号,在进行fork之后,父进程和子进程分别再输出一个,总共输出三次*,但是在这段输出中,我们显而易见,父进程和子进程都输出了两个符号,与我们所猜想的不符。

我们再来看另一段代码:

void fork19() 
{
    printf("*\n");
    fork();
    printf("*\n");
    exit(0);
} 
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ ./fork 19
*
*
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap8_code$ *

这段代码中又只输出了三个符号,而且显而易见子进程只有一个符号输出,这是为什么?

我们看这两段代码的区别在哪儿。

后面代码在输出符号之后增加了换行符号,这个换行符号起到的效果却不仅仅是换行那么简单。从我们的数据输出到终端显示,这个过程中首先要将数据读到缓冲区部分,再从缓冲区将数据打印到终端显示,这是两个过程的事情。我们知道,fork会创建一个子进程,子进程拥有和父进程一样的数据,而其代码区域更是共享的,其中,子进程的缓冲区也会完全复制父进程的缓冲区,当我们不使用\n换行符时,缓冲区中的内容输出在显示中,但是并没有就此而清除。因为标准输出是行缓冲的,当使用\n的时候就会强制性将上一行数据全部输出并且删除,以预留位置给下一行数据的读入,所以必须要使用\n或者fflush(stdout)清除缓冲区的数据,否则在创建子进程的时候就会将缓冲区数据一起复制过去,造成输出错误。

这段fork到此结束。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值