如何判断水管是否破损?

如何判断水管是否破损?

我有一个 POSIX shell 脚本,其标准输出1重定向到管道。在脚本执行的某个时刻,管道将破裂,我想知道(在我的 shell 脚本中)何时发生这种情况。

所以我尝试了这个:

(
  trap "" PIPE  # prevent shell from terminating due to SIGPIPE
  while :; do
    echo trying to write to stdout >&2
    echo writing something to stdout || break
    echo successfully written to stdout >&2
    sleep 1
  done
  echo continuing here after loop >&2
) | sleep 3

哪个打印:

trying to write to stdout
successfully written to stdout
trying to write to stdout
successfully written to stdout
trying to write to stdout
successfully written to stdout
trying to write to stdout
sh: 5: echo: echo: I/O error
continuing here after loop

在此示例中,我们使用sleep脚本替换其标准输出的程序。 3 秒后,sleep终止并且管道破裂。

我们仅将 stdout 传输到sleep,因此我们仍然可以使用 stderr 来处理中间的一些调试消息。

根据规定,写入损坏的管道会导致 SIGPIPE,其默认操作是终止程序POSIXsignal.h。这就是为什么我们必须接收trap信号并忽略它。

终止后sleep,管道中断,随后echo writing something to stdout导致 SIGPIPE,该信号被捕获(被忽略)、echo失败并|| break退出循环。脚本继续执行,没有任何问题。

所以我上面的例子工作得很好。明显的主要缺点是,我向管道发送大量“向标准输出写入内容”的垃圾邮件,只是为了查明管道是否仍在工作。如果我替换echo writing something to stdoutprintf ""“写入”管道,则不会引发 SIGPIPE 并且循环会继续,即使管道早已损坏。

我能做什么呢?

答案1

在脚本执行的某个时刻,管道会破裂,我想知道什么时候会发生这种情况。

只有当你尝试写入管道时你才能知道这一点。

根据Linux手册页,在我看来,对write()没有读取器的管道的写入端的任何调用都应该给出信号/错误,即使写入零字节也是如此。但是如果没有什么可打印的,我尝试的 shell 会跳过整个系统调用,所以这没有帮助。

如果您确实写入了非零量的数据,您可能会发现脚本在某个时刻在写入时被阻塞,也就是说,如果读取器忽略完成其工作并且管道缓冲区已满。

话又说回来,你在评论中说:

我想我基本上想使用 shell 脚本中的 select/poll 。

...在这种情况下,您确实应该从 shell 切换到正确的编程语言。或者只是切换到 Zsh,它具有zselect可用作前端的模块select()https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#The-zsh_002fzselect-Module

我确信不会select()帮助您找到管道的读取端何时关闭。

答案2

至少在 Linux 和 FreeBSD 上,使用poll()掩码POLLERR可以检测损坏的管道。

poll()POSIX 工具箱中没有 CLI 界面,但您可以使用perl通常可用的界面(与许多 POSIX 实用程序相反,例如pax,bcm4):

perl -MIO::Poll -e '$p=IO::Poll->new; $p->mask(STDOUT,POLLERR); $p->poll'

当标准输出上的管道损坏时将返回。

对于在 ssh 客户端终止时终止远程命令的用例:

ssh host '
  exec perl -MIO::Poll -we '\''
    $SIG{CHLD} = sub{wait; exit($? & 127 ? 128|($?&127) : $?>>8)};
    exec "sleep 3600 # example" unless fork;
    $p = IO::Poll->new;
    $p->mask(STDOUT, POLLERR);
    $p->poll;
    kill "HUP", 0'\'

请注意,在 Linux 上,管道可以被/proc/$pid/fd/$fd以读或读+写模式打开的人不间断地破坏,其中是以写模式打开管道的$fd进程的 fd 。$pid

$ exec 3> >(:)
$ perl -MIO::Poll -e '$p=IO::Poll->new; $p->mask(STDOUT,POLLERR); $p->poll'  >&3 && echo broken
broken
$ exec 4< /dev/fd/3
$ echo unbroken >&3
$ cat <&4
unbroken

在我看来,与其去调查这种情况,不如忍受它并处理这种情况。

对于 shell,whereprintf是内置的:

(
  trap 'echo>&2 Pipe is broken' PIPE
  while printf 'Whatever\n'; do
    sleep 1
  done
) | sleep 5

将处理 SIGPIPE。如果printf不是内置的,那么执行它的进程将因 SIGPIPE 而死亡。您可以根据退出状态进行检查[ "$(kill -l "$?") = PIPE ]

如果您忽略 SIGPIPE,例如trap '' PIPE进程(包括子进程)在写入损坏的管道时不会收到 SIGPIPE,但它们write()仍然失败EPIPE(该错误通常通过退出进程来处理)。

编辑

正如@TheDiveO 所指出的这个类似的问题,Linux select()(FreeBSD 也是如此)将在管道损坏时将打开的 fd(即使是在只写模式下)返回到管道(如果它位于监视的 fd 列表中)阅读

$ zmodload zsh/zselect
$ (zselect -r 1; echo>&2 done) | sleep 1
done

因此,如果 sshing 到登录 shell 是 zsh 的系统,您可以执行以下操作:

ssh host '
  zmodload zsh/zselect
  cmd 3> >(zselect -r 0 -r 1; kill -s HUP 0)'

zselect如果在其 stdin(到 cmd 的 fd 3 的管道)上看到 EOF 以检测 cmd 终止或在其 stdout 上检测到损坏的管道,则返回。然后它会杀死整个进程组 (0),包括cmd仍在运行的进程替换子 shell 和 shell。

答案3

tail即使不写入,您的操作系统也可能无法判断其标准输出何时是损坏的管道。看这个答案tail -f … | grep -q …为什么找到匹配项后不退出?

现代tail从GNU Coreutils就可以看出。

如果tail就是这么聪明如果您确定标准输出是一个管道,然后tail -f /dev/null在您的脚本中运行。该命令将在管道破裂后立即退出。

概念证明(它需要“smart” tail,例如来自 GNU Coreutils):

sh -c 'tail -f /dev/null; echo >&2 "Pipe broken!"' | sleep 5
#      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^    this is our script
#                                                  ^ this pipe will break after 5 seconds

笔记:

  • tail -f /dev/null不打印任何内容。
  • 如果 stdout 是一个常规文件,那么tail -f /dev/null就会不是永远自行退出。
  • tail在 Kubuntu 22.10 中使用 GNU Coreutils 8.32 进行了测试。
  • 相比之下:busybox tail -f /dev/null它并不“智能”,即使管道破裂后它也只是坐在那里。

答案4

我不认为那根管子会破裂。当您在 bash 中使用 OR 运算符 (||) 时,它几乎总是忽略它,因为它通常用于条件(if 语句)。

如果这个程序只是另一个程序的测试,我建议使用循环for

char='1 2 3 4 5' # Change this to whatever you want
for i in $char; do
  printf "Something"
done

你还可以做一个范围:

for i in {1..[your number]}; do
  printf "Something"
done

希望有帮助。祝你好运!

相关内容