进程资源回收(wait、waitpid)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/daaikuaichuan/article/details/82782594

一、wait函数

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
成功返回所回收子进程的 PID,失败返回 -1

1、函数功能与参数解析

  主要用于挂起正在运行的进程进入等待状态,直到有一个子进程终止
  status 是一个整型指针,如果 status 不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。

2、函数详解

(1)父进程在创建若干子进程以后调用 wait 函数:

  • 若所有子进程都在运行,则阻塞,直至有子进程终止。
  • 若有一个子进程已终止,则返回该子进程的 PID 并通过 status 参数 (若非 NULL)输出其终止状态。
  • 若没有需要等待的子进程,则返回 -1,置 error 为 ECHILD。

(2)如果一个子进程在 wait 函数套用之前,已经终止并处于僵尸状态,wait 函数会立即返回,并取得该子进程的终止状态,同时子进程僵尸消失。由此可见 wait 函数主要完成三个任务:

  • 阻塞父进程的运行,直到子进程终止再继续,停等同步。
  • 获取子进程的 PID 和终止状态,令父进程得知谁因何而死。
  • 为子进程收尸,防止大量僵尸进程耗费系统资源。
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>
#include <sys/wait.h>
 
int main (void)  
{  
    pid_t pid, pr;  
    int status;
    pid = fork();  
    if (pid == -1)  
        perror("fail to fork"), exit(1);  
    else if (pid == 0)  
    {  
        printf("这是子进程 pid = %d", getpid());  
        printf("父进程的 ppid = %d\n",  getppid());  
    }  
    else  
    {  
        // 子进程必须在 wait 函数套用之前结束
        // 如果参数非 NULL,则可以输出子进程终止状态
        pr = wait(&status);
        sleep(5); //可以保证子进程先被调度 (成为僵尸进程)
        printf("这是父进程 ppid = %d\n", getpid());  
        printf ("status = %d, pr = %d\n", status, pr);
    }
    return 0;  
}

(3)子进程的终止状态通过 wait 函数的 status 参数输出给该函数调用者:

 // 判断子进程是否正常终止,是则为真。
 WIFEXITED (status)   (常用)
 // 获取子进程调用 exit、_exit、Exit 函数时所传入的参数或者 main 函数中
 // return 语句返回值的低 8 位。
 WIEXITSTATUS (status)  (常用)
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
 
int main (void)
{
    int status;
    pid_t pc,pr;
    pc =  fork();
    if (pc < 0)
        printf("error ocurred!\n");
    else if(pc == 0)
    {
        printf("This is child process with pid of %d\n",getpid());
        exit (3);
    }
    else
    {
        pr = wait(&status);
        // 高8位 记录进程调用exit退出的状态(正常退出);如果正常退出(exit) ---高8位是退出状态号,低8位是0。
        // 低8位 记录进程接受到的信号 (非正常退出);如果非正常退出(signal)----高八位是0,低8位是siganl id。
        printf ("status = %d\n", status); 
        printf ("status >> 8 = %d\n", status >> 8); 
        if(WIFEXITED(status)) // 正常终止
        {
            printf("The child process %d exit normally.\n",pr);
            printf("The WEXITSTATUS return code is %d.\n",WEXITSTATUS(status)); // WEXITSTATUS返回子进程exit的参数(3)。
            printf("The WIFEXITED return code is %d.\n",WIFEXITED(status)); // WIFEXITED 为真。
        }
        else
            printf("The child process %d exit abnormally.\n",pr);
    }
    return 0;
}

【Note】:
(a)对于孤儿进程:调用wait会使父进程进入等待状态,直到所有子进程都终止。
(b)对于僵尸进程:调用wait会立即收回子进程,获取子进程的终止状态。

二、waitpid函数

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
成功返回所回收子进程的 PID 或 0,失败返回 -1

1、函数功能与参数解析

  等待并回收任意或特定子进程。
第一个参数:进程号,可取以下值。

pid < -1      // 等待并回收特定进程组(由 pid标识)的任意子进程
pid == -1   // 等待并回收任意子进程,相当于 wait 函数  (*)
pid == 0    // 等待并回收与调用进程同进程组的任意子进程
pid > 0      // 等待并回收特定子进程(由pid标识) (*)

第二个参数:输出子进程的终止状态,可置 NULL。
第三个参数:选项,可取以下值 (默认给 0 即可)。

0    // 阻塞模式,若所等子进程仍在运行,则阻塞,直至其终止。  (*)
WNOHANG   // 非阻塞模式,若所等子进程仍在运行,则返回 0  (*)
WCONTINUED  // 若实现支持作业控制,那么由 pid 指定的任一子进程在停止后已经继续,但其装填尚未报告,则返回其状态。
WUNTRACED  // 若某实现支持作业控制,而由 pid 指定的任一子进程已处于停止状态,并且其状态自停止以来还未报告过,则返回其状态。WIFSTOPPED 宏确定返回值是否对应于一个停止的子进程。

2、函数详解

(1)以阻塞模式回收子进程

  事实上,无论一个进程是正常终止还是异常终止,都会通过系统内核向其父进程发送 SIGCHLD (17)信号。父进程可以忽略该信号,也可以提供一个针对该信号的处理函数,在信号处理函数中以异步的方式回收子进程。这样做不仅流程简单,而且僵尸的存货时间短,回收效率高。其中 error 等于 ECHILD 表示没有需要等待的子进程

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
 
int main(void)
{
    pid_t pid;
    if ((pid = fork()) < 0)
        perror("fork"), exit(1);
    else if (pid == 0)
    {
        printf("这是子进程 pid = %d\n", getpid());
        printf("父进程的 pid = %d\n", getppid());
        exit(0);
    }
    else 
        printf("这是父进程 pid = %d\n", getpid());
    // 回收指定pid的子进程
    pid = waitpid(pid, NULL, 0);
    if (pid == -1)
    {
        if (errno == ECHILD) //  error 等于 ECHILD 表示没有需要等待的子进程。
            perror("waitpid"), exit(1);
    }
    else
        printf("%d子进程终止\n", pid);
    printf("%d父进程终止\n", getpid());
    return 0;
}

(2)以非阻塞模式回收子进程

  waitpid函数以非阻塞模式运行时,可以等待并回收所有子进程,等待的同时做空闲处理。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
 
int main (void)
{
    pid_t pid1;
    if ((pid1 = fork()) < 0)
        perror("fork"), exit (1);
    if (pid1 == 0)
    {
        printf("这是子进程pid1 = %d", getpid());
        printf("父进程pid1 = %d\n", getppid());
        exit(0);
    }
    pid_t pid2;
    if ((pid2 = fork()) < 0)
        perror("fork"), exit(1);
    if (pid2 == 0)
    {
        printf("这是子进程pid2 = %d", getpid());
        printf("父进程pid2 = %d\n", getppid());
        exit(0);
    }
    printf("这是父进程pid = %d\n", getpid());
    for(int i = 0; i < 5; ++i)
    {
        pid_t pid = waitpid(-1, NULL, WNOHANG); // 非阻塞模式回收任一子进程
        if (pid == -1) // 返回-1表示没有可回收的子进程了
        {
            if (errno != ECHILD)
                perror("waitpid"), exit(1);
            printf("子进程都死光了\n");
            break;
        }
        if (pid) // 返回值为pid表示成功回收某一个子进程
            printf("%d子进程终止\n", pid);
        else  // 非阻塞返回0
            printf("在这里进行空闲处理\n");
        //表示所等子进程仍在运行,此时父进程出现空闲时间,可在这里进行空闲处理。
    }
    return 0;
}

三、如何避免僵尸进程

  当我们fork()一次后,存在父进程和子进程。这时有两种方法来避免产生僵尸进程:

  • 父进程调用waitpid()等函数来接收子进程退出状态。
  • 父进程先结束,子进程则自动托管到Init进程(pid = 1)。

  考虑子进程先于父进程结束的情况:

  • 若父进程未处理子进程退出状态,在父进程退出前,子进程一直处于僵尸进程状态。
  • 若父进程调用waitpid()(这里使用阻塞调用确保子进程先于父进程结束)来等待子进程结束,将会使父进程在调用waitpid()后进入睡眠状态,只有子进程结束父进程的waitpid()才会返回。 如果存在子进程结束,但父进程还未执行到waitpid()的情况,那么这段时期子进程也将处于僵尸进程状态。

由此,可以看出父进程与子进程有父子关系,除非保证父进程先于子进程结束或者保证父进程在子进程结束前执行waitpid(),子进程均有机会成为僵尸进程。那么如何使父进程更方便地创建不会成为僵尸进程的子进程呢?这就要用两次fork了。

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

int main(void)
{  
    pid_t pid;  
    if((pid = fork()) < 0)
        perror("fork"), exit(1);
    // 父进程一次fork()后产生一个子进程随后立即执行waitpid(子进程pid, NULL, 0)
    // 来等待子进程结束,然后子进程fork()后产生孙子进程随后立即exit(0)。
    // 这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),
    // 然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),
    // 将被转交给Init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为Init,
    // Init进程在其子进程结束时会自动收尸,这样也就不会产生僵尸进程了。
    else if (pid == 0)
    {
        if((pid = fork()) < 0)  
            perror("fork"), exit(1);
        else if (pid > 0) // 孙子进程
            exit(0); // 子进程终止
        else
        {  
            sleep(2);
            printf("second child,parent pid = %d\n", getppid());  
            exit(0);
        }
    }
     //父进程等待回收第一次fork的子进程,但是第二个没有回收,最后被init进程回收。
    if(waitpid(pid, NULL, 0) != pid)
        perror("waitpid"), exit(1); 
    return 0;  
} 

四、wait和sleep的区别

  sleep是休眠指定的时间,到时间了仍然向下执行,而wait是等待需要满足一定条件才能继续执行。

参考:https://www.cnblogs.com/codingmylife/archive/2010/11/10/1874235.html
https://blog.csdn.net/qq_29350001/article/details/70255915

猜你喜欢

转载自blog.csdn.net/daaikuaichuan/article/details/82782594