如何在Shell管道中针对孤立进程与存在缺陷的应用程序保持鲁棒性
首先得明确:这绝对不是《让ssh在目标机器后台执行命令》的重复问题!那个问题聚焦的是后台进程可能收到挂起信号(hangup signal),但咱们现在的核心痛点是:当前台进程my_buggy_application终止后,管道怎么才能正常终止?毕竟这个前台进程在管道内处理I/O,还可能遗留孤儿进程。
哪怕用nohup,前台进程挂了之后管道照样会保持打开状态——nohup只是默认断开进程和标准I/O的连接,这才让那个问题的答案看起来在这儿能用,但其实不用nohup也能轻松断开,而且这么做会破坏咱们当前场景的预期功能。所以nohup跟咱们这事儿完全不沾边,咱们根本没涉及挂起信号,那个被标记的重复问题其实根本不重复,解决方案在这儿没用。
先看几个直观的测试现象
正常场景:Shell没有持久子进程
打开新的Bash会话,用pstree能清楚看到Shell没有遗留子进程:
% pstree $$ bash───pstree
简单管道测试,结果完全符合预期:
% ( echo hello; echo world ) | cat hello world % pstree $$ bash───pstree
异常场景:管道无法自动终止
但换个稍复杂的命令,问题就来了:
( echo hello; sleep infinity & echo world ) | cat hello world ^C
输出倒是不意外,但要命的是这个命令不会自动退出,必须手动按Ctrl+C干预终止。
再试异步调用的情况:
% ( echo hello; sleep infinity & echo world ) | cat & hello world % pstree $$ bash─┬─cat └─pstree
命令会立刻回到Shell,但留下了未关闭的cat子进程,得手动终止。而且sleep会以孤儿进程的形式持续存在——它不在本地进程树里,但还留在系统进程表中。
核心诉求:让管道表现可预测
孤儿进程的出现可能不算意外,但咱们作为脚本编写者,通常期望:哪怕存在子进程/孤儿进程,输入端管道也应该在直接进程退出后立即关闭。如果遗留的是已退出直接进程的孤儿后代,这种需求就更迫切了。
比如把sleep infinity &换成有缺陷的应用my_buggy_application——它可能没让所有子进程正确退出,而且调用它不一定是异步的:
( echo hello; my_buggy_application; echo world ) | cat
咱们没法控制第三方命令会不会遗留孤儿进程,但肯定希望管道不管有没有这种情况,都能表现得可预测(甚至有时候孤儿进程是有意设计的,比如my_buggy_application创建孤儿进程本身就是合理逻辑)。
实用解决方案:保护Shell脚本免受不可预测结果影响
这里有几个靠谱的方法,按需选用:
1. 用进程组管控所有关联进程
Bash里可以开启作业控制,或者手动把命令组放到新进程组里,主进程退出时给整个进程组发信号清理:
#!/bin/bash # 定义清理函数 cleanup() { kill $CAT_PID 2>/dev/null pkill -P $LEFT_PID 2>/dev/null } # 设置陷阱,脚本退出时自动清理 trap cleanup EXIT # 启动左侧命令并记录PID ( echo hello; my_buggy_application; echo world ) & LEFT_PID=$! # 启动cat并记录PID cat & CAT_PID=$! # 等待左侧命令完成 wait $LEFT_PID
这样不管my_buggy_application有没有留孤儿进程,脚本退出时都会清理相关进程,管道也能正常关闭。
2. 关闭标准I/O继承
如果孤儿进程不需要继承管道的标准I/O,启动my_buggy_application时明确关闭或重定向其标准流:
( echo hello; my_buggy_application < /dev/null > /dev/null 2>&1; echo world ) | cat
这样my_buggy_application不会持有管道的I/O描述符,左侧子shell退出后,管道输入端就会关闭,cat读取完数据后会自动退出。
3. 用timeout强制管控进程生命周期
如果能预估my_buggy_application的运行时长,用timeout给它设个最长运行时间,到点强制终止:
( echo hello; timeout 30s my_buggy_application; echo world ) | cat
要是进程不响应SIGTERM,还可以加-k参数发送SIGKILL强制杀掉:
( echo hello; timeout -k 5s 30s my_buggy_application; echo world ) | cat
4. 利用子shell的进程组特性
把左侧命令放到子shell里,结合kill命令终止整个子shell的进程组:
( echo hello; my_buggy_application; echo world ) & LEFT_PID=$! cat & CAT_PID=$! wait $LEFT_PID kill -TERM -$LEFT_PID 2>/dev/null kill $CAT_PID 2>/dev/null
这里kill -TERM -$LEFT_PID会给整个进程组发终止信号,能清理掉my_buggy_application的所有子进程。
内容的提问来源于stack exchange,提问作者brainchild

