我有一个 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 stdout
为printf ""
“写入”管道,则不会引发 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
,bc
或m4
):
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
希望有帮助。祝你好运!