为什么在通过管道传输到终止子命令时,bash while 循环不会退出?

为什么在通过管道传输到终止子命令时,bash while 循环不会退出?

为什么下面的命令不退出?循环不退出,而是无限运行。

虽然我使用更复杂的设置发现了这种行为,但该命令的最简单形式简化为以下内容。

不退出:

while /usr/bin/true ; do echo "ok" | cat ; done | exit 1

上面没有拼写错误。每个“|”都是一个管道。“exit 1”代表另一个运行并退出的进程。

我期望“exit 1”会在 while 循环中引发 SIGPIPE(在没有读取器的管道上写入)并跳出循环。但是,循环仍在继续运行。

为什么命令不停止?

答案1

这是由于实施中的选择。

在 Solaris 上运行相同的脚本ksh93会产生不同的行为:

$ while /usr/bin/true ; do echo "ok" | cat ; done | exit 1
cat: write error [Broken pipe]

触发该问题的是内部管道,如果没有它,无论 shell/OS 如何,循环都会退出:

$ while /usr/bin/true ; do echo "ok" ; done | exit 1
$

cat在 bash 下收到 SIGPIPE 信号,但 shell 仍在迭代循环。

Process 5659 suspended
[pid 28801] execve("/bin/cat", ["cat"], [/* 63 vars */]) = 0
[pid 28801] --- SIGPIPE (Broken pipe) @ 0 (0) ---
Process 5659 resumed
Process 28801 detached
Process 28800 detached
--- SIGCHLD (Child exited) @ 0 (0) ---
Process 28802 attached
Process 28803 attached
[pid 28803] execve("/bin/cat", ["cat"], [/* 63 vars */]) = 0
Process 5659 suspended
[pid 28803] --- SIGPIPE (Broken pipe) @ 0 (0) ---
Process 5659 resumed
Process 28803 detached
Process 28802 detached
--- SIGCHLD (Child exited) @ 0 (0) ---
Process 28804 attached
Process 28805 attached (waiting for parent)
Process 28805 resumed (parent 5659 ready)
Process 5659 suspended
[pid 28805] execve("/bin/cat", ["cat"], [/* 63 vars */]) = 0
[pid 28805] --- SIGPIPE (Broken pipe) @ 0 (0) ---
Process 5659 resumed
Process 28805 detached
Process 28804 detached
--- SIGCHLD (Child exited) @ 0 (0) ---

猛击文档指出:

贝壳等待所有命令在管道返回值之前终止。

肯尼亚文档指出:

每个命令(可能最后一个命令除外)都作为单独的进程运行;shell等待最后一条命令终止。

POSIX状态:

如果管道不在后台(参见异步列表),则 shell应等待最后一个命令指定要完成的管道,以及也可以等待所有命令去完成。

答案2

这个问题困扰了我好几年。感谢 jilliagre 的指引。

在我的 Linux 机器上重新表述一下这个问题,它按预期退出:

while true ; do echo "ok"; done | head

但如果我添加一个管道,它就会不是按预期退出:

while true ; do echo "ok" | cat; done | head

这让我沮丧了好几年。通过考虑 jilliagre 写的答案,我想出了这个绝妙的解决方案:

while true ; do echo "ok" | cat || exit; done | head

已确认...

嗯,不完全是。下面是一些更复杂的事情:

i=0
while true; do
    i=`expr $i + 1`
    echo "$i" | grep '0$' || exit
done | head

这不起作用。我添加了 ,|| exit所以它知道如何提前终止,但第一个echo与 不匹配,grep所以循环立即退出。在这种情况下,你真的不关心 的退出状态grep。我的解决方法是添加另一个cat。所以,这里有一个名为“tens”的人为脚本:

#!/bin/bash
i=0
while true; do
    i=`expr $i + 1`
    echo "$i" | grep '0$' | cat || exit
done

当以 运行时,此代码正确终止tens | head。感谢上帝。

答案3

先决条件:https://askubuntu.com/a/678922

最好使用临时文件描述符和进程替换。这将按照您的意愿终止(请记住,罪魁祸首不是catwhile

以下内容将永远停留

while true; do cat /dev/zero; done | head -c 5 | od

但是使用进程替换,我们可以杀死上游。

(head -c 5 | od)  < <(while true; do cat /dev/zero; done)

答案到此结束


使用黑客手段(有风险,可能并不总是有效)

while cat /dev/zero; do true; done | head -c 5 | od

观众需要理解如何cat接收SIGPIPE信号,并且true不会产生干扰(提示:如果使用有限流而不是,可能会产生干扰/dev/zero

未来的调查

# what about this?
while cat finite_file; do cat finite_file; done | head -c 5 | od

相关内容