CSAPP基本版第八章学习日志:关于fork函数(上)

在说fork函数之前先讲讲进程。进程是计算机科学中最深刻、最成功的概念之一,其经典定义就是一个执行中程序的实例。
当我们通过向shell(shell即人机交互接口,详细可参考:Shell 教程)输入一个可执行目标文件的名字(例./a.out),运行程序时shell就会创建一个新的进程,然后在这个新进程的上下文中(上下文由程序正确运行所需的状态组成,状态包括存放在内存中的程序的代码和数据等)运行这个可执行目标文件。
而fork函数可令一个进程创建一个新的运行的进程,前一个进程称为父进程,后一个进程称为子进程
新创建的子进程大部分与父进程相同,相当于子进程获得了与父进程用户级虚拟地址空间相同但是独立的一个副本,包括代码和数据段、堆、共享库以及用户栈。
两种进程最大的区别在于它们有不同的PID(PID是进程的编号,每个进程都有一个唯一的正整数进程ID)。
由于我们的所有程序要执行都要变成进程,我们的进程由shell创建,因此我们程序的进程的父PID即shell的PID。

再进一步解释之前,谈一下缓冲区的概念。来源:——关于缓冲区详解\n刷新缓冲区问题
缓冲区是内存空间的一部分,用来缓冲输入或输出的数据的存储空间就叫做缓冲区。比如,遇到printf("*");语句,程序并不是立刻将字符串内容输入到屏幕上,而是先存入缓冲区。如果程序结束之后没有清空缓冲区,那么字符串内容是不会输出的。

  • 而如果在printf里加入换行符\n,\n有清空缓冲区并换行的功能,那printf的内容就会立即输出。
  • 除此之外,语句fflush(stdout); 就是专门清空缓冲区的操作,如果在printf下加入该语句,也可清空缓冲区而输出语句。
  • exit(0); 也有先清空缓冲区再退出的功能,如果exit(0);换成_exit(0);则只退出而不清空缓冲区,这样printf的内容会一直存在缓冲区中不会在屏幕上显示。

缓冲区如果一直未被清空,可能会在某一时刻出现后面的数据存入时之前缓冲区内容被推出的情况(即之前的内容会显示在屏幕上),这样会导致意想不到的结果!

#include <sys/types.h>	//运行fork函数所需头文件
#include <unistd.h>	//运行fork函数所需头文件

pid_t fork(void);	//pid_t是int的宏定义,表示fork返回值为整数

其中,在父进程中,fork返回子进程的PID。在子进程中,fork返回0。如果出错,fork返回-1。
由此,我们可知fork函数的一些特点:

  • 调用一次,返回两次。 一次是返回到父进程,一次是返回到子进程。
  • 并发执行 父进程和子进程是并发运行的独立进程,所以运行结果具有不确定性。

下面我们来看一组代码:
forks.c代码内容:

/*
 * forks.c - Examples of Unix process control
 */
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> 
#include <signal.h>

/*
 * fork0 - The simplest fork example
 * Call once, return twice
 * Creates child that is identical to parent
 * Returns 0 to child process
 * Returns child PID to parent process
 */
void fork0() 
{
    if (fork() == 0) {
	printf("Hello from child\n");
    }
    else {
	printf("Hello from parent\n");
    }
}

/* 
 * fork1 - Simple fork example 
 * Parent and child both run same code
 * Child starts with identical private state
 */
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);
}

/*
 * fork2 - Two consecutive forks
 * Both parent and child can continue forking
 * Ordering undetermined
 */
void fork2()
{
    printf("L0\n");
    fork();
    printf("L1\n");    
    fork();
    printf("Bye\n");
}


/*
 * fork3 - Three consective forks
 * Parent and child can continue forking
 */
void fork3()
{
    printf("L0\n");
    fork();
    printf("L1\n");    
    fork();
    printf("L2\n");    
    fork();
    printf("Bye\n");
}

/* 
 * fork4 - Nested forks in parents
 */
void fork4()
{
    printf("L0\n");
    if (fork() != 0) {
	printf("L1\n");    
	if (fork() != 0) {
	    printf("L2\n");
	}
    }
    printf("Bye\n");
}

/*
 * fork5 - Nested forks in children
 */
void fork5()
{
    printf("L0\n");
    if (fork() == 0) {
	printf("L1\n");    
	if (fork() == 0) {
	    printf("L2\n");
	}
    }
    printf("Bye\n");
}

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

/*
 * fork6 - Exit system call terminates process
 * call once, return never
 */
void fork6()
{
    atexit(cleanup);
    fork();
    exit(0);
}

/* 
 * fork7 - Demonstration of zombies.
 * Run in background and then perform 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 */
    }
}

/* 
 * fork8 - Demonstration 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);
    }
}

/*
 * fork9 - 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");
}

#define N 5
/* 
 * fork10 - Synchronizing with multiple children (wait)
 * Reaps children in arbitrary order
 * WIFEXITED and WEXITSTATUS to get info about terminated children
 */
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);
    }
}

/* 
 * fork11 - 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))
	    printf("Child %d terminated with exit status %d\n",
		   wpid, WEXITSTATUS(child_status));
	else
	    printf("Child %d terminate abnormally\n", wpid);
    }
}


/********* 
 * Signals
 *********/

/*
 * fork12 - Sending signals with the kill() function
 */
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);
    }
}

/*
 * int_handler - SIGINT handler
 */
void int_handler(int sig)
{
    printf("Process %d received signal %d\n", getpid(), sig); /* Unsafe */
    exit(0);
}

/*
 * fork13 - Simple signal handler example
 */
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);
    }
}


/*
 * child_handler - SIGCHLD handler that reaps one terminated child
 */
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 */
}

/*
 * fork14 - Signal funkiness: Pending signals are not queued
 */
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)
	;
}


/*
 * child_handler2 - SIGCHLD handler that reaps all terminated children
 */
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 */
    }
}

/*
 * fork15 - Using a handler that reaps multiple children
 */
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();
    }
}

/* 
 * fork16 - Demonstration of using /bin/kill program 
 */
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);
    }
} 

/* 
 * Demonstration of using ctrl-c and ctrl-z 
 */
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);
} 


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

该组代码提供了17种不同的fork应用实例,通过在linux平台上gcc -o forks forks.c来编译此程序即可获得可执行目标文件forks。由main函数,在运行时程序会根据用户在shell行输入的./forks之后的参数决定运行哪一组实例,若不带参数则相当于选择运行fork0。
对于fork函数运行的结果,我们可以通过画进程图来展示。进程图是刻画程序语句的偏序的一种简单的前趋图。每个顶点a对应于一条程序语句的执行。有向边a->b表示语句a发生在语句b之前。边上可以标记出一些信息,例如一个变量的当前值。

这里分析其中fork0到fork11的实例。剩下利用信号的实例将在“关于fork函数(下)”介绍。

fork0()

/*
 * fork0 - The simplest fork example
 * Call once, return twice
 * Creates child that is identical to parent
 * Returns 0 to child process
 * Returns child PID to parent process
 */
void fork0() 
{
    if (fork() == 0) {
	printf("Hello from child\n");
    }
    else {
	printf("Hello from parent\n");
    }
}

在shell命令行输入./forks 0运行结果如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 0
Hello from parent
username@username-virtual-machine:/mnt/hgfs/chap8_code$ Hello from child

该实例体现了fork函数调用一次,返回两次的特点,我们可以看到输出语句printf("Hello from child\n");printf("Hello from parent\n");都执行了,这是因为fork()函数返回了一个0值(代表子进程)和>0的值(代表父进程),因此if-else的两句输出语句均被执行。
注意由于进程是并发执行的,在不同的系统上父进程和子进程执行的先后顺序可能会有所不同,比如这里就先执行了父进程并输出相关语句,由于父进程是shell main 中创建的,父进程结束后便回到shell语句,因此子进程输出前还显示了shell命令行。
该实例进程图为:
8-0

fork1()

/* 
 * fork1 - Simple fork example 
 * Parent and child both run same code
 * Child starts with identical private state
 */
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);
}

在shell命令行输入./forks 1运行结果如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 1
Parent has x = 0
Bye from process 2316 with x = 0
username@username-virtual-machine:/mnt/hgfs/chap8_code$ Child has x = 2
Bye from process 2317 with x = 2

注:这段代码用到了getpid()函数,该函数返回一个类型为pid_t(在Linux系统上是int的宏定义)的整数值,该值即为调用该函数的进程的PID。
该实例不仅体现了fork函数调用一次,返回两次的特点,还显示了子进程被创建时会得到与父进程代码、数据等相同的一个副本,并且父进程与子进程独立运行。在调用fork之前,父进程与子进程均有一变量x且值为1,接下来子进程输出x+1的值,父进程输出x-1的值,并且两进程最后都输出其进程号及x的值。同样的,在我这台机器上先选择执行了父进程两个输出语句,因此子进程输出前还显示了shell命令行。
注:由于并发,以下几种输出顺序都是可能存在的:

Parent has x = 0
Bye from process 2316 with x = 0
Child has x = 2
Bye from process 2317 with x = 2
Parent has x = 0
Child has x = 2
Bye from process 2316 with x = 0
Bye from process 2317 with x = 2
Parent has x = 0
Child has x = 2
Bye from process 2317 with x = 2
Bye from process 2316 with x = 0
Child has x = 2
Bye from process 2317 with x = 2
Parent has x = 0
Bye from process 2316 with x = 0
Child has x = 2
Parent has x = 0
Bye from process 2317 with x = 2
Bye from process 2316 with x = 0
Child has x = 2
Parent has x = 0
Bye from process 2316 with x = 0
Bye from process 2317 with x = 2

该实例进程图可以清楚显示其关系:
8-1

fork2()

/*
 * fork2 - Two consecutive forks
 * Both parent and child can continue forking
 * Ordering undetermined
 */
void fork2()
{
    printf("L0\n");
    fork();
    printf("L1\n");    
    fork();
    printf("Bye\n");
}

在shell命令行输入./forks 2运行结果如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 2
L0
L1
Bye
username@username-virtual-machine:/mnt/hgfs/chap8_code$ L1
Bye
Bye
Bye

该实例两次调用fork函数,因此父进程创建了两次子进程,注意第二次调用时子进程也创建了相应的子进程。由进程图可知,程序将会输出一个L0,两个L1,四个Bye,其中L0必定是第一个输出,Bye之前至少有一个L1输出,即第二个输出必定为L1,可以看到在该机器上是父进程先执行。
该程序有三种可能输出:

L0
L1
Bye
L1
Bye
Bye
Bye
L0
L1
L1
Bye
Bye
Bye
Bye
L0
L1
Bye
Bye
L1
Bye
Bye

该实例进程图可以清楚显示其关系:
8-2

fork3()

/*
 * fork3 - Three consective forks
 * Parent and child can continue forking
 */
void fork3()
{
    printf("L0\n");
    fork();
    printf("L1\n");    
    fork();
    printf("L2\n");    
    fork();
    printf("Bye\n");
}

在shell命令行输入./forks 3运行结果如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 3
L0
L1
L2
Bye
username@username-virtual-machine:/mnt/hgfs/chap8_code$ L2
Bye
Bye
L1
L2
Bye
Bye
Bye
L2
Bye
Bye

该实例三次调用fork函数,因此父进程创建了三次子进程,而前两次调用的子进程也创建了相应的子进程。由进程图可知,程序将会输出一个L0,两个L1,四个L2,八个Bye,其中L0必定是第一个输出,L2之前至少有一个L1输出,即第二个输出必定为L1,Bye之前至少有一个L2输出,即第三个输出必定为L2,可以看到在该机器上是父进程先执行。

该实例进程图可以清楚显示其关系:
8-3

fork4()

/* 
 * fork4 - Nested forks in parents
 */
void fork4()
{
    printf("L0\n");
    if (fork() != 0) {
	printf("L1\n");    
	if (fork() != 0) {
	    printf("L2\n");
	}
    }
    printf("Bye\n");
}

在shell命令行输入./forks 4运行结果如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 4
L0
L1
L2
Bye
username@username-virtual-machine:/mnt/hgfs/chap8_code$ Bye
Bye

该实例嵌套调用fork函数,因此父进程第一次创建的子进程由于不满足外面的if条件所以直接执行输出Bye,而父进程则进入条件语句并且再一次创建子进程,并且只有父进程输出L2,因此程序将会输出一个L0,一个L1,一个L2,三个Bye,其中L0必定是第一个输出,L2之前有L1输出,Bye之前可以只有L0,或者只有L0和L1,或者L0,L1,L2都在它前面输出(如图所示)。可以看到在该机器上是父进程先执行。

该实例进程图可以清楚显示其关系:
8-4

fork5()

/*
 * fork5 - Nested forks in children
 */
void fork5()
{
    printf("L0\n");
    if (fork() == 0) {
	printf("L1\n");    
	if (fork() == 0) {
	    printf("L2\n");
	}
    }
    printf("Bye\n");
}

在shell命令行输入./forks 5运行结果如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 5
L0
Bye
username@username-virtual-machine:/mnt/hgfs/chap8_code$ L1
Bye
L2
Bye

该实例与fork4相似,只是父进程不满足外面的if条件所以直接执行输出Bye,而父进程第一次创建的子进程则进入条件语句并且再一次创建子进程,并且只有第一次创建的子进程的子进程输出L2,因此程序将会输出一个L0,一个L1,一个L2,三个Bye,其中L0必定是第一个输出,L2之前必有L1输出,Bye之前可以只有L0,或者只有L0和L1,或者L0,L1,L2都在它前面输出(如图所示)。可以看到在该机器上是父进程先执行。

该实例进程图可以清楚显示其关系:
8-5

fork6()

在解释fork6函数之前,先来解释一下atexit函数。来源:——关于atexit函数详解
C 库函数 int atexit(void (*func)(void)) 当程序正常终止时,调用指定的函数 func(在程序终止时被调用的函数)。您可以在任何地方注册你的终止函数,但它会在程序终止的时候被调用。
在fork6里它的功能是在退出时,即exit()时调用函数cleanup(输出Cleaning up)

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

/*
 * fork6 - Exit system call terminates process
 * call once, return never
 */
void fork6()
{
    atexit(cleanup);
    fork();
    exit(0);
}

在shell命令行输入./forks 6运行结果如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 6
Cleaning up
username@username-virtual-machine:/mnt/hgfs/chap8_code$ Cleaning up

该实例通过atexit函数展示了fork函数一次调用,两次返回的特点,即父进程和子进程各执行一次退出,并且两者是独立执行的。
该实例进程图可以清楚显示其关系:
8-6

fork7()

/* 
 * fork7 - Demonstration of zombies.
 * Run in background and then perform 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 */
    }
}

在shell命令行输入./forks 7运行结果如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 7
Running Parent, PID = 2678
Terminating Child, PID = 2679

注意,该程序运行到最后并没有返回shell语句,并且我们无法进行下一步操作。这是因为父进程有死循环while (1) ; /* Infinite loop */,并且父进程是shell main 中创建的,父进程结束后才回到shell语句。此时我们需要在键盘上输入Ctrl+C终止程序或者Ctrl+Z挂起程序(在linux里Ctrl+C是终止程序的功能,Ctrl+Z是挂起程序的功能),才能进行下一步操作。
或者我们可以在一开始时在shell命令行输入./forks 7 &(&:在后台运行),这样程序在后台运行,我们就可以进行下一步操作。并且屏幕上还会显示[1] 2710表示是父进程(因为我们知道了其PID为2710)在进行死循环。

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 7
Running Parent, PID = 2678
Terminating Child, PID = 2679
^C
username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 7 &
[1] 2710
username@username-virtual-machine:/mnt/hgfs/chap8_code$ Running Parent, PID = 2710
Terminating Child, PID = 2711

通过前面几个实例我发现在本台机器上优先执行父进程,因此先执行语句printf("Running Parent, PID = %d\n", getpid());,然后由于死循环,程序就会优先选择执行子进程的语句printf("Terminating Child, PID = %d\n", getpid());,随后子进程正常退出,程序才回到父进程继续执行死循环,这就导致了一开始程序一直在运行无法进行下一步的结果。
注意:该实例与下一个实例对比可以引出回收子进程的概念。
该实例进程图可以清楚显示其关系:
8-7

fork8()

/* 
 * fork8 - Demonstration 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);
    }
}

在shell命令行输入./forks 8运行结果如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 8
Terminating Parent, PID = 2116
username@username-virtual-machine:/mnt/hgfs/chap8_code$ Running Child, PID = 2117

与上一个类似,这次是子进程出现死循环。但是比起上一个实例该实例却可以正常回到shell命令行。我们接下来输入ps(ps可以显示当前进程状态,具体可参考:Linux ps命令)查看运行结果确定如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 8
Terminating Parent, PID = 2116
username@username-virtual-machine:/mnt/hgfs/chap8_code$ Running Child, PID = 2117
ps
   PID TTY          TIME CMD
  2095 pts/0    00:00:00 bash
  2117 pts/0    00:00:39 forks
  2208 pts/0    00:00:00 ps

我们发现子进程(PID为2117)因为死循环而仍在运行,而已经找不到父进程。结合fork7与fork8,我们再输入./forks 7 &进行比较:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 8
Terminating Parent, PID = 2116
username@username-virtual-machine:/mnt/hgfs/chap8_code$ Running Child, PID = 2117
ps
   PID TTY          TIME CMD
  2095 pts/0    00:00:00 bash
  2117 pts/0    00:00:39 forks
  2208 pts/0    00:00:00 ps
username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 7 &
[1] 2374
username@username-virtual-machine:/mnt/hgfs/chap8_code$ Running Parent, PID = 2374
Terminating Child, PID = 2375
username@username-virtual-machine:/mnt/hgfs/chap8_code$ ps
   PID TTY          TIME CMD
  2095 pts/0    00:00:00 bash
  2117 pts/0    00:06:32 forks
  2374 pts/0    00:00:02 forks
  2375 pts/0    00:00:00 forks <defunct>
  2376 pts/0    00:00:00 ps

补充:2095 pts/0 00:00:00 bash中的bash代表我们的实验系统的shell。

在后台运行fork7并也通过ps看当前状态,发现父进程(PID为2374)和子进程(PID为2375)都在运行。

  • 而fork7明明只有父进程有死循环,子进程已经结束运行但仍存在并占用空间。
  • 对比fork8只有子进程有死循环,父进程结束运行并且没有留着占用空间。

这是因为当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收。一个终止了但还未被回收的进程称为僵死进程
由此,fork7中的子进程由于父进程死循环,因此它正常终止却未被回收成为僵死进程,通俗的说,就是子进程还要等父进程来回收它,不然它一直保持已终止的状态。而fork8父进程正常终止,并且由于子进程死循环,父进程没有去回收子进程,通俗的说,就是父进程运行完就走了,子进程变成了孤儿进程

如果一个父进程终止了,内核会安排init进程成为它的孤儿进程的养父,即init去回收子进程。init进程的PID为1,是在系统启动时由内核创建的,它不会终止,是所有进程的祖先。

如果要让父进程等子进程,则可以通过调用waitpid函数或wait函数来等待它的子进程终止或者停止。这将在下一个实例中解释。

该实例进程图可以清楚显示其关系:
8-8

fork9()

/*
 * fork9 - 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");
}

在shell命令行输入./forks 9运行结果如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 9
HP: hello from parent
HC: hello from child
CT: child has terminated
Bye

该程序是wait使用的一个实例。
wait函数是waitpid函数的简单版本

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid,int *statusp,int options);
//如果成功等到子进程,就返回子进程的PID;如果第三个参数设置为常量WNOHANG,则返回0;如果其他错误,例没有子进程时,返回-1
pid_t wait(int *statusp);
//如果成功,返回子进程的PID;如果出错,返回-1

语句wait(&status); 等价于语句waitpid(-1,&status,0);
默认情况下(当options=0时),waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。
等待集合的成员是由参数pid来确定的:

  • 如果pid>0,那么等待集合就是一个单独的子进程,它的进程ID等于pid。
  • 如果pid=-1,那么等待集合就是由父进程所有的子进程组成的。

如果statusp参数时非空的,那么waitpid就会在status中放上关于导致返回子进程的状态信息,status是statusp指向的值。
因此wait(&child_status);或者waitpid(-1,&status,0); 代表挂起调用进程的执行,直到等待集合(父进程的所有子进程)中的一个子进程终止,就将状态信息放在变量child_status里。

  • 因此我们可以看到该机器上父进程先执行printf(“HP: hello from parent\n”);语句
  • 后遇到wait(&child_status);则等待子进程运行printf(“HC: hello from child\n”);并退出
  • 父进程再接下来执行printf(“CT: child has terminated\n”);
  • 最后执行printf(“Bye\n”);

因此呈现图中结果。
该实例进程图可以清楚显示其关系:
8-9

fork10()

#define N 5
/* 
 * fork10 - Synchronizing with multiple children (wait)
 * Reaps children in arbitrary order
 * WIFEXITED and WEXITSTATUS to get info about terminated children
 */
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);
    }
}

在shell命令行输入./forks 10运行结果如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 10
Child 2376 terminated with exit status 104
Child 2375 terminated with exit status 103
Child 2374 terminated with exit status 102
Child 2373 terminated with exit status 101
Child 2372 terminated with exit status 100
username@username-virtual-machine:/mnt/hgfs/chap8_code$ 

该实例进一步对wait函数进行引用。该程序首先定义pid_t类型的数组pid[N],用于循环的变量i和存储子进程状态信息的变量child_status。
接下来,父进程创建N个子进程,每个子进程执行if语句以一个唯一的状态退出。这里补充一下关于exit函数C 库函数 void exit(int status) 立即终止调用进程,参数status为返回给父进程的状态值。 一般我们直接退出时只需调用exit(0)即可。该程序为了清楚显示子进程被创建的顺序,返回状态值为100+i。
接下来循环输出所有子进程信息。首先父进程调用wait(&child_status) 即waitpid(-1,&child_status,0)来等待它的子进程终止并获得其信息,因为pid参数默认为-1,所以对wait的调用会阻塞,直到任意一个子进程终止。在每个子进程终止时,对wait的调用会返回,返回值为该子进程的非零的PID给变量wpid。
接下来检查子进程的退出状态。WIFEXITED(child_status) 是由wait.h头文件定义的解释child_status参数的一个宏,它代表:如果子进程通过调用exit或者一个返回(return)正常终止,就返回真。 即只有子进程正常退出时,那么父进程就提取出退出状态,把Child 2372 terminated with exit status 100等信息输出到stdout上,若出错则输出Child 2372 terminate abnormally。
这里出现了另一个宏WEXITSTATUS(child_status),它代表:返回一个正常终止的子进程的退出状态。只有WIFEXITED()返回为真时,才会定义这个状态。 如结果所示,这里返回了exit的返回给父进程的状态值。
注意,程序不会按照特定的顺序回收子进程。 子进程回收的顺序是这台特定的计算机系统的属性。这是非确定性行为的一个示例。每一个可能的结果都同样可能出现。

fork11()

/* 
 * fork11 - 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))
	    printf("Child %d terminated with exit status %d\n",
		   wpid, WEXITSTATUS(child_status));
	else
	    printf("Child %d terminate abnormally\n", wpid);
    }
}

在shell命令行输入./forks 11运行结果如下:

username@username-virtual-machine:/mnt/hgfs/chap8_code$ ./forks 11
Child 2542 terminated with exit status 104
Child 2541 terminated with exit status 103
Child 2540 terminated with exit status 102
Child 2539 terminated with exit status 101
Child 2538 terminated with exit status 100
username@username-virtual-machine:/mnt/hgfs/chap8_code$ 

该实例与上一个类似,只做了个简单的改变,它消除了这种不确定性。调用wait(&child_status)换成了waitpid(pid[i], &child_status, 0),表示父进程的等待集合是一个单独的子进程,它的进程ID等于pid[i]。
与上一个不同的是,这次是等到一个子进程就输出其信息,因此顺序是确定的,按照父进程创建子进程的倒序回收这些子进程。父进程按照顺序存储了它的子进程的PID,然后通过用适当的PID作为第一个参数来调用waitpid,然后等待子进程。
剩下的实例请参考:CSAPP基本版第八章学习日志:关于fork函数(下)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值