【问题标题】:bash: wait for single process from a pipelinebash:等待管道中的单个进程
【发布时间】:2021-10-09 11:11:48
【问题描述】:

我正在启动 2 个进程,其中一个管道连接到另一个进程,然后将它们都设置为后台。 我想等到第二个过程完成后再决定第一个的信仰。

下面的 sn-p 给出了我期望的输出(“exit 1”)。但是,wait 似乎要等待两个进程完成(所以 5 秒),这不是我想要的。

我对@9​​87654322@ 有什么误解? 我怎样才能让它只等待管道中的第二个进程并决定稍后等待第一个(或杀死它)?

#!/bin/sh
{
  sleep 5
  exit 5
} | {
  sleep 1
  exit 1
} &
wait $!
echo "exit $?"

【问题讨论】:

    标签: bash shell subprocess wait


    【解决方案1】:

    当你写{...} | {...} &时,至少涉及到三个进程:

    1. | 的 LHS 中执行命令的那个
    2. |的RHS中执行命令的那个
    3. 执行整个管道的那个

    &list 终止符。它不适用于前面的简单或复合命令,甚至前面的管道。整个命令在单个后台进程中执行:

    a | b || c | d && e | f &
    

    & 不仅仅适用于f,或e | f,甚至c | d && e | f。它适用于整个管道序列。

    @KamilCuk 提到了命名管道。我更喜欢它们而不是进程替换,因为它允许您统一处理两个进程,至少就语法而言。 (此外,命名管道可以在任何符合 POSIX 的 shell 中工作,而不仅仅是 bash。)

    mk_fifo p1
    
    { sleep 5; exit 5; } > p1 & p1_pid=$!
    { sleep 1; exit 1; } < p1 & p2_pid=$!
    
    wait $p2_pid
    
    # Now, kill or wait for the first process as desired
    

    【讨论】:

      【解决方案2】:

      您没有误解 wait 内置行为。你误解了shell管道处理。

      wait 命令按预期运行,它等待管道右侧部分(sleep 1)的子shell 结束,并等待五秒钟,因为右侧部分无效终止,直到其管道输入关闭。仅当管道的左侧部分在其终止时(sleep 5)关闭它的管道输入,因此在五秒钟后。

      要等待管道的正确部分,您可以使用信号来知道正确的部分何时即将像这样完成:

      #!/bin/dash
      {
        sleep 5
        exit 5
      } | {
        sleep 1
        kill -s USR1 $$
        exit 1
      } &
      trap "echo \"Second shell finished\";" USR1
      wait $!
      echo "exit $?"
      

      这样,您将从正确的部分获得一个信号,然后wait 命令将只等到它收到这些信号(wait 命令等待任何信号状态更改,而不仅仅是终止)。 kill -s USR1 $$ 将 USR1 信号发送到父 shell。请注意,它可能意味着当前子shell的进程终止,您必须知道该进程将如何处理USR1(我猜sleep会忽略它)。

      但是,您无法从管道右侧部分的外壳中获取正确的退出代码,因为在管道输入关闭之前它不会有效终止。更准确地说,您将在此处获得退出状态 138,它是信号状态 USR1(值 10)加上 POSIX 信号基数(128),而不是子外壳退出状态。如果需要,您可以使用不同的信号(USR1、USR2 (POSIX)、非 POSIX: .. USR64)来表示退出状态,或者使用 subshel​​l STDOUT 或环境变量来传输退出状态。

      【讨论】:

      • &amp; 将整个管道(而不仅仅是 RHS)置于后台。当 RHS 退出时,管道的那一端会关闭,但 LHS 会继续,直到它尝试写入管道的一端(此时它将收到 SIGPIPE 信号,默认情况下终止进程)或自然退出。 LHS 完成后,后台进程最终完成。
      • @chepner 你是对的,但是在我看来,OP 的问题更多是关于为什么它的 wait 命令与来自 RHS 子shell 的特定 PID 在这个特定子shell “终止”。在这里,PID 正是 RHS 子shell PID,而不是包含整个管道的子shell 的 PID。无论如何,您认为我们应该在答案中添加&amp; 后台解释吗?
      • 不,$!是后台进程的进程ID,执行整个管道。忽略涉及exec 的任何bash 优化,这是一个与执行管道的RHS 不同的过程。 a | b &amp;( a | b ) &amp; 相同,而不是 a | (b &amp;)
      • @chepner 在 Debian Buster 上使用 POSIX shell(破折号),我得到了$! 中的 RHS 子shell PID,而不是包含整个管道的子shell 的 PID。 (我在&amp; 之后添加echo $!ps -efa 来检查它)。也许关于管道的shell选项可能会改变......
      • dash 也可能使用exec 技巧。 (没有说 必须 管道的任一侧都有单独的进程,但从概念上讲,&amp; 绝对适用于管道,而不是 RHS。这意味着管道直到两者都不会完成它的各个方面都完成了,无论可能涉及什么过程。)
      【解决方案3】:

      您可以创建fifos 并自己控制每个进程,而不是使用管道。

      最简单的方法就是使用进程替换。

      {
         cmd2
      } < <(cmd1) &
      

      或者以同样的方式,如果需要明确的生命周期控制,则在第一个命令中使用coprocess。

      另一种解决方案是在管道中的 shell 和父 shell 之间进行进程间通信,无论是信号还是文件,或者 pid 可以发送到父进程,这样父进程就可以while kill -0最后一个管道的 pid。

      : > file
      ... | {
        sleep 1
        echo end > file
      } &
      while [[ $(cat file) != "end" ]]; do
         sleep 0.1
      done
      

      【讨论】:

      • 我的部分问题是关于理解为什么等待不符合我的预期。进程替换可能无法移植到其他 shell。 coproc 也一样。而且我宁愿不碰文件系统。但可以肯定的是,这些变通办法会奏效:)
      • 因为它想让运行管道的shell结束。
      • 好吧,看看jobs -l 输出,很明显$! 指的是第二部分的pid,而不是第一部分。两者都是当前进程的直接子进程。那么当我明确地等待第二个进程的 pid 时,“运行管道的 shell”在哪里发挥作用?
      • @MathijsKwik KamilCuk 是对的,但是我添加了一个答案(我希望)通过避免轮询和文件 I/O 的方式来解释更多“为什么”。
      猜你喜欢
      • 2013-10-20
      • 1970-01-01
      • 2014-07-02
      • 2016-01-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-01-30
      相关资源
      最近更新 更多