【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成员用于维护进程的子进程链表.
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
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));}}}
在上面这段程序当中,为什么权限会被拒绝了呢?
“权限被拒绝” 的根源:子进程的退出码 / 信号编号恰好等于EPERM
的errno(1)
,导致strerror
误解析。
获取子进程信号编号
如果子进程出现异常了呢?那么父进程就不关心进程的退出码,因为出现了问题,导致OS收到了信号,杀掉了该进程,因此要关心的是为什么出现异常?那么,该如何拿到该信号数字呢?
kill 命令:
选项:
-l : 显示信号数字
可以看到没有信号0,这也符合我们status图所述情况。
status & 0x7F; //0x7F --> 7个1 , 获取子进程信号
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的返回值:
- 子进程退出 && waitpid 成功 --> 返回子进程pid
- 子进程没有退出 && waitpid成功 --> 返回0
- waitpid等待失败(如等待子进程不是你的) --> 返回-1
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
#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
#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
#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;
}