本文主要讨论进程的等待。

进程等待

为什么要进行进程等待?

在前面进程状态中我们知道,子进程退出的时候,父进程如果不做处理,进程就会变为z状态,即僵尸进程,从而导致内存泄漏。 关于进程状态我们已经在前面的文章介绍过:进程状态

而进入僵尸状态的进程不会再被杀死,就算我们输入信号也无法处理这样一个死去的进程。

所以,父进程需要通过进程等待的方式来对进程进行回收,获取子进程的退出信息;也能解决内存泄漏问题。

什么是进程等待?

进程等待是一个通过系统调用 wait/waitpid 来进行对子进程的状态检测与回收的功能。

进程等待使用wait进行系统调用,访问进程的PCB,将僵尸进程释放掉。

下面我们利用fork创建子进程,再让子进程结束运行变为僵尸进程:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t id = fork();
    if(id<0)//创建子进程失败
    {
        perror("fork");
        return 1;
    }
    else if(id == 0)
    {
    //child
        int cnt = 5;
        while(cnt)
        {
            printf("child, pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        exit(0);//子进程退出
    }
    else
    {
    //parent
        while(1)
        {
            printf("father,pid:%d,ppid:%d\n",getpid(),getppid());
            sleep(1);
        }
    }
    return 0;
}

下面我们再打开一个窗口,输入如下命令:

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

来同步监视状态的变化。运行上面代码后可以在窗口中看到如下场景:

可以看到在五次循环后,即五秒之后子进程变为僵尸进程状态。

且代码的打印结果为:


wait/waitpid

下面我们介绍如何进行进程等待。

进程等待需要用到wait。我们用man命令查看一下:

man 2 wait

其说明为等待一个进程,直到该进程的状态改变。我们先不管函数的参数,在上面的代码例子中,我们修改parent部分的代码,让进程等待实现:

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

//...

else
{
//parent
    int val = 10;
    while(val)
    {

        printf("father,pid:%d,ppid:%d,val:%d\n",getpid(),getppid(),val);
        sleep(1);
        val--;
    }
    pid_t ret = wait(NULL);
    if(ret == id)//判断等待的子进程
    {
        printf("wait success,ret:%d\n",ret);
    }
    sleep(5);
}

//...

之后重复上面的操作,运行之后可以看到子进程在五秒后进入僵尸状态,再五秒后被回收。从打印结果中可以看到,pid为ret  == 13079 的子进程被成功回收。


多个进程

我们也可以通过循环来一次性创建多个子进程。比如如下书写main函数:

int main()
{   
    int i = 0;
    for(i=0;i<N;i++)
    {
        pid_t id = fork();//创建子进程
        if(id == 0)
        {
            TestChild();
            exit(0);
        }
    }
}

由于父子进程的代码是共享的,子进程会进入if的判断并通过exit退出;而父进程则会继续进行循环并创建子进程,从而实现一次性创建多个子进程的功能。

下面我们在让子进程打印一下自己的pid信息,并通过sleep等待缓慢打印结果。

最后使用wait进行等待,将创建的僵尸子进程回收。完整代码如下:

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

#define N 10//准备创建十个进程

void TestChild()
{
    int cnt = 5;
    while(cnt)
    {
        printf("I am child process,pid:%d,ppid:%d\n",getpid(),getppid());
        cnt--;
        sleep(1);
    }
}

int main()
{   
    int i = 0;
    for(i=0;i<N;i++)
    {
        pid_t id = fork();//创建子进程
        if(id == 0)
        {
            TestChild();
            exit(0);
        }
        printf("create child process: %d success\n",id);
    }
    sleep(10);

    //等待
    for(i = 0;i < N;i++)
    {
        pid_t id = wait(NULL);
        if(id>0)
        {
            printf("wait %d success\n",id);
        }
    }
    sleep(5);

    return 0;
}

运行之后可以看到一瞬间创建出了十个僵尸子进程,在10秒后者十个子进程被全部回收,只剩下父进程仍在运行;再5秒后所有进程结束。


阻塞等待

在上面的代码中,如果子进程始终不退出:

for(i=0;i<N;i++)
    {
        pid_t id = fork();//创建子进程
        if(id == 0)
        {
            TestChild();
            //进程始终不退出
            //exit(0);
        }
    }

并且我们中途也不进行等待sleep(10),直接让父进程进入等待,那么:

父进程默认在wait的时候调用这个系统调用的时候,也就不返回;该状态就为阻塞状态。所以一个进程在阻塞态的时候,不是一定在等待硬件资源,也可以等待软件资源,也就是这个子进程不退出,不进入僵尸状态的情况。

waitpid

下面我们来介绍waitpid。同样使用man命令查看一下:

我们可以发现waitpid的参数明显毕wait更多,不过二者都有一个 int *status 参数。这些参数表示什么呢?

参数:

  • pid: Pid=-1,等待任一个子进程。与wait等效
  • pid>0:等待其进程ID与pid相等的子进程。
  • status:提供了两个能获取部分信息的宏:
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
  • options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

在上面的代码中,int *status 参数部分可以设置为NULL;此时表示进程等待可以不关心进程的状态。

status

 这部分的参数实际上是一段32位的二进制数,其存储的信息可以帮助父进程获取进程的运行情况:即程序运行是否异常?程序的退出码是多少?

如果程序是正常执行,那么就会返回退出码;如果程序异常,就会被信号终止。

在这段二进制数中,后八位表示信号;如果这八位为0,那么表示该进程跑完,没有中途停止。如果wield其他数,则会受到数字对应的信号。关于信号的内容在后面的文章介绍。

次八位就表示进程的退出码。所以,根据这段二进制数,父进程就能掌握子进程的运行情况。

阻塞非阻塞轮询

阻塞:我们用一个例子来理解传统阻塞方式;当你调用一个函数时,如果数据还没有准备好,程序就会挂起(卡住),直到数据准备完毕才返回。这样的方式会让程序一直等待返回结果,不能做自己的事情。

非阻塞轮询:和阻塞一直挂起不同,非阻塞轮询发出请求时,系统会立即返回结果;
如果数据好了,直接带走数据结果;
如果数据没好,返回一个错误码;
由于程序立即返回结果,只要没拿到结果,它就可以通过循环一直进行询问,这种方式就是轮询

在实际操作中,阻塞和非阻塞轮询都会被使用;阻塞虽然有明显的缺点,但逻辑简单;非阻塞轮询可以让程序做自己的事情,每隔一段时间过来询问,降低等待成本。

option

option这个参数就是用于选择阻塞方式。

option=0:表示阻塞。

option=WNHANG:表示非阻塞方式。

如果想要实现轮询,那么套用在循环中即可。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐