下面的代码是我在Linux机器上运行的一个简单的bash脚本,我想知道为什么每个输出之间的时间间隔是四 秒代替八?
$ for test in test1 test2 test3; do (echo ${test}; sleep 4s; echo hop2; sleep 4s; echo hop3) | date; done
Sun 11 Apr 2021 12:42:27 AM +07
Sun 11 Apr 2021 12:42:31 AM +07
Sun 11 Apr 2021 12:42:35 AM +07
尽管将后一个时间值增加得稍长,但每次输出之间的时间间隔仍然是四秒。
$ for test in test1 test2 test3; do (echo ${test}; sleep 4s; echo hop2; sleep 50s; echo hop3) | date; done
Sun 11 Apr 2021 12:42:44 AM +07
Sun 11 Apr 2021 12:42:48 AM +07
Sun 11 Apr 2021 12:42:52 AM +07
这非常令人困惑,如果有人能解释这一点,我将非常感激。
更令人困惑的是,如果我把date
命令放在前面,看起来没有任何睡眠命令被执行:
$ for test in test1 test2 test3; do (date; echo ${test}; sleep 50s; echo hop2; sleep 50s; echo hop3) | date; done
Sun 11 Apr 2021 01:22:35 AM +07
Sun 11 Apr 2021 01:22:35 AM +07
Sun 11 Apr 2021 01:22:35 AM +07
答案1
为了澄清这一点,让我在回显“hop2”之前和之后向 stderr 添加一些调试输出(绕过管道):
$ for test in test1 test2 test3; do
(echo ${test}; sleep 4s; echo before hop2 >&2;
echo hop2; echo after hop2 >&2; sleep 4s; echo hop3) | date;
done
Sat Apr 10 11:29:46 PDT 2021
before hop2
Sat Apr 10 11:29:50 PDT 2021
before hop2
Sat Apr 10 11:29:54 PDT 2021
before hop2
注意echo after hop2 >&2
从不执行,并且它后面的命令:第二个sleep
和echo hop3
.
据我了解,这就是发生的事情。在循环内,两个单独的进程并行执行,第一个进程的输出通过管道传输到第二个进程的输入。两个进程执行:
echo ${test}
sleep 4s
echo before hop2 >&2
echo hop2
echo after hop2 >&2
sleep 4s
echo hop3
和
date
以下是大致的执行顺序(前 3 个步骤和第 4 步开始的确切顺序有些随机):
- 进程1执行
echo ${test}
;这会将“test1”(和换行符)写入管道,并在管道中进行缓冲,以便稍后读取。 - 执行进程 2
date
,将当前日期打印到终端。 - 进程 2 退出,关闭管道的末端。
- 进程 1 执行
sleep 4s
。 - 4 秒后,进程 1 执行
echo before hop2 >&2
,将“before hop2”打印到终端。 - 流程1尝试执行
echo hop2
,但由于管道的唯一读取器已关闭它,因此它会收到 SIGPIPE 错误。这显然会导致整个子 shell 进程(不仅仅是echo
命令)退出。
请注意,发生这种情况只是因为echo
shell 是内置的;如果您使用/bin/echo hop2
(外部命令,而不是 shell 的echo
内置命令),它将sleep
按您的预期执行第二个命令。
顺便说一句,这在不同的 shell 之间是相对一致的。我在 bash、zsh、dash 和 ksh(交互式)中运行得到了相同的结果。脚本中的 ksh 有点不同,因为它显然不会等待进程 1 退出才继续,因此所有date
s 都会立即执行,后面是一系列“before hop2”行(4 秒后)。
答案2
(echo ${test}; ...) | date
你想在这里做什么?您正在将数据传输到 的标准输入date
,但date
不读取任何输入。它只是打印日期并退出。
退出后date
,管道关闭,之后打印到它的任何数据都无处可去,写入管道的进程(子 shell (echo; sleep; echo; sleep)
)收到 SIGPIPE 信号,然后它就终止了。
这就是管道的一般工作方式。如果左侧可以产生大量输出,甚至可能是任意数量,那么信号就是在右侧失去兴趣后告诉它停止的信号。
例如,在类似的情况下cat /dev/zero | head -c128 > /dev/null
,信号最终会杀死,cat
因此它不会无限期地留下来。 shell 和 shell 都不会cat
打印错误消息。管道这样工作只是正常操作的一部分。对于这里手头的情况也是如此。 (在某些情况下,您确实会收到错误消息,只是不要指望总是会收到错误消息。)
(外层循环不会影响结果,所以我放弃了它。您可以用它time ( ... ) | ...
来测量管道所花费的时间。)
在 Bash 中,您可以从数组中检查管道中命令的退出状态PIPESTATUS
:
$ ( echo ${test}; sleep 4s; echo hop2; sleep 50s; echo hop3; ) |
date; declare -p PIPESTATUS
Sat Apr 10 21:34:56 EEST 2021
declare -a PIPESTATUS=([0]="141" [1]="0")
SIGPIPE 至少在 Linux 上是编号 13,因此它与显示的退出状态 141 = 128 + 信号编号匹配。 (进程也可以以状态 >= 128 正常退出,但这里不是这种情况。)
或者,您可以strace
看看会发生什么:
$ strace -f bash -c '( echo ${test}; sleep 4s; echo hop2;
sleep 50s; echo hop3; ) | date'
...
[pid 31647] write(1, "hop2\n", 5) = -1 EPIPE (Broken pipe)
[pid 31647] --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=31647, si_uid=1000} ---
[pid 31647] +++ killed by SIGPIPE +++
...
另一方面,如果有东西读取管道右侧的输入,则不会有 SIGPIPE,并且整个管道将总共休眠 54 秒:
$ time ( echo ${test}; sleep 4s; echo hop2; sleep 50s; echo hop3; ) |
cat > /dev/null
real 0m54.005s
user 0m0.000s
sys 0m0.000s
或者,如果您忽略写入端的 SIGPIPE:
$ time ( trap '' PIPE; echo foo; sleep 4s; echo hop2; sleep 10s;
echo hop3; ) | date
Sat Apr 10 21:57:07 EEST 2021
bash: echo: write error: Broken pipe
bash: echo: write error: Broken pipe
real 0m14.006s
user 0m0.004s
sys 0m0.000s
如果忽略 SIGPIPE,则 shell 在写入关闭的管道时会定期返回错误,并且会为此打印错误。
请注意,在管道左侧写入管道与右侧关闭管道之间还存在计时问题。
如果我做这样的事情:
( echo foo; echo bar; sleep 4s; ) | date;
它sleep
跑了。但如果我将两者替换echo
为for i in {1..100}; do echo foo; done;
,则 LHS 在到达 之前就死掉了sleep
。 (这里的比赛将取决于系统。)
并将另一起案件date
视为 LHS 上的第一件事:
(date; echo ${test}; sleep 50s; echo hop2; sleep 50s; echo hop3) | date
这也是由于时间问题造成的。由于date
是一个外部命令,因此 shell 启动它可能比在内部处理 an 更慢echo
(毕竟,它是所有 shell 旁边的内置命令)。这使得date
右边的人更有可能赢得比赛并在第一个echo
写入之前关闭管道。
一般来说,如果您的目的不是将某些数据传递给另一端,那么写入管道并不是很有用,并且如果您还有其他事情要做,请确保处理可能的 SIGPIPE。
答案3
没有任何内容可发送,date
因为它在标准输入上没有接收到数据。
因此,使用 a|
是完全错误的。我相信你需要使用&&
。
但问题是:
为什么每次输出之间的时间间隔是四秒代替八?
仍然有效,但解释起来很混乱。简而言之,之后的命令echo hop2
永远不会被执行。第二次睡眠从未使用过。
$ (echo test; sleep 4s; echo hop2; sleep 4s; echo hop3) | date; date
Sun 11 Apr 2021 05:30:28 AM UTC
Sun 11 Apr 2021 05:30:32 AM UTC
如果我们使用stderr
而不是stdout
执行所有命令:
$ (echo test >&2; sleep 4s; echo hop2 >&2; sleep 4s; echo hop3 >&2) | date; date
test
Sun 11 Apr 2021 05:32:36 AM UTC
hop2
hop3
Sun 11 Apr 2021 05:32:44 AM UTC
原因是它date
没有在 stdin 中接收输入,任何写入它的尝试都将得到 SIGPIPE 响应,并且整个 (bash) shell(位于 左侧的内容|
)将关闭。破坏列表中的任何其他命令。
您的示例中第一个成功的原因echo
取决于时间:
- 一开始,运行的 shell 将设置管道
|
。 date
为了实现这一点,它会打开启动命令的子命令。- 不等待任何东西,shell 返回到管道的另一端并(在另一个子进程中)要求它执行
echo ${test}
。 - 由于管道仍然打开(
date
尚未关闭),因此 echo 的输出被缓冲并且命令sleep 4s
被执行。 - 该
date
命令有足够的时间来结束并关闭管道。 - 现在,随着管道关闭,下一个
echo
将无法写入它。它将导致 SIGPIPE。 - 收到 SIGPIPE 后,左侧的子 shell
|
完全退出(回显是内置的,内置错误会导致整个 shell 失败)。 - 管道上不再执行任何命令。
这就是为什么:
$ (sleep 1; echo test; sleep 4s; echo hop2 >&2; ) | date ; date
Sun 11 Apr 2021 06:10:04 AM UTC
Sun 11 Apr 2021 06:10:05 AM UTC
甚至不打印第一个回声。时间可以小至0.1秒。这足以允许date
结束。
它甚至可能是对外部命令的调用,该命令echo
足以延迟:
(/usr/bin/true; echo test; sleep 4s; echo confirm >&2 ) | date; date
Sun 11 Apr 2021 06:27:01 AM UTC
Sun 11 Apr 2021 06:27:01 AM UTC
你的for
循环就是上面的重复三遍。因此每个循环之间只有四秒。