Linux 进程等待

2023-12-13 14:16:23

进程等待的必要性

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

其中通过等待获取子进程任务完成的情况,操作系统提供了获取子进程退出状态的方式,只不过父进程可以关心子进程的推出状态,也可以不关心子进程的退出状态!但是进程等待是必须的!

进程等待演示

我们先来看进程等待的接口:我们先来看最简单的那个函数 wait,这个参数 wstatus 就是用来获取子进程的退出状态的,这里先不讲怎么用(直接传 NULL),我们直接看看进程等待的效果!
在这里插入图片描述
我们让子进程打印自己的 pidppid 每秒打印一次,持续三秒!我们想看到子进程的僵尸状态,因此让父进程 sleep 三秒以上!回收子进程成功之后,不想让父进程退出:

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        int cnt = 3;
        while(cnt)
        {
            printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
    }
    else
    {
    	sleep(5);
        wait(NULL);
    }
    while(1) sleep(1);
    return 0;
}

可以看到父进程等待子进程成功之后,子进程资源被回收,进程由 Z 状态变为 X 状态,当然 X 状态是瞬时状态,我们通过监控脚本时看不到的!

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

在这里插入图片描述

wait(int* wstatus)

  • 这个函数可以等待任意进程,等待的方式是阻塞等待!什么是阻塞等待呢?就是说在你调用 wait 函数的地方,父进程会一直在那里等,知道有子进程退出被父进程回收!
  • 参数我们还是不讲解,我们会在 waitpid 函数中讲解,因为这俩货有个一样的 wstatus 参数!
  • 返回值就是等待成功的哪一个子进程的 pid,如果等待失败则返回 -1

既然可以等待任意进程,那么应该如何回收一批进程呢?
我们使用循环创建一批子进程,让子进程每秒打印一次 pidppid 持续两秒!然后子进程退出,我们想看到子进程的僵尸状态,父进程 sleep 两秒以上再开始回收进程!使用 wait 函数等待子进程,判断返回值,如果返回值大于零表示等待子进程成功!最后我们也不想让父进程退出!

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

#define NUM 3

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

int main()
{
    for(int i = 0; i < NUM; i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            runChild();
            exit(0);
        }
    }
    sleep(4);
    for(int i = 0; i < NUM; i++)
    {
        int ret = wait(NULL);
        if(ret > 0)
        {
            printf("wait %d success\n", ret);
        }
    }

    while(1) sleep(1);

    return 0;
}

我们看到结果符合我们的预期哈:
在这里插入图片描述

waitpid(pid_t pid, int* wstatus, int potion)

  • 该函数可以等待指定 pid 的进程。
    • 如果 pid == -1 表示等待任意子进程,这就和 wait 函数差不多啦!
    • 如果 pid > 0 那么就是等待指定 pid 的进程!
  • int* wstatus:这是输出型参数,用来获取子进程的退出状态!
  • int option:表示父进程等待子进程时的等待方式!
    • 0:表示阻塞等待。
    • WNHANG:表示非阻塞轮询!
  • 返回值:
    • 如果第三个参数是 0,waitpid 函数返回状态发生改变的那个子进程,就是由 Z 状态变为 X 状态的那个子进程嘛
    • 如果第三个参数是 WNOHANGwaitpid 返回 0 表示子进程还没有退出,需要继续等待!如果返回值大于零证明等待成功!返回 -1 表示等待失败!

int* wstatus 参数详解

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

  • 子进程代码是否异常
  • 没有异常,结果对吗?还需要获取子进程的退出码

输出型参数,必须传入指针嘛!在 waitpid 函数内部一定是这样的:(*wstatus) = xx,就是对这个整形赋值,(*wstatus) 我们只会用到低 16 位!
在这里插入图片描述

  • 低七位:低七位表示进程的终止信号,也就是用来表示进程是否有异常!异常的本质就是进程收到了信号!使用 kill -l 命令查看所有信号发现信号的编号是从 1 开始的!
    在这里插入图片描述
    说明如果低七位为 0 则表示进程没有收到信号,也就是没有异常!
  • core dump 标志位会在后面讲信号的部分解释!
  • 8-16 位:保存的就是子进程的退出码!

好的,那么我们使用代码来获取一下子进程的退出状态吧!
我们使用 fork 创建一个子进程,子进程每秒打印一次 pid ppid 循环打印三秒后子进程退出,设置退出码为 11。父进程创建子进程后直接等待子进程就行啦,通过位操作将输出型参数 wstatus 中的 exit codesignal 提取出来!

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        int cnt = 3;
        while(cnt)
        {
            printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(11);
    }
    else
    {
        int status = 0;
        int ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
            printf("wait %d success, exit code: %d, signal: %d\n", id, ((status >> 8) & 0xFF), (status & 0x7F));
        }
        else
        {
            printf("wait failed\n");
        }
    }

    return 0;
}

可以看到也是成功获取到我们使用 exit 设置的退出码了,signal 为 0,表示进程没有发生异常!
在这里插入图片描述
我们也可以在子进程运行的时候,给子进程发信号,当然你也可以在代码中写出会触发异常的代码!
可以看到子进程收到了信号父进程也能获取到,子进程发生了异常,那么子进程的退出码就没有意义,这个可是讲过的哦!
在这里插入图片描述


WIFEXITED(status) 和 WEXITSTATUS(status)

这是两个宏,利用这两个宏能够根据调用 waitpid 设置的 wstatus 变量获取子进程的退出状态,就不必使用位操作了!因为又的程序员并不关心原理,只要能够运用就行啦!提供这两个宏就比较方便嘛!

  • WIFEXITED(status):这个宏可以判断子进程是否发生了异常,如果发生异常返回 0,反之返回 1 。
  • WEXITSTATUS(status):这个宏可以直接提取退出码!可以看到这个宏的实现就是位操作提取嘛!在这里插入图片描述在这里插入图片描述
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        int cnt = 5;
        while(cnt)
        {
            printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(11);
    }
    else
    {
        int status = 0;
        int ret = waitpid(id, &status, 0);
        if(WIFEXITED(status))
        {
            //子进程没有异常
            if(ret > 0)
            {
                printf("wait %d success, exit code: %d\n", id, WEXITSTATUS(status));
            }
            else
            {
                printf("wait failed\n");
            }
        }
        else
        {
            printf("进程发生异常导致退出\n");
        }
    }

    return 0;
}

在这里插入图片描述

父进程获取子进程退出状态的本质

首先,waitpid 函数是系统调用,系统调用是操作系统提供给上层用户访问或修改操作系统资源的接口!因为操作系统不相信任何人!因此才会提供系统调用!
就好比银行不相信任何人,你要到银行办理业务需要通过银行的那个小窗口来做到!


子进程退出的时候,代码和数据会直接被释放掉,子进程的退出码以及是否受到信号,收到的信号是几号都会保存在子进程的 task_struct 中!父进程等待子进程就是将子进程的 task_struct 释放嘛,在释放的时候顺便获取一下子进程的退出状态当然是可以做到的,这个过程大部分就是 waitpid 来做的!
我们能根据这个过程画出示意图:
在这里插入图片描述
子进程代码执行完毕退出之后变为 Z 状态,waitpid 检测到子进程的僵尸状态,就会读取子进程的 task_struct 内核数据结构对象,将 exit_code exit_signal 合并之后返回个上层用户!最后将子进程的状态由 Z 状态改为 X 状态,操作系统就会将 X 状态进程的 task_struct 放入回收队列,完成 task_struct 的释放!


为了验证我们结论的正确性,我们可以看到进程的 task_struct 中的确维护了 exit_codeexit_signal 这样的字段:
在这里插入图片描述

int option 参数详解

这个 option 我们学习两个选项:

  • 0:表示父进程阻塞等待子进程,这没啥好说的!使用 wait 函数的时候就讲解过啦!
  • WNOHANG:就是数字 1 哈!
    在这里插入图片描述
    这个选项可以让父进程不阻塞等待子进程,而是每隔一段时间检测子进程是否退出!相比阻塞等待的优势就是父进程可以执行自己的代码!阻塞等待子进程没有退出父进程就必须卡在那里,啥事儿都干不了!
    我们得搞清楚重点哦:等待子进程才是核心,不能让父进程检测子进程是否退出的时间间隔太长了!也就是父进程在等待子进程期间执行的代码是轻量级的,比如打印日志啥的
    使用 WNHANG 选项,如果 waitpid 返回 0,表示子进程还没有退出!如果大于 0 就表示等待子进程成功啦!如果小于零就表示等待失败了!

WNOHANG:表示非阻塞轮询,waitpid 肯定要写到循环中撒,根据返回值来判断子进程的状态,是还没退出呢?还是等待成功呢?

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

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        int cnt = 5;
        while (cnt)
        {
            printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(11);
    }
    else
    {
        int status = 0;
        while (1)
        {
            int ret = waitpid(id, &status, WNOHANG);
            if (WIFEXITED(status))
            {
                if (ret == 0)
                {
                    printf("子进程还没有退出,我会等你的哦!\n");
                    printf("执行打印日志的功能....\n");
                }
                else if (ret > 0)
                {
                    printf("wait %d success, exit code: %d\n", ret, WEXITSTATUS(status));
                    break;
                }
                else
                {
                    printf("wait failed\n");
                    break;
                }
            }
            else
            {
                printf("进程是异常退出的\n");
                break;
            }
            sleep(1);
        }

        return 0;
    }
}

在这里插入图片描述

子进程被创建之后哪一个进程先被调度我们不清楚,因为这是由调度器决定的,但是最后一个退出的进程一定是父进程,通过进程等待能够很好地保证这一点!

  1. 为什么要等待子进程

  2. 等待子进程的函数

    • wait
    • waitpid(),waitpid 函数参数理解
  3. 进程等待的本质

文章来源:https://blog.csdn.net/m0_73096566/article/details/134903954
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。