popen子进程被杀死后父进程无法正常退出问题求助
问题描述
我设计的程序逻辑如下:
- 安装信号处理器,收到特定信号时设置标志位;
- 启动事件循环;
- 标志位触发时退出事件循环并执行收尾步骤。
代码大致如下(附带Makefile):
volatile sig_atomic_t ev_flag = 0; static void signal_handler(int signum) { ev_flag = 1; char msg[] = "Signal caught\n"; write(STDOUT_FILENO, msg, strlen(msg)); } void install_signal_handler() { struct sigaction act; act.sa_handler = signal_handler; act.sa_flags = SA_RESETHAND; if (sigaction(SIGINT, &act, 0) + sigaction(SIGABRT, &act, 0) + sigaction(SIGQUIT, &act, 0) + sigaction(SIGTERM, &act, 0) + sigaction(SIGPIPE, &act, 0) + sigaction(SIGCHLD, &act, 0) + sigaction(SIGSEGV, &act, 0) + sigaction(SIGTRAP, &act, 0) < 0) { perror("sigaction()"); abort(); } } int main(void) { install_signal_handler(); FILE *child_proc; child_proc = popen(child_proc_cmd, "w"); while (ev_flag == 0) { if (fwrite(data, 1, data_size, child_proc) != data_size) { perror("fwrite()"); break; } } printf("\nThe evloop quitted, gracefully\n"); pclose(child_proc); return 0; }
该设计多数场景运行正常,但当子进程被kill <pid>或kill -KILL <pid>杀死时,即便signal_handler()被调用,父进程也无法执行到printf("\nThe evloop quitted, gracefully\n")语句,请问这是什么原因?当前设计存在哪些缺陷?
原因分析与缺陷总结
核心原因:fwrite被信号中断后的错误处理缺失
当子进程被杀死时,父进程会收到SIGCHLD信号,信号处理器设置ev_flag=1,但此时父进程大概率正阻塞在fwrite调用上。fwrite作为标准库缓冲IO函数,被信号中断后会返回部分写入字节数或设置errno为EINTR(具体取决于系统实现)。
你的代码仅判断fwrite返回值是否等于data_size,不等就直接break循环。但此时ev_flag已设为1,却无法执行收尾代码——因为fwrite出错后,进程可能因未处理的错误状态直接退出,或者pclose在子进程已终止的场景下触发额外信号/错误,导致进程提前终止。
另外,子进程被kill -KILL杀死时,不会主动关闭管道写端,父进程向管道写入会触发SIGPIPE信号。虽然你捕获了SIGPIPE,但SA_RESETHAND标志会让信号处理完成后,该信号的处理方式重置为默认(即终止进程)。如果fwrite被SIGPIPE中断后,后续若再次触发SIGPIPE(比如残留的写入操作),进程会直接被终止,根本走不到收尾步骤。
设计缺陷
SA_RESETHAND标志误用:该标志会让信号处理函数仅执行一次,之后信号处理方式恢复为默认。比如SIGCHLD可能多次触发(多子进程场景),SIGPIPE在管道断连后再次写入也会触发,此时进程会直接被终止,无法执行收尾。- 未处理
EINTR错误:标准库函数被信号中断时会返回EINTR,你的代码未检查该错误,直接break循环,后续可能因未清理资源导致异常。 fwrite错误处理不完整:仅判断是否写入全部数据,未区分部分写入、信号中断、其他错误等场景,导致流程混乱。- 信号处理函数使用非必要操作:虽然
write是异步信号安全函数,但strlen在部分场景下存在风险,且信号处理函数应只做最必要的操作(比如仅设置标志位),避免额外IO引入不确定性。 - 未监控管道状态:子进程终止后管道写端关闭,父进程写入会触发
SIGPIPE,此时即便设置了ev_flag,fwrite的错误也可能导致进程提前退出。
修复建议
- 移除
SA_RESETHAND标志,确保信号处理函数可重复触发; - 在
fwrite出错时检查errno,若为EINTR则继续循环(因ev_flag已设置),再处理其他错误; - 简化信号处理函数,仅保留设置
ev_flag的逻辑; - 循环中除了检查
ev_flag,定期检查子进程状态,避免完全依赖信号触发; - 使用
fcntl将管道设置为非阻塞,或通过select/poll监控管道可写状态,避免阻塞在fwrite上。
内容的提问来源于stack exchange,提问作者D.J. Elkind
相关产品推荐
相关产品推荐

