Shell管道中如何抵御孤儿进程与故障应用的异常影响
已确认不一致问题源于工具less的接口变更,该变更破坏了向后兼容性。从603版本开始,必须使用--exit-follow-on-close选项才能保持旧版本无需指定该选项时的行为。
注意:本问题并非《在目标机器上让ssh后台执行命令》的重复问题。另一问题涉及可能收到挂起信号的后台进程,而当前问题聚焦于前台进程
my_buggy_application终止后,管道如何正常终止——该进程在管道中处理I/O,且可能遗留孤儿进程。实际上,即使使用
nohup,当前台进程终止后,此类场景下管道仍会保持打开状态。nohup默认会断开进程与标准I/O的连接,这是被提议的重复问题的答案仅有的可能适用于当前场景的点,但无需nohup也能轻松实现该断开操作,且这会破坏当前场景的预期功能。因此nohup与本问题无关,本问题也未涉及挂起信号,提议的重复问题并非真正重复,其解决方案对当前场景无效。
测试验证
初始状态
打开新的Bash会话,可看到shell没有持久子进程:
% pstree $$ bash───pstree
简单管道测试
进一步的简单测试会得到可预测的结果:
% ( echo hello; echo world ) | cat hello world % pstree $$ bash───pstree
含后台进程的管道测试
但稍复杂的测试结果则不那么明显:
( echo hello; sleep infinity & echo world ) | cat hello world ^C
输出并不意外,但不易察觉的是该命令不会自动退出,必须手动终止。
类似地,异步调用该命令的情况:
% ( echo hello; sleep infinity & echo world ) | cat & hello world % pstree $$ bash─┬─cat └─pstree
由于命令以异步方式调用,会立即返回shell,但会留下未关闭的子进程,需手动终止。同时sleep命令作为孤儿进程持续存在,虽不在本地进程树中显示,但仍在系统进程表内。
问题核心
尽管孤儿进程的出现并不意外,但通常期望管道的输入端在直接进程退出后立即关闭,即使子进程仍存在;若持续存在的是已退出直接子进程的孤儿后代,这种需求则更为迫切。
假设将sleep infinity &替换为调用一个存在bug的应用my_buggy_application,该应用无法让所有后代进程正确退出,且调用无需异步:
( echo hello; my_buggy_application; echo world ) | cat
通常shell脚本作者无法控制命令是否会遗留孤儿进程,但自然希望无论是否存在此类情况,管道命令的行为都能保持可预测。实际上,孤儿进程并非都是无意或有害的,my_buggy_application创建孤儿进程的行为可能是合理的。
解决方案
针对这类问题,可通过以下几种方式让Shell脚本避免不可预测的结果:
显式关闭文件描述符:在子shell中执行命令时,显式关闭标准输出的文件描述符,确保直接进程退出后管道输入端被关闭。例如:
( echo hello; my_buggy_application; echo world; exec >&- ) | catexec >&-会强制关闭当前子shell的标准输出,不管是否有孤儿进程持有该描述符的副本,管道的输入端都会被标记为关闭,cat进程在读取完所有输出后会正常退出。使用进程组管理:将子shell中的所有进程纳入同一个进程组,当直接进程退出时,向整个进程组发送终止信号,清理遗留的孤儿进程(如果允许的话):
( set -m; echo hello; my_buggy_application; echo world; kill 0 ) | catset -m启用作业控制,kill 0会向当前进程组的所有进程发送SIGTERM信号,确保所有关联进程被终止,避免孤儿进程遗留。但这种方式需要确保my_buggy_application创建的子进程会继承进程组,且你有权限终止这些进程。使用命令替换捕获输出:如果不需要实时输出,可通过命令替换捕获所有输出后再传递给管道,这样子shell退出后管道会自动关闭:
echo "$( echo hello; my_buggy_application; echo world )" | cat命令替换会等待子shell内的所有命令执行完成(即使有孤儿进程,只要子shell本身退出),然后将输出一次性传递给
cat,此时管道的生命周期由命令替换的结果传递决定,不会因孤儿进程而保持打开。监控并关闭管道:通过
coproc或自定义文件描述符监控管道状态,当直接进程退出时主动关闭管道:coproc cat { cat; } ( echo hello; my_buggy_application; echo world; exec 1>&- ) >&${cat[1]} wait ${cat_PID}这里通过协程创建
cat进程,子shell显式关闭标准输出后,协程的cat会在读取完所有数据后正常退出。
内容的提问来源于stack exchange,提问作者brainchild

