【Linux】进程控制2——进程等待(wait&&waitpid)

1. 进程等待的必要性——回收子进程

  1. 子进程退出后,父进程如果不管不顾,就可能造成"僵尸进程”的问题,进而造成内存泄漏
  2. 另外,进程一旦变成僵尸状态(Z状态),那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  3. 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。

父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

什么是进程等待? 

通过系统调用wait/waitpid,来进行对子进程进行状态检测与回收功能。 

我们先来创建一个僵尸进程

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork\n");
        return 1;
    }
    else if(id==0)
    {
    //child
    int cnt=5;
    while(cnt)
        {
            printf("I am child ,pid:%d,ppid :%d",getpid(),getppid());
            cnt--;
            sleep(1);
        }
        exit(0);//子进程在这里退出
    }
    else
    {
    //parent
      while(1)
        {
            printf("I am father,pid:%d,ppid :%d",getpid(),getppid());
            sleep(1);
        }
    }
}

监视脚本 

while :; do ps ajx | head -1 && ps ajx | grep mytest | grep -v grep; sleep 1; echo "______________";done

 

这个父进程的父进程19897是谁啊?

bash进程啊

 我们看到它这个子进程退出之后,父进程还没有退出,子进程一直处于僵尸进程 

2 .wait系统调用接口

在Linux中,wait函数是一个系统调用用于等待子进程的终止并获取其终止状态该函数的原型如下所示:

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

pid_t wait(int*status);

wait函数返回值(PID)有以下几种可能的取值:

  • 如果成功等待到一个子进程的终止,返回子进程的PID。
  • 如果调用进程没有子进程,wait函数会返回-1
  • 如果调用进程被一个信号中断,wait函数会返回-1

参数:(我们这里先不讲)

  •  输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

        函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,

  1. 如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回
  2. 如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

我们来看看两种情况来理解这个函数 

2.1.子进程等待父进程回收

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

int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork\n");
        return 1;
    }
    else if(id==0)
    {
    //child
    int cnt=5;
    while(cnt)
        {
            printf("I am child ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(0);//子进程在这里退出
    }
    else
    {
    //parent
      int cnt=10;
      while(cnt)
        {
            printf("I am father ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        //目前进程等待是必要的——回收僵尸进程
        pid_t ret = wait(NULL);//我们先这么用着
        if(ret == id)//id是父进程fork的返回值——子进程的PID
        {
            printf("wait success, ret : %d\n",ret);
        }
        sleep(5);
    }
}

 当子进程终止后,wait函数会返回子进程的进程ID(PID) 

我们分析一下程序:  前5秒的时候父子进程都在打印,5秒之后子进程先退,父进程 没有进行wait,还在打印,10秒之后父进程回收子进程,回收完后父进程还要继续等待5秒

我们看看运行结果

 我们看看隔壁的监控情况 

很符合我们的预期啊!!!

wait一次是只能等待任意一个进程退出的,只不过我们这里只有一个子进程,所以只回收了这个子进程

 我们来看看怎么回收多个进程的

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

#define N 10

void RunChild()
{
    int cnt =5;
    while(cnt)
    {   
        printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());
       cnt--;
        sleep(1);
    }
}
        
int main()
{
    int i;
    for(i=0;i<N;++i)
    {
    pid_t id =fork();
    if(id==0)
        {
     RunChild();
        exit(0);//每个子进程走到这里就退出了
        }
    printf("creat child :%d  sucess\n",getpid());//这句话只有父进程会执行
    }
    
    sleep(10);
    
    //等待,我们这里有10个子进程
    for(i=0;i<N;++i)
    {
    pid_t ret =wait(NULL);
    if(ret > 0)
        {
        printf("wait %d success\n",ret);
        }
    }
    sleep(5);//父进程最后等一下
}

我们看看执行情况 

 

再看看监视情况

​while :; do ps ajx | head -1 && ps ajx | grep mytest | grep -v grep; sleep 1; echo "______________";done

5个这个

 5个这个

 最后变这个

10个进程又怎么样呢?全给你回收了

2.1.2.父进程等待回收子进程

我们上面都是子进程等待父进程来回收,我们现在让父进程来等待回收子进程

我们上面的例子都是子进程会退出,然后被wait函数回收,那么子进程要是不退出呢?

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

#define N 10

void RunChild()
{
    while(1)//子进程永远不死
    {   
        printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(1);
    }
}
        
int main()
{
    int i;
    for(i=0;i<N;++i)
    {
    pid_t id =fork();
    if(id==0)
        {
     RunChild();
        exit(0);//每个子进程走到这里就退出了
        }
    printf("creat child :%d  sucess\n",getpid());//这句话只有父进程会执行
    }
    
    sleep(10);
    
    //等待,我们这里有10个子进程
    for(i=0;i<N;++i)
    {
    pid_t ret =wait(NULL);
    if(ret > 0)
        {
        printf("wait %d success\n",ret);
        }
    }
    sleep(5);//父进程最后等一下
}

运行结果

 

 监视窗台,一直都是下面这个

事实表明子进程不退的话,父进程就会在wait函数那里阻塞等待

 阻塞等待:如果子进程不退出,父进程默认在wait这个系统调用的地方不返回,这种状态叫阻塞状态

综上, 我们可以得出

来总结一番

  当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程.

  wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.

  如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID为1)继承,当子进程终止时,init进程捕获这个状态.

3. waitpid方法

waitpid函数是Linux中用于等待指定子进程终止的系统调用

        与wait函数类似,waitpid函数也可以用于获取子进程的终止状态。

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

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

        函数功能是:父进程一旦调用了waitpid就立即阻塞自己,由waitpid自动分析是否当前进程的某个子进程已经退出,

  1. 如果让它找到了这样一个已经变成僵尸的子进程,waitpid就会收集这个子进程的信息,并把它彻底销毁后返回
  2. 如果没有找到这样一个子进程,waitpid就会一直阻塞在这里,直到有一个出现为止。 

从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。

下面我们就来详细介绍一下这两个参数:

pid:

从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。

  1.   pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
  2.   pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
  3.   pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
  4.   pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

options:

options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:

  ret = waitpid(-1,  NULL,  WNOHANG | WUNTRACED);

如果我们不想使用它们,也可以把options设为0,如:

     ret = waitpid(-1,  NULL,  0);
  • 如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。
  • 而WUNTRACED参数,由于涉及到一些跟踪调试方面的知识,加之极少用到,这里就不多费笔墨了,有兴趣的读者可以自行查阅相关材料。

看到这里,聪明的读者可能已经看出端倪了:wait不就是经过包装的waitpid吗?

没错,察看<内核源码目录>/include/unistd.h文件349-352行就会发现以下程序段:

static inline pid_t wait(int * wait_stat)

{

    return waitpid(-1,wait_stat,0);

}

返回值和错误

waitpid的返回值比wait稍微复杂一些,一共有3种情况:

  1. 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
  2. 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  3. 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

我们先看个简单的例子

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

int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork\n");
        return 1;
    }
    else if(id==0)
    {
    //child
    int cnt=5;
    while(cnt)
        {
            printf("I am child ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(0);//子进程在这里退出
    }
    else
    {
    //parent
      int cnt=10;
      while(cnt)
        {
            printf("I am father ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        //等价于pid_t ret = wait(NULL);
        pid_t ret =waitpid(-1,NULL,0);//我们先这么用着
        if(ret == id)//id是父进程fork的返回值——子进程的PID
        {
            printf("wait success, ret : %d\n",ret);
        }
        sleep(5);
    }
}

 我们看看运行情况

监视情况

5个这个

5个这个

 5个这个

这个跟wait几乎一模一样

我们也验证了返回值

 

我们现在不来谈讨僵尸进程,我们现在来讨论子进程的退出码,这个是需要status参数的

wait和waitpid都有这个status参数

4..参数status——获得子进程退出情况

        当子进程终止后,wait函数会返回子进程的进程ID(PID),并将子进程的终止状态存储在指针status指向的变量中。

  • status参数是一个指向整型变量的指针,用于存储子进程的终止状态。
  • 通过status可以获取子进程的退出状态、终止信号等信息
  • 如果不关心终止状态,可以将status设置为NULL。

                               status是个输出型参数

什么叫输出型参数?就是函数调用结束以后,会将参数的值写到这个变量里。

        换句话说,这个status是用来接收的,本质上不是用来传参的。

        我们把我们的status定义好了之后,放到该函数里,作为参数传递过去,函数调用完后,操作系统就会把status的值自动填充好,然后还给我们。实现的原理很简单,因为其用的是指针,传递的是变量的地址。倘若我们不关心这个status状态,那么直接传递NULL即可。

        但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:

pid = wait(NULL);

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

我们现在来了解一下这个参数的用法

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

int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork\n");
        return 1;
    }
    else if(id==0)
    {
    //child
    int cnt=5;
    while(cnt)
        {
            printf("I am child ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(1);//子进程在这里退出,注意是1
    }
    else
    {
    //parent
      int cnt=10;
      while(cnt)
        {
            printf("I am father ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        int status=0;//定义一个整型变量

        //等价于pid_t ret = wait(NULL);
        pid_t ret =waitpid(id,&status,0);//注意这个id
        
        if(ret == id)//id是父进程fork的返回值——子进程的PID
        {
            printf("wait success, ret : %d,status: %d\n",ret,status);//把status打印出来看看
        }
        sleep(5);
    }
}

我们看看运行情况

 我们看看监控情况

5个这个

5个这个

5个这个

注意:我们说的status,不是参数status,而是status指向的那个整数 

为什么status是256? 我们先不讲,先回答几个问题

 1.子进程在这里有几种退出场景?

2.父进程希望获得子进程的哪些信息?

  • 子进程代码是否异常呢
  • 没有异常,结果对吗?不对是因为什么?
  • 不同的退出码对应不同的错误原因

一个参数要记录这么多东西,我靠??? 吃得消吗?

正因为这个status这个先天苦命圣体,所以它得被拆分成几个部分,分别记录不同的信息

注意:我们说的status,不是参数status,而是status指向的那个整数  

2.2.1.status 参数是位图结构 

这个小小的数字,怎么存下这么多消息?

它虽然是一个 int 型整数,但是不能简单地将其看作整型,而是被当作一个 位图结构 看待。

不过,关于 status 我们只需要关心该整数的 低 16 个比特位!

int有32个比特位,我们不必去关心它的高 16 位,因为凭借低 16 位就足以判断了。

然而,整数的低 16 位,其中又可以分为 最低八位 和 次低八位(具体细节看图):

我们之研究status的低16比特位

  1. 最低八位(包括core dump)存储的是终止信号(是否出异常的问题),本质就是该进程收到信号
  2. 次低八位存储的是退出状态(退出状态)

 2.2.2、次低八位:存储了子进程退出码

重点:通过提取 status 的次低八位(倒数第9-16位),就可以拿到子进程的退出码。 

2.2.3、 最低七位:提取子进程的退出信号

重点:通过提取 status 的最低七位,就可以拿到子进程的退出信号。

我们的 status 的最低八位用于表示处理异常的地方,其中有 1 位是 core dump,我们等到信号那节再讲。

除去 core dump,剩余七位用于进程中的退出信号,这就是 最低七位。

进程退出,如果异常退出,是因为这个进程收到了特定的信号。

退出状态,没有0号信号,进程信号是0就代表进程没有异常 

有人就问了,搞这么复杂,能不能直接使用几个全局变量来实现这一功能?

  • 这是不行的,因为父子进程具有独立性,父子进程都会修改这个全局变量,父进程拿不到子进程的返回信息

 我们用代码

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

int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork\n");
        return 1;
    }
    else if(id==0)
    {
    //child
    int cnt=5;
    while(cnt)
        {
            printf("I am child ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(1);//子进程在这里退出,注意是1
    }
    else
    {
    //parent
      int cnt=10;
      while(cnt)
        {
            printf("I am father ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        int status=0;//定义一个整型变量

        //等价于pid_t ret = wait(NULL);
        pid_t ret =waitpid(id,&status,0);//注意这个id
        
        if(ret == id)//id是父进程fork的返回值——子进程的PID
        {
            //7F:0111 1111,status&7F就是status的倒数第七位
            printf("wait success, ret : %d,exit sign: %d,exit code:%d\n",
                    ret,status&0x7F,(status>>8)&0xFF);
        }
        sleep(5);
    }
}

我知道大家对status&0x7F和(status>>8)&0xFF很好奇

 这个是退出信号的

status是一个32位(比特位)的整数,并且我们执行status & 0x7F操作时,我们实际上是在从status中提取其最低的7位。

  • 这里,0x7F在二进制中表示为01111111,它是一个8位的数,但只有最低7位是1。
  • 按位与操作(&)的特点是,只有当两个操作数的相应位都为1时,结果的该位才为1,否则为0。
  • 因此,status & 0x7F会保留status中最低7位的值,而将高于这7位的所有位都清零。

这个是退出码的

表达式 (status >> 8) & 0xFF 是一种位操作,这个表达式具体做了两件事情:

  1. 右移操作 (>>)status >> 8 这部分将 status 的二进制表示向右移动8位。这意味着原来 status 中的最高8位(从左边数起)会被丢弃,而原来的低8位及其以下的所有位都会向左移动8位,原本不存在的位(即新出现的最高位)会被填充为0

  2. 按位与操作 (&)(status >> 8) & 0xFF 这部分将上一步得到的结果与 0xFF 进行按位与操作。0xFF 在二进制中表示为 11111111,这是一个8位的数,所有位都是1。按位与操作的特点是,只有当两个相应的位都为1时,结果的该位才为1,否则为0。因此,与 0xFF 进行按位与操作实际上就是保留操作数的低8位,而将高于这8位的所有位都清零。

综上所述(status >> 8) & 0xFF 这个表达式的目的是从 status 中提取出右移8位之后的新低8位(即原 status 的第9位到第16位,如果我们从0开始计数的话)。

​exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F;      //退出信号

 我们运行一下

退出信号是0,退出码是1,这个1从哪里来? 

 

这个exit(1)就是源头

 我们来让子进程出一次异常,看看会是什么情况?

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

int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork\n");
        return 1;
    }
    else if(id==0)
    {
    //child
    int cnt=5;
    while(cnt)
        {
            printf("I am child ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        int a=10; a/=0;//注意这里
        exit(1);//子进程在这里退出,注意是1
    }
    else
    {
    //parent
      int cnt=10;
      while(cnt)
        {
            printf("I am father ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        int status=0;//定义一个整型变量

        //等价于pid_t ret = wait(NULL);
        pid_t ret =waitpid(id,&status,0);//注意这个id
        
        if(ret == id)//id是父进程fork的返回值——子进程的PID
        {
            //7F:0111 1111,status&7F就是status的倒数第七位
            printf("wait success, ret : %d,exit sign: %d,exit code:%d\n",
                    ret,status&0x7F,(status>>8)&0xFF);
        }
        sleep(5);
    }
}

其实子进程执行一次处0错误,就立马奔溃了

 异常信号是8,8是什么?

8就是浮点数异常

为什么要通过系统调用来获得子进程的退出情况?父进程不能直接获取吗?

父,子进程在操作系统中是相互独立的,都有自己的数据结构

        我们讲个故事,小张是A学校的,他的编程能力很强,隔壁B学校想让小张替他们学校去参加一次比赛,那就必须和A学校的校长沟通,才能让小张去参加

那么父进程就相当于B学校,子进程是小张

进程在什么时候会等待失败?

这个情况一般是等待的进程不是自己的子进程

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

int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork\n");
        return 1;
    }
    else if(id==0)
    {
    //child
    int cnt=5;
    while(cnt)
        {
            printf("I am child ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(1);//子进程在这里退出,注意是1
    }
    else
    {
    //parent
      int cnt=10;
      while(cnt)
        {
            printf("I am father ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        int status=0;//定义一个整型变量

        //等价于pid_t ret = wait(NULL);
        pid_t ret =waitpid(id+4,&status,0);//注意这个id
        
        if(ret == id)//id是父进程fork的返回值——子进程的PID
        {
            //7F:0111 1111,status&7F就是status的倒数第七位
            printf("wait success, ret : %d,exit sign: %d,exit code:%d\n",
                    ret,status&0x7F,(status>>8)&0xFF);
        }
        else
        {
        printf("wait filled\n");
        }
        sleep(5);
    }
}

很明显了,等待失败了 

4.1、进程退出的宏

我们在上面使用status&0x7F和(status>>8)&0xFF来获取进程的退出信息,但是并不需要这么麻烦

我们今天写的代码,是通过位操作去截 status 得到退出码和退出信号的。

实际上,你也可以不用位操作,因为  已经给我们提供了一些宏供我们直接调用。

它们是 WEXITSTATUS 和 WIFEXITED,在这之前,我们再思考一个问题:

思考:一个进程退出时,可以拿到退出码和退出信号,我们先看谁?

        一旦程序发现异常,我们只关心退出信号,退出码没有任何意义。

        所以,我们先关注退出信号,如果有异常了我们再去关注退出码

  如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中, 这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。

        由于这些信息 被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:

  • 1,WIFEXITED(status) :用于查看进程是否正常退出,用来判断子进程是否为正常退出的,如果是正常终止的子进程返回状态,则为真。

        (请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数——指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了。)

  • 2, WEXITSTATUS(status): 这个宏用来查看进程的退出码,,若非 0,提取子进程退出码。

当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。

        请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,WEXITSTATUS(status)就毫无意义。

所以我们先判断程序也没有出异常,再看退出码,如果出现了异常,退出码就不看了

就先看WIFEXITED(status)!=0,如何打印 WEXITSTATUS(status)

我们看个例子 

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

int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork\n");
        return 1;
    }
    else if(id==0)
    {
    //child
    int cnt=3;
    while(cnt)
        {
            printf("I am child ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(1);//子进程在这里退出,注意是1
    }
    else
    {
    //parent
      int cnt=5;
      while(cnt)
        {
            printf("I am father ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        int status=0;//定义一个整型变量

        //等价于pid_t ret = wait(NULL);
        pid_t ret =waitpid(id,&status,0);//注意这个id
        
        if(ret == id)//id是父进程fork的返回值——子进程的PID
        {
            if(WIFEXITED(status)!=0)
            {
            printf("child is run successfull,exit code:%d\n",WEXITSTATUS(status));
            }
        }
        else
        {
        printf("wait filled\n");
        }
        sleep(3);
    }
}

​

完美啊!!!!!还不用起来??

我们再来看看多个子进程来

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

#define N 10

void RunChild()
{
    int cnt =5;
    while(cnt)
    {   
        printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());
       cnt--;
        sleep(1);
    }
}
        
int main()
{
    int i;
    for(i=0;i<N;++i)
    {
    pid_t id =fork();
    if(id==0)
        {
     RunChild();
        exit(i);//注意这里
        }
    printf("creat child :%d  sucess\n",getpid());//这句话只有父进程会执行
    }
    
    sleep(10);
    
    //等待,我们这里有10个子进程
    for(i=0;i<N;++i)
    {
    int status =0;
    pid_t ret =waitpid(-1,&status,0);
    if(ret > 0)
        {
        printf("wait %d success,exit code: %d\n",ret,WEXITSTATUS(status));
        }
    }
    sleep(5);//父进程最后等一下
}

这就是进程等待,必须通过父进程来进行等待

5.option参数

当前设置等待的方式——有两种主要的等待方式

  • 参数0——阻塞等待
  • 非阻塞轮询

5.1.阻塞等待——对应参数0

  •  如果子进程没有退出,而父进程在进行执行waitpid进行等待,阻塞等待。
  • 大部分IO类的函数例如scanf各种各样的接口,只要涉及IO的或多或少会可能出现阻塞的状态。
  • 现在所用的大部分接口都是阻塞接口(逻辑简单,容易实现)
  •  阻塞等待(Blocking Wait)在编程中通常指的是一个线程或进程在等待某个条件满足或某个操作完成之前,会暂停执行其他任务,处于等待状态。这种状态会一直持续,直到等待的条件满足或操作完成,线程或进程才会继续执行后续的任务。在Java中,阻塞等待常用于多线程编程中,用于线程之间的同步和通信。

5.2.阻塞等待和非阻塞等待的区分

场景:张三找李四求助帮他复习期末考试。

张三在李四的楼下等待李四就绪。

非阻塞等待

张三每隔几分钟就给李四打电话询问他是否就绪了,张三在没有打电话的时间看书/游戏/抖音

  1. 就绪的过程本质就是非阻塞等待。
  2. 张三非阻塞等待李四过程 == 函数调用
  3. 张三给李四打电话 == 函数传参
  4. 李四说等着没好 == 函数的返回值
  5. 每次函数调用的本质是检测李四的状态(是否就绪)
  6. 立刻有返回值,多次等待,多次返回。
  • pid_ t waitpid(pid_t pid, int *status, WNOHANG);
  • pit_t == 0 :检测是成功的,只不过子进程还没退出,需要你下一次进行重复等待。
  • pit_t > 0 :等待成功,子进程退出了,并且父进程回收成功。
  • pit_t < 0 :等待失败。

阻塞等待

        张三一直给李四打着电话,直到李四就绪,期间张三一直等待李四就绪,不敢别的事情。一直检测李四的状态(不就绪,就不返回)
      一直等待。直到子进程终止才返回。

  1. pid_ t waitpid(pid_t pid, int *status, 0);
  2. pit_t > 0 :等待成功,子进程退出了,并且父进程回收成功。
  3. pit_t < 0 :等待失败。

5.3. 非阻塞轮询——对应参数WNOHANG

在子进程运行期间,父进程除了等待子进程或者是休眠,能不能干点其他的事情❓

        当然可以,在父进程等待,阻塞状态。可以通过设置options来让父进程干点事情。不阻塞等待而是非阻塞等待。

什么是非阻塞等待呢?用代码该怎么去实现呢?

非阻塞等待(Non-blocking Wait)则与阻塞等待相反。

  • 当线程或进程在等待某个条件满足或某个操作完成时,它不会暂停执行其他任务,而是会继续执行后续的任务。
  • 也就是说,即使等待的条件还没有满足或操作还没有完成,线程或进程也不会被阻塞,而是会继续执行其他的操作。

        通过设置options的宏值WNOHANG(wait no hang 等待没有阻塞 = 非阻塞等待)

在计算机中,"HANG" 通常指的是程序或系统出现无响应或停顿的状态,也就是常说的“卡住”或“死机”。

        当程序或系统由于某种原因(如资源锁定、死循环、死锁或外部系统交互问题等)而无法继续正常执行时,就可能会出现"HANG"的情况。这种情况下,用户可能无法与程序或系统进行交互,需要等待程序或系统恢复正常或进行重启操作。

        另外,在一些特定的语境下,"HANG" 也可能被用来描述服务器或数据库的某些服务出现故障或无法访问的情况,这也可以被视为一种"宕机"现象。在这种情况下,"HANG" 指的是服务器或数据库的服务因为某种原因而停止响应或无法提供服务。

具体操作

  1. options这个参数只要一设置WNOHANG就会出现非阻塞等待。
  2. 设置waitpid的WNOHANG本质上是检测一次进程的状态变化。
  3. 调用一次waipid就检测一次。每次调用都是检测,多次调用多次检测。
  4. 非阻塞等待调用多次waitpid,调用waitpid检测是否退出等待过程无问题,只是子进程还未终止,需要等待下次等待。

综上:非阻塞等待的时候 + 循环 = 非阻塞轮询

看个例子 

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

int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork\n");
        return 1;
    }
    else if(id==0)
    {
    //child
    int cnt=5;
    while(cnt)
        {
            printf("I am child ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(1);//子进程在这里退出,注意是1
    }
    else
   {
    //parent
        int status=0;//定义一个整型变量

    while(1)
    {   //等价于pid_t ret = wait(NULL);
        pid_t ret =waitpid(id,&status,WNOHANG);//注意这个id
        
        if(ret >0)//等待成功
        {
            if(WIFEXITED(status)!=0)
            {
            printf("child is run successfull,exit code:%d\n",WEXITSTATUS(status));
            }
            break;//成功了就撤啊
        }
        else if(ret<0)//还需要等待
        {
        printf("wait filled\n");
          break;//失败了就赶紧跑
        }
        else
        {
        printf("child is no exit,I am waitting...\n");
        sleep(1);//父进程再等等子进程
        }
    }
        sleep(3);
   }
}

 

很完美吧

我们加了sleep(1)来加长父进程等待的时间。我们也可以不等待,也可以干别的事情

我们只需在waitpid返回值为0的地方设置好父进程该干的事情即可

我们来将等待换成别的任务

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

int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork\n");
        return 1;
    }
    else if(id==0)
    {
    //child
    int cnt=3;
    while(cnt)
        {
            printf("I am child ,pid:%d, ppid :%d,cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(1);//子进程在这里退出,注意是1
    }
    else
   {
    //parent
        int status=0;//定义一个整型变量

    while(1)
    {   //等价于pid_t ret = wait(NULL);
        pid_t ret =waitpid(id,&status,WNOHANG);//注意这个id
        
        if(ret >0)//等待成功
        {
            if(WIFEXITED(status)!=0)
            {
            printf("child is run successfull,exit code:%d\n",WEXITSTATUS(status));
            }
            break;//成功了就撤啊
        }
        else if(ret<0)//还需要等待
        {
        printf("wait filled\n");
          break;//失败了就赶紧跑
        }
        else//干别的事情
        {
        int i=0;
        for(;i<4;++i)
        {
        printf("Father is doing other things:%d\n",i);
        sleep(1);
        }
    }
        sleep(2);
   }
}

 

怎么样呢?

  • 49
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 27
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值