您当前的位置: 首页 >  编程语言

Linux进程(一)

进程的东西太多了,分两部分写吧 操作系统

概念:操作系统是管理计算机硬件与软件资源的计算机程序,简称OS。

为什么要有操作系统:

1.给用户提供稳定、高效和安全的运行环境,为程序员提供各种基本功能(OS不信任任何用户,不让用户或者程序员直接与硬件进行交互)。

2.管理好各种软硬件资源。

从这张图我们可以看到几点内容:

OS管理的硬件部分: 网卡、硬盘等 OS管理的软件部分: 内存管理、驱动管理、进程管理和文件管理,还有驱动和系统调用接口 进程 进程和程序的概念

我们平时所写的C语言代码,通过编译器的编译,最终会成为一个可执行的程序,当这个可执行程序运行起来之后,它就变成了一个进程。

程序是存放在存储介质(程序平时都存放在磁盘当中)上的一个可执行文件,而进程就是程序执行的过程。进程的状态是变化的,其中包括进程的创建、调度和死亡。程序是静态的,进程是动态的。

进程: 计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

如何描述进程:

进程的所有属性信息都被放在一个叫做进程控制块的结构体中,可以理解为进程属性的集合。 这个数据结构的英文名称是PCB(process control block),在Linux的OS下的PCB是task_struct(Linux内核中的一种数据结构,它会被装载到RAM(内存)中并且包含并包含进程的信息)。

task_struct内容有哪些?

标识符:描述本进程的唯一标识符(就像是我们每个人的身份证)。

状态:任务状态、退出代码、退出信号等。

优先级: 程序被CPU执行的顺序(后面会单独介绍)。

程序计数器: 一个寄存器中存放了一个pc指针,这个指针永远指向即将被执行的下一条指令的地址。

内存指针: 包含程序代码和进程相关的数据的指针,还有和其它进程共享的内存快的指针。这样就可以PCB找到进程的实体。

上下文数据: 在单核CPU中,进程需要在运行队列(run_queue) 中排队,等待CPU调度,每个进程在CPU中执行时间是在一个时间片内的,时间片到了,就要从CPU上下来,继续去运行队列中排队。

I/O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。

记账信息: 能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

组织进程 在内核源代码中发现,所有运行在系统里的进程都以task_struct链表形式存在内核中。

进程的状态

进程的状态反应进程执行过程的变化。这些状态随着进程的执行和外界的变化而转换。

五态模型中,进程分为新建态,终止态,运行态,就绪态,就绪态。

(1)TASK_RUNNING(运行态):进程正在被CPU执行。当一个进程被创建的时候会处于TASK_RUNNABLE,表示已经准备就绪,正在准备被调度。

(2)TASK_INTERRUPTIBLE(可中断状态):进程正在睡眠(阻塞)等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置成运行态。处于此状态的进程也会因为接收到信号而提前被唤醒,比如给一个TASK_INTERRUPTIBLE状态的进程发送SIGKILL信号,这个进程将会被先唤醒(进入TASK_RUNNABLE状态),然后再响应SIGKILL信号而退出(变为TASK_ZOMBIE状态),并不会从TASK_INTERRUPTIBLE状态直接退出。

(3)TASK_UNINTERRUPTIBLE(不可中断):处于等待中的进程,待资源被满足的时候被唤醒,但是不可以由其他进程通过信号或者中断唤醒。由于不接受外来的任何信号,因此无法用KILL杀掉这些处于该状态的进程。而TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。

(4)TASK_ZOMBIE(僵死):表示进程已经结束,但是其父进程还没有回收子进程的资源。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用wait函数释放子进程的资源,子进程的进程描述符就会被释放。

(5)TASK_STOPPED(停止):进程停止执行。当进程接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。当接收到SIGCONT信号,会重新回到TASK_RUNNABLE状态。

下面是进程状态在源码中的定义:

static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

查看进程状态相关的命令:

ps命令可以查看进程详细的状态,常用选项如下:

选项 含义 -a 显示终端上的所有进程,包括其他进程 -u 显示进程的详细状态 -x 显示没有控制终端的进程 -w 显示加宽,以便显示更多的信息 -r 只显示正在运行的进程

PID就是进程的进程号,STAT是进程此时处于什么状态。

有下面两种命令(前者查看所用进程的名字,后者可以查看进程的父子关系):

ps aux/ps axj

进程号和相关函数

每个进程都有一个进程号来标识,其类型为pid_t(整型)。进程号是唯一的,但是进程号是可以重用的。当一个进程终止后,其进程号可以再次使用。

进程号(PID)

getpid()可以获取当前进程的进程号。

父进程号(PPID)

getppid()可以获取当前进程的父进程号

进程组号(PGID)

getpgid()可以获取当前进程进程组号

进程创建 fork函数(系统调用)

pid_t fork(void);

功能:通过复制当前进程,为当前进程创建一个子进程

返回值:成功:子进程中返回0,父进程中返回子进程的pid_t。

​ 失败:返回-1。

进程调用fork函数,内核需要做什么?

给子进程分配内存空间,并为子进程创建PCB 将父进程部分数据结构内容(还有代码和数据暂时共享)拷贝至子进程 添加子进程到系统进程列表(运行队列)当中 fork返回,开始CPU调度器调度

fork之后执行什么?

父子进程共享一份代码,fork之后,一起执行fork之后的代码,且二者之间是独立的,不会相互影响。

父进程绝大部门东西都被子进程继承,代码也是,但是在执行的过程中,父进程的PCB中存在一个pc指针,记录着下一条指定的地址,当父进程执行到fork的时候,pc指针也只想fork的下一条指令,子进程也继承了pc指针的虚拟地址,本来子进程全部继承了父亲的共享代码,但是此时pc也是指向fork的下一条指令,所以父子进程都从fork之后开始执行。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  pid_t ret = fork();
  
  if (ret < 0)
  {
    perror("fork");
    return 1;
  }
  else if (ret == 0)// 子进程
  {
    printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
    sleep(1);
  }
  else if (ret > 0)// 父进程
  {
    printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
    sleep(1);
  }

  sleep(1);

  return 0;
}
父子进程关系

使用fork函数得到的子进程是父进程的一个复制品,每个进程都有自己的进程控制块PCB,再这个PCB中子进程从父进程中继承了整个进程的地址空间:包括进程上下文,进程堆栈,打开的文件描述符,信息控制设定,进程优先级,进程组号等等,但是进程的地址空间都是虚拟空间,子进程PCB继承的都是虚拟地址。

写时拷贝

通常情况下,父子进程共享一份代码,并且数据都是共享的,当任意一方试图写入更改数据的时候,那么这一份便要以写时拷贝的方式各自私有一份副本。

从图中可以看出,发生写时拷贝后,修改方将改变页表中对该份数据的映射关系,父子进程各自私有那一份数据,且权限由只读变成了只写,虚拟地址没有改变,改变的是物理内存页的物理地址。(涉及到虚拟地址,可以看我上面发的文章)

问题思考:

1.为什么代码要共享?

代码是不可以被修改的,所以各自私有很浪费空间,大多数情况下是共享的,但要注意的是,代码在特殊情况下也是会发生写时拷贝的,也就是进程的程序替换(后面会单独介绍)。

2.写实拷贝的作用?

可以减少空间的浪费,在双方都不对数据或代码进行修改的情况下,各自私有一根数据和代码是浪费空间的。 维护进程之间的独立性,虽然父子进程共享一份数据,但是父子中有一方对数据进行修改,那么久拷贝该份数据到给修改方,改变修改方中页表对这份数据的映射关系,然后对数据进行修改,这样不管哪一方对数据进行修改都不会影响另一方,这样就做到了独立性。

3.写时拷贝是对所有数据进行拷贝吗?

答案是否定的。如果没有修改的数据进行拷贝,那么这样还是会造成空间浪费的,没有被修改的数据还是可以共享的,我们只需要将修改的那份数据进行写时拷贝即可。

理论还是太枯燥,上代码!

代码1:栈区局部变量

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
   int var = 88;
   //创建一个子进程
   pid_t ret = fork();
   if (ret < 0)
    {
        perror("fork");
        return 1;
    }
   else if (ret == 0)// 子进程
    {
        sleep(1);
        printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
        printf("子进程睡醒之后 var = %d\n",var);
    }
        else if (ret > 0)// 父进程
   {
        printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
        printf("父进程之前 var =%d\n", var);
        var++;
        printf("父进程之后 var =%d\n", var);
   }
        sleep(1);
        return 0; 
 }

运行结果:

读时共享,写时拷贝。这里的父进程一开始时共享var的数据给子进程,但是此时子进程睡了一秒,就执行父进程,父进程中var的值被改变,此时写时拷贝,var会拷贝一份到子进程当中,所以父进程修改var的值不会影响到子进程中var的值。这里的局部变量在栈区。

代码2:全局变量

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int var = 88;
int main()
{
   //创建一个子进程
   pid_t ret = fork();
   if (ret < 0)
    {
        perror("fork");
        return 1;
    }
   else if (ret == 0)// 子进程
    {
        sleep(1);
        printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
        printf("子进程睡醒之后 var = %d\n",var);
    }
        else if (ret > 0)// 父进程
   {
        printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
        printf("父进程之前 var =%d\n", var);
        var++;
        printf("父进程之后 var =%d\n", var);
   }
        sleep(1);
        return 0; 
 }

运行结果:

子进程var值也不会受到影响,遵循读时共享,写时拷贝的原则。

总结:

父子进程由独立的数据段、堆、栈、共享代码段(每个进程都有属于自己的PCB)。

Linux中每个进程都有4G的虚拟地址空间(独立的3G用户空间和共享的1G内核空间),fork创建的子进程也不例外。

(1)1G内核空间既然是所有进程共享,因此fork创建的子进程自然也将有用;

(2)3G的用户空间是从父进程而来。

fork创建子进程时继承了父进程的数据段、代码段、栈、堆,值得注意的是父进程继承来的是虚拟地址空间,进程上下文,打开的文件描述符,信息控制设定,进程优先级,进程组号,同时也复制了页表(没有复制物理块)。因此,此时父子进程拥有相同的虚拟空间,映射的物理内存也是一致的。(独立的虚拟地址空间,共享父进程的物理内存)。

由于父进程和子进程共享物理页面,内核将其标记为“只读”,父子双方均无法对其修改。无论父子进程尝试对共享的页面执行写操作,就产生一个错误,这时内核就把这个页复制到一个新的页面给这个进程,并把原来的只读页面标志为可写,留给另外一个进程使用----写时复制技术。

内核在子进程分配物理内存的时候,并没有将代码段对应的数据另外复制一份给子进程,最终父子进程映射的时同一块物理内存。

进程终止

可以通过echo$?查看进程退出码

exit函数和return函数的区别 main函数结束的时候也会隐式的调用exit函数。exit函数运行的时候首先会执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所有的输出流,关闭所有打开的流并且关闭通过标准IO函数创建的临时文件。 exit时结束一个进程,他将删除进程使用的内存空间,同时把错误信息返回父进程;而return是返回函数值(return所在的函数框内)并且退出函数。通常情况:exit(0)表示程序正常, exit(1)和exit(-1)表示程序异常退出,exit(2)表示表示系统找不到指定的文件。在整个程序中,只要调用exit就结束(当前进程或者在main时候为整个程序)。return也是如此,如图return在main函数中,那么结束的就是整个进程。return是函数的结束,exit是进程的结束。 return是语言级别的,它表示了调用堆栈的返回;return( )是当前函数返回,当然如果是在主函数main, 自然也就结束当前进程了,如果不是,那就是退回上一层调用。在多个进程时。如果有时要检测上个进程是否正常退出。就要用到上个进程的返回值,依次类推。而exit是系统调用级别的,它表示了一个进程的结束。 exit函数是退出应用程序,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息。 在main函数中exit(0)等价于return 0。 1.return函数返回退出码

main函数退出的时候,return的返回值就是进程的退出码。0在函数的设计中,一般代表是正确而非0就是错误。

2.调用exit函数

void exit(int status);

功能:结束当前正在执行的进程。

参数:返回给父进程的参数,根据需要填写。

在任意位置调用,都会使得进程退出,调用之后会执行执行用户通过 atexit或on_exit定义的清理函数,还会 关闭所有打开的流,所有的缓存数据均被写入。

int main()
{
  cout << "12345";
  sleep(3);
  exit(0);// 退出进程前前会执行用户定义的清理函数,且刷新缓冲区
  return 0;
}//输出12345
3.调用_exit函数

exit()和_exit()函数功能和用法都是一样的,但是区别就在于exit()函数是标准库函数,而__exit函数是系统调用。

在Linux的标准函数库中,有一套称做“高级I/O”的函数,我们熟知的printf(),fopen(),fread(),fwrite()都在此列,它们也被称作缓冲IO (buffered IO)",其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符\n和文件结束EOF),再将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用exit()函数。
exit()作为库函数,封装的比较完善,exit将终止调用的进程,在退出程序之前,所有文件关闭,缓冲区刷新(输出内容),将刷新定义,并且调用所有已刷新的“出口函数”,在执行完清理工作之后,会调用_exit来终止进程。 _exit()调用,但是不关闭文件,不刷新缓冲区,也不调用出口函数。
int main()
{
  cout << "12345";
  sleep(3);
   _exit(0);// 直接退出进程,不刷新缓冲区
  return 0;
}//不输出12345
4.异常终止 ctrl+C终止前台进程 kill发生9号信号杀死进程 进程等待

进程等待的必要性:

在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等,这就是在执行exit时候执行的工作。但是仍然会保留一定的信息,这些信息主要指的是进程控制块PCB的信息(包括进程号,退出状态,运行事件等),而这些信息需要父进程调用wait或者waitpid函数得到他的退出状态同时彻底清理掉这个进程残留的信息。

子进程必须要比父进程先退出,否则会变成孤儿孤儿进程 父进程必须读取子进程的退出状态,回收子进程的资源。如果父进程不读取子进程退出状态,还不会释放子进程资源,那么子进程将处于僵死状态,会造成内存泄漏 父进程派给子进程的任务完成的如何,得知子进程执行结果 wait方法

*pid_wait(int status);

功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源。

参数:status进程退出时候的状态。

返回值:成功:返回结束子进程的进程号。失败:-1.

注意以下几点:

调用wait会阻塞当前的进程,直到任意一个子进程退出或者收到一个不能忽视的信号才能被唤醒。 若调用进程没有子进程,该函数立刻返回;若它的子进程已经结束,该函数同样会立刻返回,并且会回收那个早已经结束进程的资源。 如果参数status的值不是NULL,wait就会把子进程退出时候的状态取出来并存入,这是一个整数值,指出了子进程是正常退出还是被非正常结束。

演示:

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

int main()
{
	pid_t ret= fork();
	if (ret< 0){
	  cerr << "fork error" << endl;
	}
	else if (ret== 0){
	  // child
	  int count = 5;
	  while (count){
		printf("child[%d]:I am running... count:%d\n", getpid(), count--);
		sleep(1);
	  }
	  exit(1);
	}
	// parent
	printf("father begins waiting...\n");
	sleep(10);
	pid_t id = wait(NULL);// 不关心子进程退出状态
	
	printf("father finish waiting...\n");
	if (id > 0){ 
	  printf("child success exited\n"); 
	} else{
	  printf("child exit failed\n"); 
	} 
	//父进程再活5秒 
	sleep(5);
	return 0;
}

由运行结果可以看出,父进程一只等待子进程结束,等待的时候子进程变成僵尸进程,等父进程彻底释放资源,子进程的状态由僵尸变成死亡状态。

waitpid方法

*pid_t waitpid(pid_t pid, int status , int options);

功能:等待子进程结束,如果子进程终止,此函数就会回收子进程资源。

参数:

pid:参数pid有以下几种类型:

​ pid>0 等待进程ID等于pid的子进程结束。

​ pid=0 等待同一个进程组中的任何子进程,如果子进程已经进入了别的进程组,waitpid不会等待它。

​ pid=-1 等待任意子进程,此时waitpid和wait的作用是一样的。

​ pid<-1 等待指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

options:options提供了一些额外的选项来控制waitpid()

​ 0:通wait(),阻塞父进程,等待子进程退出。

​ WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID(可以进行基于阻塞等待的轮询访问)。

​ WUNTRACED:如果子进程暂停了此函数立马返回,并且不予理会子进程的结束状态(很少调用)。

返回值:

​ waitpid有三种情况:

(1)正常返回的时候,waitpid返回收集到的已回收子进程的进程的进程号。

(2)如果设置了WNOHANG,而调用中发现了没有已经退出的子进程可以等待,返回0。

(3)如果调用中出错,返回-1,此时errno会被设置成相应的值来指示错误所在。

代码示例:

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

int main()
{
	pid_t ret= fork();
	if (ret< 0){
	  cerr << "fork error" << endl;
	}
	else if (ret== 0){
	  // child
	  int count = 5;
	  while (count){
		printf("child[%d]:I am running... count:%d\n", getpid(), count--);
		sleep(1);
	  }
	  exit(1);
	}
	// parent
	printf("father begins waiting...\n");
	sleep(10);
	pid_t id = waitpid(-1, NULL, 0);// 不关心子进程退出状态,以阻塞方式等待
	
	printf("father finish waiting...\n");
	if (id > 0){ 
	  printf("child success exited\n"); 
	} else{
	  printf("child exit failed\n"); 
	} 
	//父进程再活5秒 
	sleep(5);
	return 0;
}
获取子进程的status wait和waitpid中都有一个status参数,该参数是一个输出型参数,由操作系统来填充 如果该参数给NULL,那么代表不关心子进程的退出信息

status的几种状态:(我们只研究status的低16位)

看图可以知道,低7位代表的是终止信号,第8位时core dump标志,高八位是进程退出码(只有正常退出是这个退出码才有意义) status的0-6位和8-15位有不同的意义。我们要先读取低7位的内容,如果是0,说明进程正常退出,那就获取高8位的内容,也就是进程退出码;如果不是0,那就说明进程是异常退出,此时不需要获取高八位的内容,此时的退出码是没有意义的。

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

int main()
{
	pid_t ret = fork();
	if (ret < 0){
	  cerr << "fork error" << endl;
	}
	else if (ret == 0){
	  // child
	  int count = 5;
	  while (count){
	    printf("child[%d]:I am running... count:%d\n", getpid(), count--);
	    sleep(1);
	  }
	
	  exit(1);
	}
	// parent
	printf("father begins waiting...\n");
	
	int status;
	pid_t id = wait(&status);// 从status中获取子进程退出的状态信息
	printf("father finish waiting...\n");
	
	if (id > 0 && (status&0x7f) == 0){
	  // 正常退出
	  printf("child success exited, exit code is:%d\n", (status>>8)&0xff);
	}
	else if (id > 0){
	  // 异常退出
	  printf("child exit failed,core dump is:%d,exit singal is:%d\n", (status&(1<<7)), status&0x7f);
	}
	else{
	  printf("father wait failed\n");
	}
	if (id > 0){ 
	  printf("child success exited\n"); 
	} else{
	  printf("child exit failed\n"); 
	} 
 	return 0;
}

运行结果如下:

阻塞等待和非阻塞等待

操控者: 操作系统阻塞的本质: 父进程从运行队列放入到了等待队列,也就是把父进程的PCB由R状态变成S状态,这段时间不可被CPU调度器调度等待结束的本质: 父进程从等待队列放入到了运行队列,也就是把父进程的PCB由S状态变成R状态,可以由CPU调度器调度

阻塞等待: 父进程一直等待子进程退出,期间不干任何事情

示例:

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
  pid_t id = fork();
  if (id < 0){
    cerr << "fork error" << endl;
  }
  else if (id == 0){
    // child
    int count = 5;
    while (count){
      printf("child[%d]:I am running... count:%d\n", getpid(), count--);
      sleep(1);
    }
    exit(0);
  }
  
  // 阻塞等待
  // parent
  printf("father begins waiting...\n");
  int status;
  pid_t ret = waitpid(id, &status, 0);
  printf("father finish waiting...\n");

  if (id > 0 && WIFEXITED(status)){
    // 正常退出
    printf("child success exited, exit code is:%d\n", WEXITSTATUS(status));
  }
  else if (id > 0){
    // 异常退出
    printf("child exit failed,core dump is:%d,exit singal is:%d\n", (status&(1<<7)), status&0x7f);
  }
  else{
    printf("father wait failed\n");
  }
}

运行结果如下:

非阻塞等待: 父进程不断检测子进程的退出状态,期间会干其他事情(基于阻塞的轮询等待)

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

int main()
{
  pid_t id = fork();
  if (id < 0){
    cerr << "fork error" << endl;
  }
  else if (id == 0){
    // child
    int count = 5;
    while (count){
      printf("child[%d]:I am running... count:%d\n", getpid(), count--);
      sleep(1);
    }
    exit(0);
  }
  // 基于阻塞的轮询等待
  // parent
  while (1){
    int status;
    pid_t ret = waitpid(-1, &status, WNOHANG);
    if (ret == 0){
      // 子进程还未结束
      printf("father is running...\n");
      sleep(1);
    }
    else if (ret > 0){
      // 子进程退出
      if (WIFEXITED(status)){
        // 正常退出
        printf("child success exited, exit code is:%d\n", WEXITSTATUS(status));
      }
      else{
        // 异常退出
        printf("child exited error,exit singal is:%d", status&0x7f);
      }
      break;
    }
    else{
      printf("wait child failed\n");
      break;
    }
  }
  
}

运行结果如下: