【Linux仓库】进程等待【进程·捌】

🌟 各位看官好,我是egoist2023

🌍 Linux == Linux is not Unix !

🚀 今天来学习Linux的指令知识,并学会灵活使用这些指令。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦!

目录

进程等待必要性

进程等待

等待方法

wait等待

waitpid等待(最佳实践)

获取子进程退出码

第二种获取子进程退出码:

strerror

获取子进程信号编号

非阻塞等待

为何释放时不能立即释放task_struct


进程等待必要性

  • 之前讲过,**⼦进程退出,⽗进程如果不管不顾,就可能造成'僵⼫进程'**的问题,进⽽造成内存泄漏。
  • 另外,进程**⼀旦变成僵⼫状态**,那就⼑枪不⼊,"杀⼈不眨眼"的kill -9 也⽆能为⼒,因为谁也没有办法杀死⼀个已经死去的进程
  • 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是不对,或者是否正常退出。(如何判断子进程把任务完成怎么样? --> 退出码,这也是为什么要讲进程终止的原因,是为了进程等待服务)
  • 父进程通过进程等待的方式,回收子进程资源(必要),获取⼦进程退出信息(可选)。

进程等待

父进程创建子进程后,将来子进程执行完程序后,子进程进程终止,此时子进程的代码和数据会被释放,但是PCB不能立马释放;父进程通过等待的方式来回收子进程PCB(如果父进程没有回收,那么子进程就会处于 ' Z ' 僵尸状态), 如果父进程有需要的话也可以获取子进程的退出信息。

等待方法

wait等待

pid_t wait(int* status)

返回值:

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

参数:

输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL。

如果子进程执行完自己的代码后,父进程迟迟不进行回收,那么我们应该会观察到子进程处于 ' Z ' 状态,那该如何模拟出子进程 ' Z ' 状态呢?只要让父进程一直死循环执行,不去回收子进程即可

waitpid等待(最佳实践)

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相等的⼦进程。

当pid设为-1的时候,父进程是如何获取所有子进程信息的呢?在task_struct存在一个链表成员,就是children成员用于维护进程的子进程链表.

bash 复制代码
struct task_struct
{
    struct list_head children;
}

status: 输出型参数

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

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

退出码)

options:默认为0,表⽰阻塞等待.

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

  • 如果⼦进程已经退出,调⽤wait/waitpid时,wait/waitpid会⽴即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调⽤wait/waitpid,⼦进程存在且正常运⾏,则父进程可能阻塞。
  • 如果不存在该子进程,则⽴即出错返回。
获取子进程退出码
  • wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。
  • 如果传递NULL,表⽰不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给⽗进程。

在下面这段程序中,父进程创建了10个子进程后,后续需要等待回收子进程的资源,并且父进程关心子进程的退出信息(是否出错),那么通过打印子进程的状态来查看,理论上应该能看到打印了status:115。

不对啊???子进程的退出码不是115吗?为什么变成256了呢?这里可以肯定的是status不仅仅是退出码。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

由上图可知,我们要拿到子进程的退出状态,那么就需要拿到次低8位的退出状态,通过位运算:

(status >> 8)& 0xFF; //0xFF --> 8个1

第二种获取子进程退出码:

WEXITSTATUS能从wait系列函数返回的status中提取有效退出码,帮助父进程了解子进程的执行结果。

strerror
bash 复制代码
    std::vector<pid_t> subids;
    for(int i=0;i<N;i++)
    {
        pid_t id = fork();
        if(id==0)
        {
            printf("我是一个子进程,pid:%d ,ppid:%d,count:%d\n",getpid(),getppid(),i);
            sleep(1);
            break;
        }
        printf("子进程退出了\n");
        exit(1);

        subids.push_back(id);
    }
    
    sleep(5);
    //父进程执行流
    for(auto& sid:subids)
    {
        int status =0;
        printf("父进程开始等待子进程ing...,%d\n",sid);
        pid_t rid =waitpid(sid,&status,0);
        if(rid>0)
        {
            int exit_code=(status>>8)&0xFF;
            int exit_signal=status&0x7F;

            if(exit_code>0&&exit_signal==0)
            {
                printf("子进程运行完毕,结果不正确:%d:%s\n",exit_code,strerror(exit_code));
            }
        }
    }

在上面这段程序当中,为什么权限会被拒绝了呢?

"权限被拒绝" 的根源 :子进程的退出码 / 信号编号恰好等于EPERMerrno(1),导致strerror误解析。

获取子进程信号编号

如果子进程出现异常了呢?那么父进程就不关心进程的退出码,因为出现了问题,导致OS收到了信号,杀掉了该进程,因此**要关心的是为什么出现异常?**那么,该如何拿到该信号数字呢?

kill 命令:

选项:

-l : 显示信号数字

可以看到没有信号0,这也符合我们status图所述情况。


status & 0x7F; //0x7F --> 7个1 , 获取子进程信号

bash 复制代码
   for(int i=0;i<N;i++)
   {
       pid_t id = fork();
       if(id==0)
       {
           int i=10;
           while(i)
           {
               printf("我是一个子进程,pid:%d ,ppid:%d,count:%d\n",getpid(),getppid(),i);
               sleep(1);
               i--;
           }
           printf("子进程退出了\n");
           exit(0);
       }
   }
   
   printf("父进程开始等待子进程ing...\n");
   sleep(5);
   //父进程执行流
   for(int i=0;i<N;i++)
   {
       int status =0;
       pid_t rid =waitpid(-1,&status,0);
       if(rid>0)
       {
           printf("父进程等待子进程成功,子进程pid:%d,status:%d,status code:%d,
                   status singal:%d\n",rid,status,(status>>8)&0xFF,status&0x7F);
       }
   }

那如果我向子进程发出 信号9 呢? 子进程会异常终止,父进程回收到子进程的资源,通过status输出型参数获得子进程的 信号 ,进行打印验证是否为 信号9 。

综上所述:

进程正常结束: status = 退出码 + 0(信号编号)

进程异常结束: status = 信号编号(退出码无意义)

非阻塞等待

非阻塞等待:本质其实是检测子进程状态是否退出,若没有退出不会因为条件没有就绪而阻塞,而是立即返回。

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

而要设为非阻塞等待,options要为WNOHANG,表示不要卡住,即非阻塞等待。
waitpid的返回值:

  1. 子进程退出 && waitpid 成功 --> 返回子进程pid
  2. 子进程没有退出 && waitpid成功 --> 返回0
  3. waitpid等待失败(如等待子进程不是你的) --> 返回-1
bash 复制代码
    pid_t id = fork();
    if(id==0)
    {
        int cnt=10;
        while(cnt--)
        {
            printf("子进程在运行:%d\n",cnt);
            sleep(1);
        }
        exit(0);
    }
    //父进程
    pid_t rid = waitpid(id,NULL,WNOHANG);
    if(rid==id)
    {
        printf("wait child success\n");
        break;
    }
    else if(rid==0)
    {
        printf("child note quit\n");
        sleep(1);
    }
    else
    {
        printf("wait error\n");
        break;
    }

在上面这段程序:父进程fork创建子进程,此时父子进程各自执行自己的执行流,子进程在执行while循环的时候,父进程非阻塞等待子进程,但子进程还没退出,此时pid_t 的返回值为 0 ,因此会打印 rid == 0 的条件内容,父进程提前退出。

同时会发现子进程用ctrl + c 退出不了,只有等自己的进程结束后才会终止,这又是为什么呢?这里使用ps工具进行观察。

由于父进程的提前退出,导致子进程变成孤儿进程,而我们前面说过孤儿进程会被1号进程所领养,即被bash所领养。为什么无法 ctrl + c 终止呢?孤儿进程的父进程变为 init/systemd,而Ctrl + C的信号传递依赖原进程组结构,导致信号无法有效作用于孤儿进程。

那真正的非阻塞轮询该怎样做才有意义?又是怎样的呢?

很明显,父进程在非阻塞等待子进程的同时也可以做做其他事情

Task.hpp

bash 复制代码
#pragma once

#include<iostream>

void Download()
{
    std::cout<<"我是一个下载任务\n"<<std::endl;
}

void Printlog()
{
    std::cout<<"我是一个打印日志任务\n"<<std::endl;
}

void FlushData()
{
    std::cout<<"我是一个刷新数据的任务\n"<<std::endl;
}

Tool.hpp

bash 复制代码
#pragma once

#include<iostream>
#include<vector>
#include<functional>

using func_t = std::function<void()>;
//typedef functional<void()> func_t;

class Tool
{
public:
    Tool()
    {}
    
    void PushFunc(func_t f)
    {
        _funcs.push_back(f);
    }

    void Execute()
    {
        for(auto& f:_funcs)
        {
            f();
        }
    }

    ~Tool()
    {}
private:
    std::vector<func_t> _funcs; //方法集
};

myproc.cc

bash 复制代码
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/wait.h>
#include"Task.hpp"
#include"Tool.hpp"

int main()
{
    //方法集
    Tool tool;
    tool.PushFunc(Download);
    tool.PushFunc(Printlog);
    tool.PushFunc(FlushData);

    pid_t id = fork();
    if(id==0)
    {
        int cnt=10;
        while(cnt--)
        {
            printf("子进程在运行:%d\n",cnt);
            sleep(1);
        }
        exit(0);
    }
    //父进程
    while(1)
    {
        pid_t rid = waitpid(id,NULL,WNOHANG);
        if(rid==id)
        {
            printf("wait child success\n");
            break;
        }
        else if(rid==0)
        {
            printf("child note quit\n");
            //做做其他事情
            tool.Execute();
            sleep(1);
        }
        else
        {
            printf("wait error\n");
            break;
        }
    }
    return 0;
}

为何释放时不能立即释放task_struct