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


    一、进程等待的必要性

    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;                                                                                                                                                                                  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

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

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

    运行结果如下所示

    image-20231116145041367

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

    2.wait

    我们先来看wait函数

    image-20231116145824190

    #include
    #include
    pid_t wait(int*status);
    
    • 1
    • 2
    • 3

    返回值:

    • 成功返回被等待进程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;                                                
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    最终它的运行结果为

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

    image-20231116151813720

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

    那么如果我们这个程序有很多个子进程呢?应该等待哪一个呢?其实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;    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    运行结果如下

    image-20231116162422040

    image-20231116162355469

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

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

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

    image-20231116163402097

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

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

    3.waitpid

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

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

    返回值:

    • 当正常返回的时候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是等价的

    image-20231116170410856

    image-20231116170457366

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

    image-20231116170713686

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

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

    image-20231116172438286

    最终运行结果为

    image-20231116172521576

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

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

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

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

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

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

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

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

    image-20231116173351873

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

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

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

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

    image-20231116180710223

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

    image-20231116180806236

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

    image-20231116180943671

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

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

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

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

    image-20231116183217652

    运行结果为

    image-20231116183253354

    如果我们的退出码是11

    image-20231116183415758

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

    image-20231116183449280

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

    image-20231116183853947

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

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

    image-20231116184002668

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

    image-20231116184951852

    4.status的原理

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

    image-20231116185916779

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

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

    image-20231116190251450

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

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

    image-20231116190447768

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

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

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

    5.等待失败

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

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

    image-20231116201118094

    image-20231116201149355

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

    6.与status有关的两个宏

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

    image-20231116201653437

    所以linux提供了两个宏

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

    我们可以这样用

    image-20231116202146635

    image-20231116202211322

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

    image-20231116203830467

    运行结果为

    image-20231116203932399

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


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

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

    7.options

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

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

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

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

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

    类似于小明有事找小王

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

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

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

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

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

    小于0:等待失败

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

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

    image-20231116212255400

    运行结果如下所示:

    注意在非阻塞轮询中,最好加上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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108

    运行结果为

    image-20231117202347179

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

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

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

    image-20231117203625030

    最后一点需要注意的是

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

  • 相关阅读:
    高速公路安全监测预警系统的功能优势
    深度学习基础知识数据 数据预处理transforms流程讲解
    VS2019创建GIt仓库时剔除文件或目录
    J2EE进阶(九)org
    Android 设置密码文本是否暂时显示字符
    Idea快捷键
    【Gazebo入门教程】第七讲 Gazebo与ROS的通信(附如何开源模型到线上数据库)
    etcd全部key和简单操作
    如何预防CSRF攻击
    游戏动画技术简介
  • 原文地址:https://blog.csdn.net/jhdhdhehej/article/details/134470623