【Linux】第十八站:进程等待

文章目录

  • 一、进程等待的必要性
    • 1.进程等待是什么
    • 2.进程等待的必要性
    • 3.为什么要进程等待呢?
  • 二、进程等待的方法
    • 1.问题
    • 2.wait
    • 3.waitpid
    • 4.status的原理
    • 5.等待失败
    • 6.与status有关的两个宏
    • 7.options

一、进程等待的必要性

1.进程等待是什么

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

2.进程等待的必要性

  • 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3.为什么要进程等待呢?

答案已经在前面回答了

就是因为僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄漏问题----必须解决的

我们要通过进程等待,获得子进程的退出情况,要能够知道布置给子进程的任务,它完成的怎么样了----要么关心,也可能不关心

二、进程等待的方法

1.问题

我们使用如下代码

#include    
#include    
#include
int main()    
{    
    pid_t id = fork();    
    if(id < 0)                                                                
    {                                          
        perror("fork:");    
        return 1;    
    }         
    else if(id == 0)    
    {                     
        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     
    {                                                                               
        while(1)      
        {    
            printf("I am parent,pid:%d,ppid:%d\n",getpid(),getppid());    
            sleep(1);    
        }    
    }    
    return 0;                                                                                                                                                                                  
}

上述代码的功能不难理解。然后我们使用监控去运行一下

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

运行结果如下所示

【Linux】第十八站:进程等待_第1张图片

为了解决上面的问题,我们可以让父进程通过调用wait/waitpid进行僵尸进程的回收问题!

2.wait

我们先来看wait函数

【Linux】第十八站:进程等待_第2张图片

#include
#include
pid_t wait(int*status);

返回值:

  • 成功返回被等待进程pid,失败返回-1。

参数:

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

这个系统调用的作用就是等待一个进程,直到它的状态发生改变。

对于wait函数,我们可以看到它需要传入一个指针,但是在wait函数,我们先不关心它的这个参数。也就是我们先给他一个NULL指针

这个wait它会返回一个值,这个值就是对应等待的子进程的pid

#include                                                                                           
#include                                                                                      
#include                                                                    
#include                                                                           
#include                                                                                    
int main()                                                           
{                                                                                        
    pid_t id = fork();                                                                  
    if(id < 0)                                                                   
    {                                                                        
        perror("fork:");                                                                       
        return 1;                                                                
    }                                                                 
    else if(id == 0)                                                                        
    {                                                                   
        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                                                                        
    {        
        int cnt = 10;                                                                             
        while(cnt)                                                                  
        {                                                                         
            printf("I am parent,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);                        
            cnt--;                                                                        
            sleep(1);                                                                         
        }                                                                                          
        pid_t ret = wait(NULL);                                                                         
        if(ret == id)                                                                                  
        {                                                                                        
            printf("wait success:,ret : %d\n",ret);                        
        }                                                                      
        sleep(5);                                                                                         
    }                                                                                                                                                      
    return 0;                                                
} 

最终它的运行结果为

程序一共经历了三个5秒。第一个五秒钟,是父子进程都存在,第二个五秒,子进程进入僵尸状态。第三个五秒,子进程的僵尸状态被回收了。

【Linux】第十八站:进程等待_第3张图片

所以说,到目前为止,进程等待是必须的。因为这个进程等待最重要的作用就是回收僵尸进程

那么如果我们这个程序有很多个子进程呢?应该等待哪一个呢?其实wait是等待任意一个子进程退出。

所以如果我们要等待多个进程退出,我们要这样做

如下代码所示

#include
#include
#include
    
#define N 10    
    
void RunChild()    
{    
    int cnt = 5;    
    while(cnt)    
    {    
        printf("I am a child process, pid:%d, ppid:%d\n",getpid(),getppid());    
        sleep(1);    
        cnt--;    
    }    
}    
int main()    
{    
    for(int i = 0; i < N; i++)    
    {    
        pid_t id = fork();    
        if(id == 0)    
        {    
            RunChild();    
            exit(0);    
        }    
        printf("create child process: %d success\n",id);    
    }    
    
    sleep(10);    
    
    for(int i = 0; i < N; i++)    
    {    
        pid_t id = wait(NULL);    
        if(id > 0)    
        {    
            printf("wait %d success\n",id);                                                  
        }    
    }    
    sleep(5);
    return 0;    
}

运行结果如下

【Linux】第十八站:进程等待_第4张图片

【Linux】第十八站:进程等待_第5张图片

上面都是子进程会退出的情况,那么如果子进程都不退出呢?

即我们将上面的子进程都改为死循环

那么最终的运行结果是一个也不退出,父进程也不退出,在那里阻塞等待。

【Linux】第十八站:进程等待_第6张图片

所以这说明,如果子进程不退出,父进程默认在wait的时候,调用这个系统调用的时候,也就不返回,默认就叫做阻塞状态!

所以说阻塞不仅仅只是像我们之前要等待scanf的时候,需要等待硬件,还有可能等待软件

3.waitpid

现在我们已经知道对于子进程的僵尸进程问题是如何解决的了,就是使用wait即可。这解决了进程等待中最为重要的一点,那么如果父进程需要获得子进程的退出时的状态,得知子进程任务完成的怎么样了,那么应该如何解决呢?

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

返回值:

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

参数:

  • pid:

Pid=-1,等待任一个子进程。与wait等效。

Pid>0.等待其进程ID与pid相等的子进程。

  • status:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

  • options:
    WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

从这里的描述,我们就可以知道,wait其实就相当于waitpid的一个子集

即在这段代码中,wait与waitpid是等价的

【Linux】第十八站:进程等待_第7张图片

【Linux】第十八站:进程等待_第8张图片

在我们这个wait和waitpid的函数中,他们都有一个status,这个就是用来获取退出时的状态的。如果我们不想用,直接设置为空即可,这两个函数的这个参数是一样的

【Linux】第十八站:进程等待_第9张图片

对于这个status,它是一个输出型参数。其次,这个int是被当作几部分来使用的。

我们可以先来使用一下,为了展示出效果,我们让子进程的退出设置为1

【Linux】第十八站:进程等待_第10张图片

最终运行结果为

【Linux】第十八站:进程等待_第11张图片

我们可以发现,我们退出信息本应该是1,但是结果却是256

这里我们就有如下几个问题

  1. 子进程退出,一共会有几种退出场景呢?

对于这个问题,我们在前面已经知道了:总共三种场景

①:代码运行完毕,结果正确 ②:代码运行完毕,结果不正确 ③:代码异常终止

  1. 父进程等待,期望获得子进程退出的哪些信息呢?

①:子进程代码是否异常?②:没有异常的话,结果对吗?这里可以用exitcode退出码来获取,不对是什么原因呢?可以用不同的退出码表示原因

所以我们可以看到,这个status需要做的事情其实挺多的,所以这个变量注定了不能被看作一个,而是要划分为几个部分

【Linux】第十八站:进程等待_第12张图片

对于这个status它一共有32位,但是我们目前只考虑它的低16位

低八位,用于表示是否出异常了。其中有一共比特位是core dump标志位,我们后面再谈

我们在前面说过,一共进程异常了,本质就是该进程收到了一个信号。

对于它的次低八位,代表的就是退出的状态,即退出码

【Linux】第十八站:进程等待_第13张图片

比如我们刚刚所说的明明是退出码是1,但是打印结果是256,其实就是退出码的最低位被置为了1

【Linux】第十八站:进程等待_第14张图片

对于第七位,我么可以看到总共有64种信号,但是我们会发现没有0号信号,是因为0要代表正常终止。所以总共65种状态,就需要七位来表示。

【Linux】第十八站:进程等待_第15张图片

那么在这里我们可能会有一个问题,就是我们觉得可能没有必要这样做,因为完全可以设置一个全局变量,然后再子进程退出的时候,修改这个全局变量来处理状态就可以了,不需要用wait来处理?

其实这是因为,进程具有独立性

即便子进程中将这个全局变量给修改了,但是父进程也是看不到的。所以必须得通过wait系统调用,让操作系统去拿这个数据。

我们可以这样做,就可以分别拿出两种码了

【Linux】第十八站:进程等待_第16张图片

运行结果为

【Linux】第十八站:进程等待_第17张图片

如果我们的退出码是11

【Linux】第十八站:进程等待_第18张图片

那么运行结果的退出码就是11

【Linux】第十八站:进程等待_第19张图片

我们也可以模拟当他出现异常的时候

【Linux】第十八站:进程等待_第20张图片

可以看到,子进程第一次就发生了除0错误,直接进入了僵尸状态。

并且最终就是8号信号浮点数错误

【Linux】第十八站:进程等待_第21张图片

我们也可以让他进入死循环,然后我们使用kill去杀掉这个信号

【Linux】第十八站:进程等待_第22张图片

4.status的原理

在下图种,意思是,父进程再某行种调用了waitpid这个函数,cpu去调度父进程,当子进程执行完毕的时候。子进程为了保证自己的结果可以被上层获取,子进程可以允许把代码和数据释放掉,但是子进程对应的进程控制块不能被释放掉。我们需要将子进程退出时的信息放入进程控制块中

【Linux】第十八站:进程等待_第23张图片

所以子进程中一定有sigcode,exitcode

如下在linux内核中就可以看到这两个

【Linux】第十八站:进程等待_第24张图片

当他退出时,会将值写入exit_code中,当他异常终止时,会将信号写入exit_signal中。然后waitpid就可以读取这里面的数据了

waitpid一方面可以检测当前进程是否退出了,比如说z状态。是z状态,就直接读取这两个值,通过位运算,让上层拿到

【Linux】第十八站:进程等待_第25张图片

所以waitpid的核心工作就是读取子进程的task_struct,内核数据结构对象,并且将进程的Z状态改为X状态

那么为什么不让我们写代码时候直接访问子进程的pcb中呢,而是必须要通过系统调用呢?

我们必须要通过管理者拿到被管理者的数据,不能直接拿被管理者的数据,因为操作系统不相信任何人

5.等待失败

前面我们知道,wait/waitpid函数在等待失败的时候,会返回-1,那么什么时候会失败呢?

这里有一个很经典的场景,那就是等待的进程不存在或者等待的进程不是该进程的子进程

【Linux】第十八站:进程等待_第26张图片

【Linux】第十八站:进程等待_第27张图片

这就说明了,等待的进程必须是该进程的子进程

6.与status有关的两个宏

其实在我们的代码中,如果要让我们去写这两个的话是比较麻烦的

image-20231116201653437

所以linux提供了两个宏

  • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

我们可以这样用

【Linux】第十八站:进程等待_第28张图片

【Linux】第十八站:进程等待_第29张图片

我们可以在试一下多进程的

【Linux】第十八站:进程等待_第30张图片

运行结果为

【Linux】第十八站:进程等待_第31张图片

不过要注意的是,我们这个具有一定的偶然性,因为多进程的话不一定越早的就一定是最快的。取决于CPU的调度


所以现在我们就知道了前面所说的进程等待的原因之二了:我们要通过进程等待,获得子进程的退出情况,要能够知道布置给子进程的任务,它完成的怎么样了----要么关心,也可能不关心。我们也就知道了main函数的返回到底是什么了

就好比,当我们正在运行我们上面的代码的时候,bash也在等待这个进程退出。因为这个进程也是bash的子进程。

7.options

我们知道,wait本身会等待子进程,但是当子进程一直停不下来的时候,父进程只能等待,wait会导致父进程阻塞调用,导致父进程陷入阻塞状态。options可以指定等待方式。如果是0,则是阻塞等待方式(即父进程要等待子进程,子进程一直处于R,父进程就一直处于S,然后将父进程投入到等待队列中,投入到了子进程的等待队列。当子进程结束的时候,再从这个等待队列中将父进程唤醒)

我们在等待的时候,还可以选择另外一种等待方式:非阻塞轮询

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

在这个函数中,如果options是0,那么就是阻塞等待方式

还有一种选项是WNOHANG (wait no hang, hang可以理解为夯住了,类似于服务器宕机了,意思是等待的时候不要夯住,也就是非阻塞)

类似于小明有事找小王

如果小明在楼下给小王隔一会就打一下电话,因为小王一直说忙着等会马上到。这就是非阻塞轮询(小明是在不打电话的时候是非阻塞的,而且是一直循环的打电话)

如果小明在楼下给小王打电话,然后不挂了,知道小明事情做完才挂电话,这就是阻塞等待(因为小明啥也干不了了)

如果小明在楼下给小王隔一会就打一下电话,在这中间的间隙做一些自己的事情,就是非阻塞轮询+做自己的事情

与之对应的就是pid_t的返回值其实应该有三种

大于0:等待成功,即返回子进程的pid

小于0:等待失败

等于0:等待的条件还没有就绪。

如下就是非阻塞轮询的示例

【Linux】第十八站:进程等待_第32张图片

运行结果如下所示:

【Linux】第十八站:进程等待_第33张图片

注意在非阻塞轮询中,最好加上sleep,否则的话频繁的printf,可能会对系统压力比较大,导致卡住了。达不到预期的结果。


那么这里我们可能会疑惑父进程具体要具体做什么样子的工作?

我们可以用下面这个例子

#include    
#include    
#include    
#include    
#include    
       
#define TASK_NUM 10    
typedef void(*task_t)();    
task_t tasks[TASK_NUM];    
void task1()    
{    
    printf("这是一个执行打印日志的任务,pid:%d\n",getpid());    
}    
void task2()    
{    
    printf("这是一个执行检测网络健康状况的任务,pid:%d\n",getpid());    
}    
void task3()    
{    
    printf("这是一个绘制图形界面的任务,pid:%d\n",getpid());    
}    
void InitTask()    
{    
    for(int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;    
}    
int AddTask(task_t t)    
{    
    int pos = 0;    
    for(pos = 0; pos < TASK_NUM; pos++)    
    {    
        if(!tasks[pos]) break;    
    }    
    if(pos == TASK_NUM) return -1;                                                                                                             
    tasks[pos] = t;    
    return 0;    
}    
void DelTask()                                     
{}
void CheckTask()
{}
void UpdateTask()
{}
void ExecuteTask()
{
    for(int i = 0; i < TASK_NUM; i++)
    {
        if(!tasks[i]) continue;
        tasks[i]();
    }
}
int main()
{
    pid_t id = fork();
    if(id < 0)                                     
    {
        perror("fork:");
        return 1;
    }
    else if(id == 0)
    {
        int cnt = 5;
        while(cnt)
        {
            printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(11);
    }
    else
    {
        int status = 0;
        InitTask();
        AddTask(task1);
        AddTask(task2);
        AddTask(task3);
        while(1) //轮询
        {
             pid_t ret = waitpid(-1, &status,WNOHANG); //非阻塞
             if(ret > 0)
             {
                 if(WIFEXITED(status))
                 {
                     printf("进程是正常跑完的,退出码为:%d\n",WEXITSTATUS(status));
                 }
                 else 
                 {
                     printf("进程出异常了\n");
                 }
                 break;
             }
             else if(ret < 0)
             {
                 printf("wait fail\n");
                 break;
             }
             else 
             {
                 ExecuteTask();
                 usleep(500000);
                 //printf("子进程还没有退出,在等等\n");
                 //sleep(1);
             }
        }
        sleep(3);
    }
    return 0;
}

运行结果为

【Linux】第十八站:进程等待_第34张图片

在这里我们需要注意的是,在这里,我们等待子进程才是最核心的任务,这些其他的任务都是一些顺带的事情。

这些顺带的任务不可以太重了,应该得是轻量化的任务。比如打印日志,检测网络状况等。而且在这里,我们这里也只是延迟回收了一会子进程,而不是说不回收子进程了。

在上面的代码中,我们只是单进程的等待任务,如果我们想要改为多进程的等待任务的话,那么我们只需要将这里稍作修改即可,不让他直接break,而是设置一个计数器,计数子进程的个数,当一个子进程结束后,计数器减减即可。只有减到0以后,才是break。还有就是在出错的时候,也break即可

【Linux】第十八站:进程等待_第35张图片

最后一点需要注意的是

waitpid在回收子进程的时候,它可以保证父进程一定是最后一个被回收的。因为子进程可以全部被waitpid给回收掉。

你可能感兴趣的:(【Linux】,linux,运维,服务器,c++,c语言,操作系统)