天天看点

fork()函数有关实验及解析

fork()是什么?

fork()的原理其实非常简单:当你调用fork()时,会产生一个子进程,这个子进程会拥有父进程所有的东西(记住,是所有)更通俗一点来讲,在函数代码段中,调用fork后,调用一次,返回两次,如果返回0,则代表当前的进程是子进程,不是的话则是父进程。接下来我们看段代码深入理解一下:

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); 
	fflush(stdout);
	return 0;
    }

    /* Parent */
    printf("parent: x=%d\n", --x); 
    fflush(stdout);
    return 0;
}
           

代码中先定义了x=1,之后调用了fork并获取返回值,它判断了pid的值,如果等于0,代表是子进程,则输出child:x= ,如果是父进程则输出parent:x= ,在这里要注意两个进程是相互独立互不干扰的,所以每一次的输出结果可能都会不一样,我本次输出的是:

  • child:x=2
  • parent: x=0

如答案所示:判断pid等于0,则是子进程输出x加一之后的值,之后则退出,return 0直接为退出程序,而到了父进程的时候,x仍为1,则父进程输出x减一之后的值。怎么样,看到现在是不是有点明白原理了呢?接下来一大波实验即将来袭!

实验一

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

这个是很简单的例子,如果是子进程则输出Hello from child,父进程则输出Hello from parent。

则输出结果可能是以下可能:

  1. Hello from child

    Hello from parent

  2. Hello from parent

    Hello from child

还有一点不知道有没有细心的小伙伴注意到,上面实验中有一个fflush(stdout),而这个实验中没有,而是换行符。

首先呢,对于一个shell中运行的程序,默认的标准输出是控制台,而控制台是行缓冲的,也就是系统在缓冲区看到换行符,就帮你冲洗缓冲区。如果调用fflush,也可以触发输出,这就是强制冲洗。因此,如果只写输出语句不加换行,屏幕上是不会有语句的哦~不仅不会,有的时候还会产生令你满头问号的结果,因此,养成一个良好的习惯,从现在做起!

实验二

void fork2()
{
    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);
}
int main(){
	fork2();
}
           
fork()函数有关实验及解析

可以看到运行结果,调用fork后,子进程拥有父进程所有代码,所以printf也要执行,即如上图所示,也可以观察到子进程和父进程的进程号。

实验三

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

           
fork()函数有关实验及解析

好的,我估计到这里就有人开始头冒问号了,这里我用最通俗的语言讲一下,不然再看下去的小伙伴就会秃头了!

大家可以在本子上画一个进程图,就会很清晰

fork()函数有关实验及解析

我们可以从图中看出大体结构,打印完L0只后,调用了fork返回两个进程父进程继续往下走,打印L1,调用fork又返回两个进程,父进程继续往下走,打印bye,而第一个fork的子进程只会继承fork之后的代码,因此打印L1后继续调用fork,再产生两个子进程,只后如上,但因为所有的父进程和子进程都是相互独立的(就是当前干嘛完全是计算机想干啥干啥,反复横跳,但要记住,要有bye必须得先有L1,要想有L1,必须得先有L0)所以输出可能性就有很多种,有啥可能性呢?如下(这里就不换行了 麻烦):

  1. L0 L1 Bye L1 Bye Bye Bye
  2. L0 L1 L1 Bye Bye Bye Bye
  3. L0 L1 Bye Bye L1 Bye Bye

好了,你有没有列出来呢,接下来难度升级哦,准备好!

实验四

void fork4()
{
    printf("L0\n");
    fork();
    printf("L1\n");
    fork();
    printf("L2\n");
    fork();
    printf("Bye\n");
}
int main(){
    fork4();
}
           
fork()函数有关实验及解析

好的,我们可以数一数一共有一个L0,两个L1,四个L2,8个Bye,但因为进程独立,所以输出方式又有很多种啦,上面只是其中之一,列举如下的可能性(太多啦,只列举一些,大家只要记住上面的规则就可以都推出来的,完全是数学的排列组合问题嘛,o(╥﹏╥)o):

  1. L0 L1 L2 Bye Bye L1 L2 Bye Bye L2 Bye Bye L2 Bye Bye
  2. L0 L1 L2 Bye L1 Bye L2 L2 L2 Bye Bye Bye Bye Bye Bye
  3. L0 L1 L2 Bye L1 Bye L2 Bye Bye L2 Bye Bye L2 Bye Bye
  4. L0 L1 L2 Bye L1 Bye L2 Bye Bye L2 L2 Bye Bye Bye Bye
  5. L0 L1 L2 L2 Bye Bye Bye Bye L1 L2 L2 Bye Bye Bye Bye

实验五

void fork5()
{
    printf("L0\n");
    if (fork() != 0) {
	printf("L1\n");
		if (fork() != 0) {
	    printf("L2\n");
		}
    }
    printf("Bye\n");
}
int main(){
    fork5();
}
           
fork()函数有关实验及解析

梳理一下,结果是1个L0 ,1个L1, 1个L2, 3个Bye,因为是if的嵌套,所以我们来走一遍,先输出L0,if中调用fork产生两个进程,父进程继续往下走,输出L1,调用fork,输出L2,输出Bye,第一个子进程在第一个if中条件不符合,直接输出Bye,第二个子进程也不满足第二个if,直接输出Bye,因此答案就出来啦~

实验六

void fork6()
{
    printf("L0\n");
    if (fork() == 0) {
	printf("L1\n");
	if (fork() == 0) {
	    printf("L2\n");
	}
    }
    printf("Bye\n");
}
int main(){
    fork6();
}

           

这个跟上面是类似的,就不再多做赘述了喔。

fork()函数有关实验及解析

实验七

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

void fork7()
{
    atexit(cleanup);
    fork();
    exit(0);
}
int main(){
    fork7();
}
           
fork()函数有关实验及解析

首先先来解释一下atexit函数:atexit函数是一个特殊的函数,它是在正常程序退出时调用的函数,被称为登记函数,因此,在上文中,cleanup就是一个登记函数,登记函数被退出函数exit自动调用,因此调用fork后的子进程因为继承了exit函数,所以也会输出cleaning up,了解了重要函数这个就很简单啦~

实验八

void fork8()
{
    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 */
    }
}
int main(){
    fork8();
}
           
fork()函数有关实验及解析

很简单的判断父进程和子进程的例子,但因为其中加了一个死循环,即while(1),则当前会一直在运行,强制退出即可。

实验九

void fork10()
{
    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");
}
int main(){
    fork10();
}
           
fork()函数有关实验及解析

在这个代码中又出现了新的函数:wait(),我们来看看它的具体作用:进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1

因此,在上面,父进程输出语句后调用了wait,则子进程运行结束后,输出子进程结束了,输出Bye。

实验十

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#define N 5
void fork11()
{
    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);
    }
}
int main(){
    fork11();
}
           
fork()函数有关实验及解析

因为一个for循环,循环了五次,每次的fork返回一个子进程一个父进程,子进程调用exit直接退出,父进程每次调用wait返回子进程的状态,当结束时调用输出语句,输出多少进程号的子进程结束,并伴有状态码。

实验十一

void fork12()
{
    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);
    }
}
int main(){
    fork12();
}

           
fork()函数有关实验及解析

与上个不同的是,父进程的循环是反着的,因此输出子进程的进程号也是从大到小输出的。

实验十二

#define N 5

void fork13()
{
    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 main(){
    fork13();
}
           
fork()函数有关实验及解析

这里的不同则是,子进程中有一个死循环,即一直运行,父进程中杀死了子进程,输出的时候wait获得子进程的状态,因为不是exit正常退出而是被杀死的,因此输出子进程不正常的结束。

实验十三

#define N 5
void int_handler(int sig)
{
    printf("Process %d received signal %d\n", getpid(), sig); /* Unsafe */
    exit(0);
}
void fork14()
{
    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);
    }
}
int main(){
    fork14();
}

           
fork()函数有关实验及解析

又出现了新的函数signal:signal(参数1,参数2);

参数1:我们要进行处理的信号。

参数2:我们处理的方式,上文中则是输出语句

而信号则是进程与进程之间通信的方式。

上面进程收到了信号2,则是进程中断,父进程输出语句时,wait得到的状态改变了。

因为进程独立,因此我们在运行一次,可能会出现如下结果

fork()函数有关实验及解析

都是正常的哦~

具体信号如下:

  • SIGABRT 进程停止运行 6

    SIGINT 终端中断 2

    SIGKILL 停止进程(此信号不能被忽略或捕获)

    SIGQUIT 终端退出 3

实验十四

#define N 5
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 fork15()
{
    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)
	;
}
int main(){
    fork15();
}
           
fork()函数有关实验及解析

这个实验的光标一直在闪,说明没有结束进程,具体我们和下个实验结合一起来一起看

实验十五

#define N 5
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 fork16()
{
    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();
    }
}
int main(){
    fork16();
}
           
fork()函数有关实验及解析

看到这里迷惑了,为什么只加了一个函数结果完全 不一样了?我们先来看看函数吧

pause() :它会令目前的进程挂起, 直到信号被传送,进程要么终止要么触发调用信号处理函数。

返回值:仅当捕获信号并且信号捕获函数返回时才返回。这种情况下,仅返回-1,并且errno设置为EINTR

sleep() :sleep()会令目前的进程暂停参数seconds 所指定的时间, 或是暂停中途被信号所中断.

返回值:若进程暂停到参数seconds 所指定的时间则返回0, 若有信号中断则返回剩余秒数.

看到这两个函数,会发现相似点,都是会被信号中断的函数,而实验十五中,在死循环中使用pause函数让父进程被挂起,只有捕获到信号的时候 触发信号处理程序,这个时候子进程运行完,被挂起的父进程收到了子进程返回的状态信号,因此,触发了输出语句。

实验十六

void fork17()
{
    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);
    }
}
int main(){
    fork17();
}
           
fork()函数有关实验及解析

这里看一下新的函数:

getpgrp():用来取得目前进程所属的组识别码. 此函数相当于调用getpgid(0),嵌套if中,只有子进程进入,之后子进程中又调用了fork,同样只有子进程输出语句。

书中家庭作业有关fork实验

8.14

void doit(){
    if(fork()==0){
        fork();
        printf("hello\n");
        exit(0);
    }
    return;
}
int  main(){
    doit();
    printf("hello\n");
    exit(0);
}
           
fork()函数有关实验及解析

这个的进程图如下图所示:

fork()函数有关实验及解析

因为doit函数中,子程序最后调用exit直接退出,则不会继续执行输出语句,而父进程不会进if,则执行return返回到main函数,执行之后的输出语句,后exit结束进程。

8.15

void doit(){
    if(fork()==0){
        fork();
        printf("hello\n");
        return;
    }
    return;
}
int  main(){
    doit();
    printf("hello\n");
    exit(0);
}
           
fork()函数有关实验及解析

进程运行结果和上面不同,是五个hello,这就对比出了exit和return的区别,下图是进程图

fork()函数有关实验及解析

exit函数是系统级调用,来终结程序用的,使用后程序自动结束,跳回操作系统。而return是函数级调用,是返回调用的地方,因此子进程执行完调回main函数,不会终结,所以会有五个输出。

8.18

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);
 }
           
fork()函数有关实验及解析
fork()函数有关实验及解析

前面讲过atexit函数,叫做登记函数,在调用exit的时候,自动调用end函数,因为第一个fork的时候只有子进程调用了登记函数,因此只有上面的才会在调用exit时候同时输出语句,而下方父进程则没有。因此我们看看这道题的选项:

A.112002 B.211020 C.102120 D.122001 E.100212

正确答案是ACE,因为就像上面讲过的原理,虽然各进程是独立的,但绝不会“无中生有”,像进程图中画的很明白:第一个子进程的两条进程中,没有输出0,1,就不可能输出2

所以我们一起来看看,如果我们从上到下将进程标为①②③④的话,讲起来会简单一点:

A:正确。其中的一种可能是 ② :1 → ④:1 → ②:2 →①:0→ ③:0 → ①:2

B:错误。正如上面所说,没有0,1,不可能出2 ,所以2不可能在前。

C: 正确。 可以是 ②:1 →①:0 → ①:2 → ④:1 → ②:2 →③:0

D: 错误。因为只输出一个1,不可能产生两个2.

E:正确。可以是②:1 →①:0 → ③:0 → ②:2 → ④:1 → ①:2

所以正确答案是ACE,只要理解独立但不能“无中生有”的原则,这道题就很好做啦~