linux c:解决僵死进程(SIGCLD捕获wait/忽略、双fork、阻塞wait)
作者:@罗一
本文为作者原创,转载请注明出处:https://www.cnblogs.com/luoyicode/p/17064922.html
今天咱们一起实验分析一下僵死进程的解决:
我所知的解决僵死进程的方法有4种:1.阻塞wait法、2.忽略SIGC(H)LD托管法、3.双fork托管法、4.SIGCLD捕获函数wait
接下来让我从信号处理和进程回收的角度逐步讲解僵死进程的产生与解决方法,并进行实际解决。
1 基本原理(结合实验)
1.1 SIGCLD信号与子进程回收
当子进程运行结束,将会向父进程发送一个SIGCLD信号,通知父进程将堆栈等资源回收,但是这里面会有几种情况:
1.1.1 父进程先于子进程退出(子进程托管)
子进程还在运行时父进程结束、退出,此时父进程会将所有子进程托管给一个系统程序:/lib/systemd/systemd,所有子进程的父亲变成这个系统程序。实验验证一下:
#include <stdio.h>
#include <unistd.h>
void main(){
pid_t pid;
if((pid = fork()) == 0){ // child process //
fprintf(stderr,"child start\n");
sleep(20);
fprintf(stderr,"child end\n");
}else { // parent process //
fprintf(stderr,"parent start\n");
sleep(10);
fprintf(stderr,"parent end\n");
}
}
运行结果:刚运行时pid2734是2735的父进程,之后10秒后父进程退出,子进程2735的父进程变成了1610,
之后我查看进程1610,发现1610是 /lib/systemd/systemd,这就是子进程托管。
1.1.2 子进程先退出,父进程忽略SIGCLD(子进程托管)
父进程忽略SIGCLD,子进程包括其PCB会被内核回收,而不是交给父进程回收。实验验证一下:
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
void main(){
pid_t pid;
if((pid = fork()) == 0){ // child process //
sleep(5);
fprintf(stderr, "child end");
} else { // parent process //
signal(SIGCLD, SIG_IGN);
sleep(10);
fprintf(stderr, "parent end");
}
}
运行结果:子进程运行结束后PCB也被回收,没有产生僵死进程。
1.1.3 子进程先退出,父进程默认处理SIGCLD/不调用wait(产生僵死进程)
父进程默认处理SIGCLD,同时不调用wait等待子进程,那么子进程将会进入<defunct>状态,也就是变成僵死进程。实验验证一下:
#include <stdio.h>
#include <unistd.h>
void main(){
pid_t pid;
if((pid = fork()) == 0){ // child process //
fprintf(stderr, "child begin\n");
sleep(10);
fprintf(stderr, "child end\n");
}
else { // parent process //
fprintf(stderr, "parent begin\n");
sleep(20);
fprintf(stderr, "parent end\n");
}
}
运行结果:
第一个时间段(0sec - 10sec),父子进程都在运行,3816是父进程,3817是子进程,它们同时运行。
第二个时间段(10sec - 20sec),子进程退出,父进程运行。可以看到子进程3817已经进入了<defunct>状态,此时子进程变成了僵死进程!
第三个时间段(>20sec),父进程结束,父子(僵死)进程都被内核回收,在进程表内消失。
1.1.4 父进程调用wait等待子进程退出(wait处理)
父进程调用wait()阻塞等待子进程退出,此时不产生僵死进程。
1.2 优化wait函数的阻塞:将信号捕获函数设置为wait()
通过signal设置信号捕获函数为wait(),可以实现SIGCLD信号的异步处理,避免阻塞;但是要注意两点:
1.2.1 需将wait()函数封装为void WAIT(int)
因为signal函数的第二个参数是void(*)(int)类型,所以需要对wait函数进行一次封装:
void WAIT(int status){
wait(&status);
}
// 入口函数内 //
signal(SIGCLD, WAIT);
1.2.2 不可忽略信号导致sleep中断并暂时失效
sleep函数有一个特点,就是当收到了不可忽略的信号后,它将失效,所以在收到SIGCLD信号后它将暂时失效,此时要进行优化,也就是将sleep放到while循环中:
sleep(10); // 优化前
- > // 优化后
int s_clock = 10;
while(s_clock){
s_clock = sleep(s_clock);
if(s_clock != 0)s_clock++;
}
接下来组建一个完整的实验程序,进行测试:
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
void WAIT(int status){
wait(&status);
}
void main(){
pid_t pid;
if((pid = fork()) == 0){ // child process //
fprintf(stderr, "child begin\n");
sleep(5);
fprintf(stderr, "child end\n");
} else { // parent process //
signal(SIGCLD, WAIT);
int s_clock = 10;
fprintf(stderr, "parent begin, s_clock = [%d]\n",s_clock);
while(s_clock){
s_clock = sleep(s_clock);
if(s_clock != 0)s_clock++;
fprintf(stderr, "signal_break_sleep, s_clock = [%d]\n", s_clock);
}
fprintf(stderr, "parent end, s_clock = [%d]\n",s_clock);
}
}
运行结果:子进程运行5秒退出,之后子进程发送SIGCLD信号,之后父进程sleep被中断,s_clock剩余5秒,之后进入while循环继续sleep直到s_clock = 0 sec。
2 四种方法解决僵死进程
2.1 阻塞wait
父进程代码中插入一行:wait(&status);
2.2 忽略SIGCLD
父进程代码中插入一行:signal(SIGCLD, SIG_IGN);
2.3 wait做SIGCLD的捕获函数
父进程代码中插入几段:
//封装wait函数为void(*)(int)类型
void WAIT(int status){
wait(&status);
}
// 入口函数内 //
signal(SIGCLD, WAIT);
// sleep代码的优化 //
sleep(10); // 优化前
- > // 优化后
int s_clock = 10;
while(s_clock){
s_clock = sleep(s_clock);
if(s_clock != 0)s_clock++;
}
2.4 两次fork
在fork()后,子进程马上调用一次fork(),之后子进程没有其余代码直接退出,将业务代码全部写到子子进程内;父进程马上调用一次wait()回收子进程,这样负责业务的子子进程就被系统托管,而子进程则被wait回收。
#include <sys/wait.h>
//入口函数内
if((pid = fork()) == 0){
if((pid = fork()) == 0){ // child process //
/* *
* 子进程业务代码 *
*/
}
}
else { // // parent process //
wait(NULL);
/* * *
* * 父进程业务代码 *
*/
}
接下来进行一个小实验,实验代码:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
void main(){
pid_t pid;
if((pid = fork()) == 0){ // double nest fork()
if((pid = fork()) == 0){ // child process //
fprintf(stderr, "child begin\n");
sleep(10);
fprintf(stderr, "child end\n");
}
}
else { // parent process //
wait(NULL);
fprintf(stderr, "parent begin\n");
sleep(20);
fprintf(stderr, "parent end\n");
}
else { // parent process //
wait(NULL);
fprintf(stderr, "parent begin\n");
sleep(20);
fprintf(stderr, "parent end\n");
}
}
运行结果:父进程6554由6398的bash进程管理,子子进程6556因为子进程6555的直接退出而称为了孤儿进程,交给了1610的systemd进程托管:
感谢观看,我们一起加油!
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!