【Linux仓库】进程等待【进程·捌】_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 subids; for(int i=0;i0) { 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;i0) { 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#includevoid 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#include#includeusing func_t = std::function;//typedef functional 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 _funcs; //方法集};
myproc.cc
#include#include#include#include#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










