bash 脚本不会执行所有 sleep 命令

bash 脚本不会执行所有 sleep 命令

下面的代码是我在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 从不执行,并且它后面的命令:第二个sleepecho hop3.

据我了解,这就是发生的事情。在循环内,两个单独的进程并行执行,第一个进程的输出通过管道传输到第二个进程的输入。两个进程执行:

echo ${test}
sleep 4s
echo before hop2 >&2
echo hop2
echo after hop2 >&2
sleep 4s
echo hop3

date

以下是大致的执行顺序(前 3 个步骤和第 4 步开始的确切顺序有些随机):

  1. 进程1执行echo ${test};这会将“test1”(和换行符)写入管道,并在管道中进行缓冲,以便稍后读取。
  2. 执行进程 2 date,将当前日期打印到终端。
  3. 进程 2 退出,关闭管道的末端。
  4. 进程 1 执行sleep 4s
  5. 4 秒后,进程 1 执行echo before hop2 >&2,将“before hop2”打印到终端。
  6. 流程1尝试执行echo hop2,但由于管道的唯一读取器已关闭它,因此它会收到 SIGPIPE 错误。这显然会导致整个子 shell 进程(不仅仅是echo命令)退出。

请注意,发生这种情况只是因为echoshell 是内置的;如果您使用/bin/echo hop2(外部命令,而不是 shell 的echo内置命令),它将sleep按您的预期执行第二个命令。

顺便说一句,这在不同的 shell 之间是相对一致的。我在 bash、zsh、dash 和 ksh(交互式)中运行得到了相同的结果。脚本中的 ksh 有点不同,因为它显然不会等待进程 1 退出才继续,因此所有dates 都会立即执行,后面是一系列“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跑了。但如果我将两者替换echofor 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取决于时间:

  1. 一开始,运行的 shell 将设置管道|
  2. date为了实现这一点,它会打开启动命令的子命令。
  3. 不等待任何东西,shell 返回到管道的另一端并(在另一个子进程中)要求它执行echo ${test}
  4. 由于管道仍然打开(date尚未关闭),因此 echo 的输出被缓冲并且命令sleep 4s被执行。
  5. date命令有足够的时间来结束并关闭管道。
  6. 现在,随着管道关闭,下一个echo将无法写入它。它将导致 SIGPIPE。
  7. 收到 SIGPIPE 后,左侧的子 shell|完全退出(回显是内置的,内置错误会导致整个 shell 失败)。
  8. 管道上不再执行任何命令。

这就是为什么:

$ (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循环就是上面的重复三遍。因此每个循环之间只有四秒。

相关内容