考虑代码:
printf '%s\n' 1 2 3 4 5 | head -n 2
其输出为:
1
2
我的理解是,当head
进程在读取前两行后破坏管道时,该printf
进程会捕获损坏的管道并优雅退出。
在我的一个脚本中,我使用一个 python 应用程序,它不会在损坏的管道上正常退出,而是继续运行,直到它终止完成它设定的操作或由于某些其他原因终止。每次它尝试写入标准输出并失败时,它都会抱怨broken pipe
.
这文章解释如何在 python 中处理损坏的管道。
我也许可以鼓励应用程序开发人员实现对破损管道的处理。出于某些原因,我怀疑他们会这样做。我也许可以分叉该应用程序,但它相当复杂,所以不愿意。
我似乎剩下的唯一选择是试图找到一种方法,让 shell (Bash) 在尝试写入管道时杀死进程。这可能吗?如果是这样,我该怎么做?
答案1
是的,python3 有一种令人讨厌的行为,它会忽略 SIGPIPE,并在write()
EPIPE 失败时引发异常,即使损坏的管道是生活和正常操作的一部分。
您确实可以通过使用一个包装器来解决它,该包装器转发其输出并使用 SIGTERM 杀死 python3,例如当输出变成损坏的管道时。
bash
不过,这不是我用来做这种事情的外壳。
你可以使用perl
:
perl -e '
$pid = open CMD, "-|", @ARGV;
$SIG{PIPE} = "IGNORE";
while (sysread CMD, $buf, 8192) {
if (!syswrite STDOUT, $buf) {
kill "TERM", $pid;
last;
}
}
close CMD;
exit($? & 127 ? ($? & 127) | 128 : $? >> 8)' -- your-python-program
如果必须是 shell,zsh 会是更好的选择:
zsh -c '
zmodload zsh/system
coproc {"$0" "$@" <&3 3<&-} 3<&0
trap "" PIPE
while sysread -s 8192 buf <&p; do
syswrite -- $buf || {
kill $! 2> /dev/null
break
}
done
wait $!' your-python-program
例子:
$ python3 -uc 'import time; print("foo"); time.sleep(1); print("bar")' | head -n1
foo
Traceback (most recent call last):
File "<string>", line 1, in <module>
BrokenPipeError: [Errno 32] Broken pipe
$ zsh -c that-code python3 -uc 'import time; print("foo"); time.sleep(1); print("bar")' | head -n1
foo
$ echo $pipestatus
143 0
$ kill -l 143
TERM
现在,让一个进程花费时间通过额外的管道推送输出,只是为了解决 python3 的烦恼,似乎有点过分了。
另一种方法可能是python3
从一开始就防止忽略这些信号。
$ strace -qqqZ -e signal=none -e rt_sigaction -e inject=rt_sigaction:retval=0 python3 -uc 'import time; print("foo"); time.sleep(1); print("bar")' | head -n1
foo
有效地strace
短路了所有 python3 的系统调用调用,rt_sigaction()
阻止其安装任何信号处理程序或更改(任何信号的)信号处理。
因此,它还消除了您在停止 python3 脚本时收到的烦人消息^C
,但如果您的 python 脚本确实安装了一些信号处理程序以在被杀死时进行清理,则可能会很危险。
最好只对有关 SIGPIPE 信号的调用执行此操作,但据我所知,strace 无法执行此操作。不过,您应该能够使用一些$LD_PRELOAD
技巧来实现它:
$ cat leave-sigpipe-alone.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
int sigaction(int signum, const struct sigaction * restrict act, struct sigaction * restrict oldact)
{
static int (*orig_sigaction)(int, const struct sigaction * restrict, struct sigaction * restrict) = 0;
if (!orig_sigaction)
orig_sigaction = (int (*)(int, const struct sigaction * restrict, struct sigaction * restrict)) dlsym (RTLD_NEXT, "sigaction");
if (signum == SIGPIPE) return 0;
return orig_sigaction(signum, act, oldact);
}
$ gcc -fPIC -shared -o leave-sigpipe-alone.so leave-sigpipe-alone.c -ldl
$ LD_PRELOAD=$PWD/leave-sigpipe-alone.so python3 -uc 'import time; print("foo"); time.sleep(1); print("bar")' | head -n1
foo
$ echo $pipestatus
141 0
$ kill -l 141
PIPE
python3
就像行为良好的可执行文件一样,被 SIGPIPE 悄悄杀死。
无论strace
有没有该-f
选项,sigaction()
调用都只会被父进程(执行的进程python3
)拦截。即使它执行单独的命令,也会继续拦截它们,但不会在子命令中执行此操作(除非您传递该-f
选项)。虽然该LD_PRELOAD
技巧将影响所有子进程,甚至在它们执行单独的命令(如果这些命令是动态链接的)之后也是如此。
与 python3 编程语言(大多数人使用的一种)的 cpython 解释器配合使用的一种更简洁的方法是在自定义中恢复 SIGPIPE 的默认信号配置sitecustomize.py
:
$ cat ~/lib/python3-leave-sigpipe-alone/sitecustomize.py
from signal import signal, SIGPIPE, SIG_DFL
signal(SIGPIPE,SIG_DFL)
$ PYTHONPATH=~/lib/python3-leave-sigpipe-alone python3 -uc 'import time; print("foo"); time.sleep(0.2); print("bar")' | head -n1
foo
$ echo $pipestatus
141 0
在这里,我们不更改系统的目录,sitecusomize.py
而是在专用目录中更改一个目录,因此我们可以PYTHONPATH
仅针对那些我们希望在尝试写入损坏的管道时被 SIGPIPE 杀死的 python3 脚本将变量设置为该目录。
那个似乎不适用于pypy3
.