CSAPP的18个fork的前12个fork

一. Examples

首先,简单介绍一下forks()函数

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

一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

源代码

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> 
#include <signal.h>

	.....(省略18个forks)

int main(int argc, char *argv[])
{
    int option = 0;
    if (argc > 1)
	option = atoi(argv[1]);
    switch(option) {
    case 0: fork0();
	break;
    case 1: fork1();
	break;
    case 2: fork2();
	break;
    case 3: fork3();
	break;
    case 4: fork4();
	break;
    case 5: fork5();
	break;
    case 6: fork6();
	break;
    case 7: fork7();
	break;
    case 8: fork8();
	break;
    case 9: fork9();
	break;
    case 10: fork10();
	break;
    case 11: fork11();
	break;
    case 12: fork12();
	break;
    case 13: fork13();
	break;
    case 14: fork14();
	break;
    case 15: fork15();
	break;
    case 16: fork16();
	break;
    case 17: fork17();
	break;
    default:
	printf("Unknown option %d\n", option);
	break;
    }
    return 0;
}

那么接下来,就由易到难用实例来体会一下fork().

0. The simplest fork example

void fork0() 
{
    if (fork() == 0) {
	printf("Hello from child\n");
    }
    else {
	printf("Hello from parent\n");
    }
}

这一个算是很简单的了,开始先调用fork(),那么就创建了一个子进程。
if (fork() == 0)即如果当前进程为子进程,那么就printf("Hello from child\n");,如果不是,即为父进程,那么就printf("Hello from parent\n");

在这里补充说明一个东西,

fork调用的一个有趣又令人迷惑的一点就是它只被调用一次,却能够返回两次。一次在父进程中,而另一次在新创建的子进程中

它可能有三种不同的返回值:
(1)在父进程中,fork返回新创建子进程的进程ID;
(2)在子进程中,fork返回0;
(3)如果出现错误,fork返回一个负值;

所以但我们在虚拟机中运行该代码,即在命令行中输入:gcc -o forks.o foks.c以及 ./forks.o 0
那么就可能会会得到如下结果:
Hello from parent
Hello from child

该运行结果的输出顺序每一次都不一定相同,可能是子进程先输出,也可能是父进程先输出,两个进程是并发运行的。(这一点非常重要!!!)

分析如下:
在这里插入图片描述

1. Simple fork example

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

运行结果如下:
Parent has x = 0
Bye from process 10317 with x = 0
Child has x = 2
Bye from process 10318 with x = 2

分析如下:
在这里插入图片描述

  1. 首先pid_t pid = fork();是保存进程的进程号,如果为0,说明当前运行的是子进程,而如果>0说明当前运行的是父进程。因为在父进程中,fork返回子进程的PID(进程号)。在子进程中,fork返回0。而这一点也便于我们判断当前正在运行的进程是父进程还是子进程
  2. 特别值得注意的一点是,子进程虽然说会继承父进程的所有的数据和指令,但是他们两个终究是拥有各自的数据和指令,所以两个进程对x的操作是不会相互影响的,故最后输出的结果,子进程输出为Child has x = 2,父进程输出为Parent has x = 0
  3. 同时,运行所输出的顺序虽然每次都未必相同,但仍然是按照本人手绘的分析图的箭头方向输出的。也就是说,虽然每次输出未必相同,但有些输出顺序是完全不可能的。
    例如:
    Bye from process 10318 with x = 2
    Parent has x = 0
    Bye from process 10317 with x = 0
    Child has x = 2
    该输出顺序就是不可能的

2. Two consecutive forks

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

运行结果如下:
L0
L1
L1
Bye
Bye
Bye
Bye

与上一程序类似,每次运行的顺序也不尽相同,但是也会有一定的输出可能。

分析如下:
在这里插入图片描述
即运行结果的输出一定是按箭头方向的顺序。
例如:L0 Bye Bye Bye Bye L1 L1 ,这样顺序的输出就是不可能的。

3. Three consective forks

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

该程序和上一程序的区别…基本没啥区别,具体的分析就不再赘述了。

运行结果:
L1
L2
L1
L2
Bye
L2
Bye
Bye
Bye
L2
Bye
Bye
Bye
Bye

这一代码不过是相比较上一程序多了一个fork(),…直接看分析吧
在这里插入图片描述

4. Nested forks in parents

void fork4()
{
    printf("L0\n");
    if (fork() != 0) {		//如果当前进程为父进程
		printf("L1\n");    
		if (fork() != 0) {		//如果当前进程为父进程
			printf("L2\n");
		}
    }
    printf("Bye\n");
}

运行结果:
L0
L1
Bye
L2
Bye
Bye

这一部分代码穿插较多的if-else,就可能显得这一段代码会复杂些,但只要仔细分析,也不会很难。这里再次说明,

  1. fork被调用一次,但是会返回两次,一次是在调用进程(即父进程)中,而另一次是在新创建的子进程中,两个进程是并发运行的。
  2. 如果fork的返回值大于0,说明当前进程是父进程,若fork的返回值为0,则当前进程为子进程。

那么我们既可以根据这两点,来完成代码中的if-else的判断,判断哪些指令是父进程会做的,而哪些是子进程会做的。

既然如此我就直接给出分析吧。
在这里插入图片描述
(…这个图好像多了个箭头,忽略它吧)

对于这一段代码,那么只要分析清除哪些是子进程会运行的,而哪些是父进程运行的,那么就可以画出类似的这种分析图,也就能够快速的写出该程序的某一种运行结果。

5. Nested forks in children

void fork5()
{
    printf("L0\n");
    if (fork() == 0) {		//如果当前进程为子进程
		printf("L1\n");    
		if (fork() == 0) {		//如果当前进程为子进程
			printf("L2\n");
		}
    }
    printf("Bye\n");
}

这一段代码,相比较fork4(),只是if-else当中的判断条件有些许的不同。这里就不再做分析了。
运行结果:
L0
Bye
L1
Bye
L2
Bye

6. Exit system call terminates process

void cleanup(void) {
    printf("Cleaning up\n");
}

void fork6()
{
    atexit(cleanup);
    fork();
    exit(0);
}

这里增加了一个cleanup()函数,作用仅为输出一段英文。
其中,atexit(cleanup);表达的是当程序终止是调用cleanup()函数。

C 库函数 int atexit(void (*func)(void)) 当程序正常终止时,调用指定的函数 func。您可以在任何地方注册你的终止函数,但它会在程序终止的时候被调用。

代码中调用了一次fork(),也就是说只新创建了一个子进程,故当这两个进程都结束时,最后有两个输出。
运行结果显然如下:
Cleaning up
Cleaning up

分析如下:
在这里插入图片描述
这个图大致能够体现是在什么时候调用了cleanup()函数

7. Demonstration of zombies.

注:Run in background and then perform ps(在虚拟机中运行时,注意要让程序在后台运行,在用ps命令查看当前的程序运行的状态)

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 */
    }
}
  1. 其中getpid()是获取当前运行的进程的进程号。那么也就是说子进程和父进程都将输出一句话,并且会输出自己的进程号。
  2. 但是在else当中,也就是父进程会执行的指令当中,出现了无限循环while (1)。也就是说父进程一直都没有结束,这也是为什么该程序要在后台运行。但是子进程最终是exit(0);

下面是我运行该程序时的情况:


 [Tong@localhost chap8_code]$ ./fork7.o && 可以让程序在后台运行)
 [1] 10923
  Running Parent, PID = 10923 
 Terminating Child, PID = 10927
 [Tong@localhost chap8_code]$ ps
    PID TTY          TIME CMD
  10039 pts/0    00:00:00 bash
  10923 pts/0    00:00:05 fork7.o
  10927 pts/0    00:00:00 fork7.o <defunct>
  10931 pts/0    00:00:00 ps
 [Tong@localhost chap8_code]$ kill 10923
 [1]+  Terminated              ./fork7.o
 [Tong@localhost chap8_code]$ ps
    PID TTY          TIME CMD
  10039 pts/0    00:00:00 bash
  10972 pts/0    00:00:00 ps
  

在这一段运行结果中,我们可以非常容易看到子进程和父进程的进程号。同时,在输入命令ps后,我们可以看到 10927 pts/0 00:00:00 fork7.o <defunct>,这最开始的几个数字即为进程的进程号,而我们也可以发现,这就是子进程的进程号。这句话也告诉我们子进程是最后是“死了的”。
但是我们也可以发现10923 pts/0 00:00:05 fork7.o,开头的进程号是父进程的,但是它并没有 < defunct > ,它仍然在运行,而这也符合我们之前的分析。

这时我们可以使用kill命令,把我们想要结束的进程“杀死”。用 kill + 想要终止的进程的进程号,即可”杀死“该进程。掌握着一个进程的生死的感觉还是很不错的呢(bushi)

8. emonstration of nonterminating child.

注:Child still running even though parent terminated. Must kill explicitly.
(当父亲走后,孩子还在运行。必须显式杀死。哦,有点残忍

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

该代码与上一代码只是把父子做的事交换了一下,不再分析。
运行结果如下:


 [Tong@localhost chap8_code]$ ./fork8.o &
 [1] 11063
 Terminating Parent, PID = 11063
 Running Child, PID = 11066
 [1]+  Done                    ./fork8.o
 [Tong@localhost chap8_code]$ ps
    PID TTY          TIME CMD
  10039 pts/0    00:00:00 bash
  11066 pts/0    00:00:05 fork8.o
  11080 pts/0    00:00:00 ps
 [Tong@localhost chap8_code]$ kill 11066
 [Tong@localhost chap8_code]$ ps
    PID TTY          TIME CMD
  10039 pts/0    00:00:00 bash
  11127 pts/0    00:00:00 ps
 

9. synchronizing with and reaping children (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");
}
  1. wait(&child_status); 即父进程等待子进程,子进程被终止后,在执行下面的指令。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *statusp);

//返回:如果成功,则为子进程的PID; 如果出错,则为-1。
  1. if (fork() == 0)如果当前进程为子进程,则执行if下的语句,否则执行else下的语句。

运行结果如下:
HP: hello from parent
HC: hello from child
CT: child has terminated
Bye

分析如下:
在这里插入图片描述

10. Synchronizing with multiple children (wait)

注:Reaps children in arbitrary order(以任意的顺序回收子进程)
WIFEXITED and WEXITSTATUS to get info about terminated children(利用WIFEXITED和WEXITSTATUS来获得以终止的子进程的信息)

#define N 5
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);//出错则返回-1,否则返回子进程号
		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);
    }
}
  1. WIFEXITED(status):如果子进程通过调用exit或者一个返回(return)正常终止,就返回真。
  2. WEXITSTATUS(child_status):返回一个正常终止的子进程的退出状态。只有在WIFEXITED()返回为真时,才会定义这个状态。
  3. pid_t pid[N];用来存放子进程的进程号,因为我们已知,在父进程中,fork()会返回子进程的进程号。而且,在代码中,子进程一旦新建,就会不做任何事情直接exit(100+i);而当执行完5个fork()后,父进程会开始新的循环,来回收已经终止的子进程的信息(注意,在这里父进程是随机回收子进程的,因为谁也不能确定究竟是哪个子进程先结束呢)。

运行结果如下:
Child 11334 terminated with exit status 100
Child 11335 terminated with exit status 101
Child 11336 terminated with exit status 102
Child 11337 terminated with exit status 103
Child 11338 terminated with exit status 104

(虽然在我的虚拟机上这些子进程是恰好按顺序终止的,但是每次、不同的机器运行的结果都未必是这样的,它的终止顺序是随机的!!!)接下来就让我们来看看我画的分析图吧。

分析如下:
在这里插入图片描述
这里仅仅是画出了大概的一个流程,父进程在子进程结束后做的事情并没与画出,因为这取决于那个子进程会先执行中止语句,画起来就会比较麻烦。大概能理解就行吧。

11. Using waitpid to reap specific children

注:Reaps children in reverse order(按倒序回收子进程)

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))//如果子进程通过调用exit或return正常终止,则返回真
	   		printf("Child %d terminated with exit status %d\n",wpid, WEXITSTATUS(child_status));
	else
	    printf("Child %d terminate abnormally\n", wpid);
    }
}

这一段代码与上一段代码可以说是十分的相似,但是我们细细观察就能发现其中细微的差异。

  1. pid_t wpid = waitpid(pid[i], &child_status, 0);在这一句就是与上一段不一样的地方,注释中也说明了,这里父进程会等待进程号为pid[i]的进程终止后,再执行接下来的操作,这里也就控制了父进程按倒序回收子进程。

运行结果如下:
Child 11435 terminated with exit status 104
Child 11434 terminated with exit status 103
Child 11433 terminated with exit status 102
Child 11432 terminated with exit status 101
Child 11431 terminated with exit status 100

这里就不再画分析图了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值