fork()应用

fork()函数用于父进程创建一个新的运行的子进程

一、父进程与子进程的关系

            子进程几乎但是不完全与父进程相同。

            子进程得到与父进程用户级虚拟地址空间相同的(但是独立)的一份副本,包括代码数据段共享库用户栈

            子进程还获得与父进程任何打开文件描述符相同的副本,这意味着,当父进程调用fork()时,子进程可以读写父进程种打开的任何文件。

            父进程和子进程最大的区别在于他们有不同的PID。

二、fork()的特性

            我们先来看一个简单的代码

int main()
{
    pid_t pid;
    int x=1;

    pid=fork();
    if(pid==0){  /*Child*/
        printf("child:x=%d\n",++x);
        exit(0);
    }

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

//例子ex1

运行之后的结果是

                      parent:x=0

                      child:x=2

扫描二维码关注公众号,回复: 9487083 查看本文章

  1、调用一次,返回两次。

         fork函数被父进程调用一次,但是却返回两次。一次返回到父进程中,一次返回到新创建的子进程中。在子进程中,fork的返回值为0。

  2、并发执行

        父进程和子进程是并发运行的独立进程。内核能够以任何方式交替执行他们的逻辑控制流中的指令。在我的电脑系统上运行上述的例子时,可以看到父进程先完成printf语句,接着才是子进程。然而,在另一个系统上可能正好相反,一般而言,作为程序员,我们绝不能对不同进程中指令的交替执行做任何假设

  3、相同但是独立的地址空间

     父进程和子进程会有相同的用户栈、相同的本地变量值、相同的堆、全局变量值和代码。所以当fork函数在第6行返回时,本地变量x无论在父进程还是子进程中都是1。然而,他们二者都是独立的进程,他们都有自己的私有地址空间,所以他们对x的所作的任何改变都是独立的,不会反应在另一个进程的内存中。这就是为什么最后的输出值不同

 4、共享文件

   父进程和子进程他们的输出都显示在屏幕上。原因是子进程继承了父进程所有的打开文件。当父进程调用fork时,stdout文件是打开的,并指向屏幕,子进程继承了这个文件,因此他的输出也是指向屏幕的。

三、进程图

进程图时刻画程序语句偏序的一种简单的前驱图

每个顶点对应于一条程序语句的执行

每个有向边a—>b代表a发生在语句b之前

边上可以标记一些信息

ex1的进程图可表示为

      再看一个进程图,帮助我们理解在嵌套情况下,fork调用程序的情况

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

它对应的进程图

运行的结果和进程图是一样的

四、fork举例

①普通调用

#include "csapp.h"

int main()
{
    int i;

    for(i=0;i<2;i++)
        Fork();
    printf("hello\n");
    exit(0);
}

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

}

根据运行结果我们可以大致可以看出,这个结果还是有一定顺序可依的,先运行了上半部分,再运行了下半部分。但是如果fork次数多起来,结果会是什么样?

int mian()
{
    printf("L0\n");
    fork();
    printf("L1\n");    
    fork();
    printf("L2\n");    
    fork();
    printf("Bye\n");
}

结果如下,这次的结果看着非常的混乱。我们贴出流程图进行一下对比,方便起见,图上序号代表执行的顺序。

从流程图可以看出,这次的例子看样子不再顺序执行。其实这是因为子进程和父进程是并行的,不一定执行哪个进程,执行到哪一步。所以顺序是混乱的,而且我标注的只是可能的情况之一,肯定存在其他执行的顺序。不过有一点可以确定的是,这看似没有顺序的表面下是存在规则的。那便是,一个进程也是要从前往后,绝不可能从后向前。比如,流程图中,9号L2是永远不可能比8号L1先执行的。 但是13号Bye却可以在14号L2前执行,因为二者并不属于一个进程(一共存在八个进程)。

 ②exit和return

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

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

接下来,如果我们把if语句中的exit换成return会是什么样呢?? 

我们会发现,输出的hello多了两个。exit代表着退出程序,说明不再执行这个程序了。而return知识返回上一层函数。所以当exit换为return后,我们可以看到,两次fork后产生的子进程执行return回到了main函数,执行了main中的printf,所以输出结果不同。 

③printf与清除缓存fflush

int main()
{
        int i;
        for(i = 0; i < 2; i++)
        {
            fork();
            printf("A");
        }
        wait(NULL);
    
        return 0;
}

这是一个很普通的嵌套调用,大概想象一下,他应该输出6个‘A’,我们运行一下 

运行结果好像不太对,程序居然输出了8个'A',这是为什么?

        原因在于,printf("A")只是把A放进了进程的缓冲区,在进程结束后,才输出A。然而,fork创建子进程时,子进程连带着父进程的缓冲区一起复制了,所以在整个进程的执行过程中,进程缓冲区情况如下

但是,如果我们把输出加换行,或者及时用fflush清理缓存,及时输出,效果就和预想一样。有多少printf就有多少个A

int main()
{
        int i;
        for(i = 0; i < 2; i++)
        {
            fork();
            printf("A\n");

            //fflush(stdout);
        }
        wait(NULL);
    
        return 0;
}

④等待waitpid和wait

int main()
{
    if(fork()==0){
        printf("a");
        fflush(stdout);
        exit(0);
    }
    else{
        printf("b");
        fflush(stidou);
        waitpid(-1,NULL,0);//此处waitpid效果与wait相同,一直等待子进程结束
    }
    printf("c");
    fflush(stdout);
    exit(0);
}

 我的系统下先运行了父进程,父进程执行wait一直等待子进程结束,才执行printf("c")。若是注释wait,父进程先输出,子进程后输出

接下来我们看一下wait和waitpid的对比

#define N 10
int main()
{
    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);
    }

}

可以看出来,被回收的子进程的顺序是杂乱的。这是因为当父进程调用wait(&child_status),只要任一一个子进程成为了僵尸进程就会将其回收,并不会指定某一个特定的子进程。所以回收进程的顺序杂乱。

如果我们把 wait(&child_status) 换成 waitpid(pid[i],&child_status,0) ,看一下运行结果

这下回收子进程的是有顺序的。这是因为waitpid函数的第一个参数pid[i]如果>0,则是指定某个子进程。所以顺序结束的子进程也会被顺序回收。 

更多有关wait和waitpid的详细用法可以参考我的另一篇博客https://blog.csdn.net/qq_43135849/article/details/103050773

⑤exit

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


/**
atexit函数是注册终止函数
即结束进程时调用注册的函数
*/
void fork6()
{
    atexit(cleanup);
    fork();
    exit(0);
}

可以看到最后出现了两个Cleaning up,父子进程各自结束后都调用了cleanup函数

⑥信号

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

}

第一个for循环产生了N个无限不停止的子进程,和正常的N个父进程。第二个for循环父进程向子进程发送kill的信号,子进程被杀死。第三个for循环回收僵尸进程,可以看到结果子进程不是正常结束。

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


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

}

利用signal修改关联行为。signal原型函数是sighandler_t signal ( int signum , sighandler_t handler ) ;

signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为

若 handler 为SIG_IGN 忽略类型为signum的信号
若handler 为SIG_DFL 类型为signum的信号行为恢复为默认行为
若handler 为函数地址 这个函数被称为信号处理程序,只要进程接收到一个类型为signum的信号,就会调用这个程序。通过把处理程序的地址传递好signal函数从而该改变默认行为,这叫做设置信号处理程序

第三种是我们重点研究的对象。所以根据下面的结果可以看到这个例子当中子进程最后是执行了exit正常退出,而非上一个例子非正常退出。

我们再看两个关于pause的例子 

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

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

从结果我们可以看到 最终只回收了一个子进程,这是因为子进程睡眠1s,父进程陷入了循环,不能回收全部的子进程。

但是,我们稍微改一下。把while循环的内容改为pause();,把关联函数中回收子进程的wait放入循环

int ccount;
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 */
    }
}

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

 现在我们可以看到,这个程序把所有的子进程全部回收。结果在于pause().pause()函数使该进程暂停让出CPU,但是该函数的暂停是可被中断的睡眠,也就是说收到了中断信号之后可以继续执行,所以父进程可以被唤醒从而回收子进程。

⑦逻辑运算符

一道很经典的关于fork的面试题,运行以下代码,除去main进程,一共会产生多少子进程。方便起见,每产生一个子进程便输出一个语句,利于观察。

int Fork()
{
    printf("Child is here");
    int pid;
    pid=fork();
    return pid;
}
int main()
{
    Fork();
    Fork()&&Fork()||Fork();
    Fork();
    exit();
}

我们先复习一下逻辑运算符

A&&B 若A=ture,则检查B;若A=false,则不检查B
A||B 若A=true,则不检查B;若A=false,则检查B

我们来看一下Fork()&&Fork()||Fork()这句话,为了解释,我们分别命名为Fork_1,Fokr_2,Fork_3。看一下每个Fork取值情况。

一共存在五种情况,在这两条语句之前分别有一句Fork,所以除去main进程,一共有5*2*2-1=19个,我们来运行验证一下

⑧死循环

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 */
    }

}

运行后可以发现,因为父进程陷入了死循环,程序一直无法结束。所以我们用Ctrl+Z停止进程,再通过ps命令查看进程情况

可以看到,子进程23110已经执行了exit,但是还没有被回收,变成了僵尸进程,这都是因为父进程23109处于死循环状态,无法回收子进程。只能手动杀死父进程23109

我们再来看一个例子

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

}

它的运行结果就不太一样了

这下我们可以看到,程序会自动结束,不像上一个例子程序陷入死循环需要Ctrl+Z强制停止。这是因为在这个例子中,子进程陷入里死循环,父进程退出。子进程变成了孤儿进程,此时系统把init进程(所以进程的父进程)设为子进程的养父进程,代替父进程执行工作,收集子进程的信息。

⑨进程组

我们可以简单查一下进程组组号

int main() 
{
    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);
    }else printf("Parent pid = %d ",getpid());
} 

可以这样理解一下,父进程的pid最为进程组的组号

int main() 
{
    if (fork() == 0) {
	printf("Child: pid=%d pgrp=%d\n",
	       getpid(), getpgrp());
    }
    else {
	printf("Parent: pid=%d pgrp=%d\n",
	       getpid(), getpgrp());
    }
    while(1);
} 
发布了22 篇原创文章 · 获赞 2 · 访问量 5439

猜你喜欢

转载自blog.csdn.net/qq_43135849/article/details/102875491