从 bash 脚本中终止损坏管道上的进程

从 bash 脚本中终止损坏管道上的进程

考虑代码:

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.

相关内容