【问题标题】:Get exit code of a background process获取后台进程的退出代码
【发布时间】:2010-12-06 21:59:04
【问题描述】:

我有一个从我的主 bourne shell 脚本调用的命令 CMD,它需要很长时间。

我想修改脚本如下:

  1. 作为后台进程并行运行命令 CMD (CMD &)。
  2. 在主脚本中,每隔几秒就有一个循环来监控生成的命令。该循环还将一些消息回显到标准输出,指示脚本的进度。
  3. 当生成的命令终止时退出循环。
  4. 捕获并报告衍生进程的退出代码。

有人可以给我指点来完成这个吗?

【问题讨论】:

  • ...获胜者是?
  • @TrueY .. 自从他提出问题那天起,bob 就没有登录过。我们不太可能知道!

标签: linux shell unix process


【解决方案1】:

怎么样...

# run your stuff
unset PID
for process in one two three four
do
    ( sleep $((RANDOM%20)); echo hello from process $process; exit $((RANDOM%3)); ) & 2>&1
    PID+=($!)
done

# (optional) report on the status of that stuff as it exits
for pid in "${PID[@]}"
do
    ( wait "$pid"; echo "process $pid complemted with exit status $?") &
done

# (optional) while we wait, monitor that stuff
while ps --pid "${PID[*]}" --ppid "${PID[*]}" --format pid,ppid,command,pcpu
do
    sleep 5
done | xargs -i date '+%x %X {}'

# return non-zero if any are non zero
SUCCESS=0
for pid in "${PID[@]}"
do
    wait "$pid" && ((SUCCESS++)) && echo "$pid OK" || echo "$pid returned $?"
done

echo "success for $SUCCESS out of ${#PID} jobs"
exit $(( ${#PID} - SUCCESS ))

【讨论】:

    【解决方案2】:

    1:在 bash 中,$! 保存最后执行的后台进程的 PID。无论如何,这会告诉你要监控什么进程。

    4: wait <n> 等到 PID <n> 的进程完成(它会阻塞直到进程完成,所以你可能不想在确定进程完成之前调用它),然后返回已完成进程的退出代码。

    2、3:ps或ps | grep " $! "可以告诉你进程是否还在运行。如何理解输出并决定距离完成有多近取决于您。 (ps | grep 不是白痴。如果你有时间,你可以想出一个更可靠的方法来判断进程是否仍在运行)。

    这是一个框架脚本:

    # simulate a long process that will have an identifiable exit code
    (sleep 15 ; /bin/false) &
    my_pid=$!
    
    while   ps | grep " $my_pid "     # might also need  | grep -v grep  here
    do
        echo $my_pid is still in the ps output. Must still be running.
        sleep 3
    done
    
    echo Oh, it looks like the process is done.
    wait $my_pid
    # The variable $? always holds the exit code of the last command to finish.
    # Here it holds the exit code of $my_pid, since wait exits with that code. 
    my_status=$?
    echo The exit status of the process was $my_status
    

    【讨论】:

    • ps -p $my_pid -o pid= 既不需要grep。
    • kill -0 $! 是判断进程是否仍在运行的更好方法。它实际上并不发送任何信号,只使用内置的 shell 而不是外部进程来检查进程是否处于活动状态。正如man 2 kill 所说,“如果 sig 为 0,则不发送任何信号,但仍会执行错误检查;这可用于检查进程 ID 或进程组 ID 是否存在。 "
    • @ephemient kill -0 如果您无权向正在运行的进程发送信号,则将返回非零值。不幸的是,在这种情况下和进程不存在的情况下,它都会返回1。这使得它很有用除非您不拥有该进程 - 如果涉及像 sudo 这样的工具或者如果它们是 setuid(并且可能删除 privs),即使是您创建的进程也是如此)。
    • wait 不返回变量$? 中的退出代码。它只是返回退出码,$?是最新前台程序的退出码。
    • 对于许多投票支持kill -0 的人。这是来自 SO 的同行评审参考,表明 CraigRinger 的评论是合法的:kill -0 will return non-zero for running processes... but ps -p will always return 0 for any running process。
    【解决方案3】:

    当我有类似需求时,我就是这样解决的:

    # Some function that takes a long time to process
    longprocess() {
            # Sleep up to 14 seconds
            sleep $((RANDOM % 15))
            # Randomly exit with 0 or 1
            exit $((RANDOM % 2))
    }
    
    pids=""
    # Run five concurrent processes
    for i in {1..5}; do
            ( longprocess ) &
            # store PID of process
            pids+=" $!"
    done
    
    # Wait for all processes to finish, will take max 14s
    # as it waits in order of launch, not order of finishing
    for p in $pids; do
            if wait $p; then
                    echo "Process $p success"
            else
                    echo "Process $p fail"
            fi
    done
    

    【讨论】:

    • 谢谢!这在我看来是最简单的方法。
    • 此解决方案不满足要求 #2:每个后台进程的监控循环。 waits 导致脚本等到(每个)进程结束。
    • 简单而好的方法..一直在寻找这个解决方案..
    • 这不起作用..或者没有做你想做的事:它不检查后台进程的退出状态?
    • @conny,是的,它确实检查了后台进程的退出状态。 “wait”命令返回进程的退出状态。在此处显示的示例中,它通过“Process $p 成功/失败”来演示。
    【解决方案4】:

    我的解决方案是使用匿名管道将状态传递给监控循环。没有用于交换状态的临时文件,因此无需清理。如果您不确定后台作业的数量,则中断条件可能是 [ -z "$(jobs -p)" ]。

    #!/bin/bash
    
    exec 3<> <(:)
    
    { sleep 15 ; echo "sleep/exit $?" >&3 ; } &
    
    while read -u 3 -t 1 -r STAT CODE || STAT="timeout" ; do
        echo "stat: ${STAT}; code: ${CODE}"
        if [ "${STAT}" = "sleep/exit" ] ; then
            break
        fi
    done
    

    【讨论】:

      【解决方案5】:

      后台子进程的 pid 存储在 $! 中。 您可以将所有子进程的 pid 存储到一个数组中,例如PIDS[]。

      wait [-n] [jobspec or pid …]
      

      等到每个进程 ID pid 或作业规范 jobspec 指定的子进程退出,并返回最后等待的命令的退出状态。如果给出了作业规范,则等待作业中的所有进程。如果没有给出参数,则等待所有当前活动的子进程,并且返回状态为零。如果提供了 -n 选项,wait 将等待任何作业终止并返回其退出状态。如果 jobspec 和 pid 都没有指定 shell 的活动子进程,则返回状态为 127。

      使用wait命令可以等待所有子进程完成,同时可以通过$?获取每个子进程的退出状态,并将状态存入STATUS []。然后你可以根据状态做一些事情。

      我尝试了以下 2 种解决方案,它们运行良好。 solution01 是 更简洁,而 solution02 有点复杂。

      解决方案01

      #!/bin/bash
      
      # start 3 child processes concurrently, and store each pid into array PIDS[].
      process=(a.sh b.sh c.sh)
      for app in ${process[@]}; do
        ./${app} &
        PIDS+=($!)
      done
      
      # wait for all processes to finish, and store each process's exit code into array STATUS[].
      for pid in ${PIDS[@]}; do
        echo "pid=${pid}"
        wait ${pid}
        STATUS+=($?)
      done
      
      # after all processed finish, check their exit codes in STATUS[].
      i=0
      for st in ${STATUS[@]}; do
        if [[ ${st} -ne 0 ]]; then
          echo "$i failed"
        else
          echo "$i finish"
        fi
        ((i+=1))
      done
      

      解决方案02

      #!/bin/bash
      
      # start 3 child processes concurrently, and store each pid into array PIDS[].
      i=0
      process=(a.sh b.sh c.sh)
      for app in ${process[@]}; do
        ./${app} &
        pid=$!
        PIDS[$i]=${pid}
        ((i+=1))
      done
      
      # wait for all processes to finish, and store each process's exit code into array STATUS[].
      i=0
      for pid in ${PIDS[@]}; do
        echo "pid=${pid}"
        wait ${pid}
        STATUS[$i]=$?
        ((i+=1))
      done
      
      # after all processed finish, check their exit codes in STATUS[].
      i=0
      for st in ${STATUS[@]}; do
        if [[ ${st} -ne 0 ]]; then
          echo "$i failed"
        else
          echo "$i finish"
        fi
        ((i+=1))
      done
      

      【讨论】:

      • 我已经尝试并证明它运行良好。你可以在代码中阅读我的解释。
      • 请阅读“How do I write a good answer?”,您可以在其中找到以下信息:...尝试在您的回答中提及任何限制、假设或简化。简洁是可以接受的,但更全面的解释会更好。 因此,您的回答是可以接受的,但如果您能详细说明问题和解决方案,您获得支持的机会就会大得多。 :-)
      • pid=$!; PIDS[$i]=${pid}; ((i+=1)) 可以更简单地写为PIDS+=($!),它只是附加到数组中,而不必使用单独的变量进行索引或 pid 本身。 STATUS 数组也是如此。
      • @codeforester,谢谢你的建议,我已经将我的初始代码修改为solution01,看起来更简洁了。
      • 同样的事情也适用于您将东西添加到数组中的其他地方。
      【解决方案6】:

      我们的团队也需要一个远程 SSH 执行的脚本,该脚本在 25 分钟不活动后超时。这是一个解决方案,监控循环每秒检查一次后台进程,但每 10 分钟打印一次以抑制不活动超时。

      long_running.sh & 
      pid=$!
      
      # Wait on a background job completion. Query status every 10 minutes.
      declare -i elapsed=0
      # `ps -p ${pid}` works on macOS and CentOS. On both OSes `ps ${pid}` works as well.
      while ps -p ${pid} >/dev/null; do
        sleep 1
        if ((++elapsed % 600 == 0)); then
          echo "Waiting for the completion of the main script. $((elapsed / 60))m and counting ..."
        fi
      done
      
      # Return the exit code of the terminated background process. This works in Bash 4.4 despite what Bash docs say:
      # "If neither jobspec nor pid specifies an active child process of the shell, the return status is 127."
      wait ${pid}
      

      【讨论】:

        【解决方案7】:

        使用这种方法,您的脚本不必等待后台进程,您只需监视一个临时文件的退出状态。

        FUNCmyCmd() { sleep 3;return 6; };
        
        export retFile=$(mktemp); 
        FUNCexecAndWait() { FUNCmyCmd;echo $? >$retFile; }; 
        FUNCexecAndWait&
        

        现在,您的脚本可以做任何其他事情,而您只需要继续监视 retFile 的内容(它还可以包含您想要的任何其他信息,例如退出时间)。

        PS.: 顺便说一句,我用 bash 编码思考

        【讨论】:

          【解决方案8】:
          #/bin/bash
          
          #pgm to monitor
          tail -f /var/log/messages >> /tmp/log&
          # background cmd pid
          pid=$!
          # loop to monitor running background cmd
          while :
          do
              ps ax | grep $pid | grep -v grep
              ret=$?
              if test "$ret" != "0"
              then
                  echo "Monitored pid ended"
                  break
              fi
              sleep 5
          
          done
          
          wait $pid
          echo $?
          

          【讨论】:

          • 这是一个避免grep -v的技巧。您可以将搜索限制在行首:grep '^'$pid 另外,您也可以使用ps p $pid -o pid=。另外,tail -f 在你杀死它之前不会结束,所以我认为这不是演示这个的好方法(至少没有指出这一点)。您可能希望将ps 命令的输出重定向到/dev/null,否则它会在每次迭代时显示在屏幕上。您的exit 会导致wait 被跳过——它可能应该是break。但是while/ps 和wait 不是多余的吗?
          • 为什么大家都忘了kill -0 $pid?它实际上并没有发送任何信号,只是使用内置的 shell 而不是外部进程来检查进程是否处于活动状态。
          • 因为你只能杀死一个你拥有的进程:bash: kill: (1) - Operation not permitted
          • 循环是多余的。等一下。更少的代码 => 更少的边缘案例。
          • @Brais Gabin 监控循环是问题的要求 #2
          【解决方案9】:

          正如我所见,几乎所有答案都使用外部实用程序(主要是 ps)来轮询后台进程的状态。有一个更 unixesh 的解决方案,捕获 SIGCHLD 信号。在信号处理程序中,必须检查哪个子进程已停止。它可以通过kill -0 &lt;PID&gt; 内置(通用)或检查/proc/&lt;PID&gt; 目录的存在(Linux 特定)或使用jobs 内置( 特定。jobs -l 还报告 pid。在这种情况下,输出的第三个字段可以是 Stopped|Running|Done|Exit 。)。

          这是我的例子。

          启动的进程称为loop.sh。它接受-x 或数字作为参数。对于-x,退出代码为 1。对于数字,它等待 num*5 秒。它每 5 秒打印一次 PID。

          启动器进程称为launch.sh:

          #!/bin/bash
          
          handle_chld() {
              local tmp=()
              for((i=0;i<${#pids[@]};++i)); do
                  if [ ! -d /proc/${pids[i]} ]; then
                      wait ${pids[i]}
                      echo "Stopped ${pids[i]}; exit code: $?"
                  else tmp+=(${pids[i]})
                  fi
              done
              pids=(${tmp[@]})
          }
          
          set -o monitor
          trap "handle_chld" CHLD
          
          # Start background processes
          ./loop.sh 3 &
          pids+=($!)
          ./loop.sh 2 &
          pids+=($!)
          ./loop.sh -x &
          pids+=($!)
          
          # Wait until all background processes are stopped
          while [ ${#pids[@]} -gt 0 ]; do echo "WAITING FOR: ${pids[@]}"; sleep 2; done
          echo STOPPED
          

          更多解释见:Starting a process from bash script failed

          【讨论】:

          • 既然我们在谈论 Bash,for 循环可以写成:for i in ${!pids[@]}; 使用参数扩展。
          【解决方案10】:

          另一个解决方案是通过 proc 文件系统监视进程(比 ps/grep 组合更安全);当您启动一个进程时,它在 /proc/$pid 中有一个相应的文件夹,因此解决方案可能是

          #!/bin/bash
          ....
          doSomething &
          local pid=$!
          while [ -d /proc/$pid ]; do # While directory exists, the process is running
              doSomethingElse
              ....
          else # when directory is removed from /proc, process has ended
              wait $pid
              local exit_status=$?
          done
          ....
          

          现在您可以随意使用 $exit_status 变量了。

          【讨论】:

          • 在 bash 中不起作用? Syntax error: "else" unexpected (expecting "done")
          【解决方案11】:

          这可能超出了您的问题范围,但是如果您担心进程运行的时间长度,您可能有兴趣在一段时间后检查正在运行的后台进程的状态。使用pgrep -P $$ 可以很容易地检查哪些子 PID 仍在运行,但是我想出了以下解决方案来检查那些已经过期的 PID 的退出状态:

          cmd1() { sleep 5; exit 24; }
          cmd2() { sleep 10; exit 0; }
          
          pids=()
          cmd1 & pids+=("$!")
          cmd2 & pids+=("$!")
          
          lasttimeout=0
          for timeout in 2 7 11; do
            echo -n "interval-$timeout: "
            sleep $((timeout-lasttimeout))
          
            # you can only wait on a pid once
            remainingpids=()
            for pid in ${pids[*]}; do
               if ! ps -p $pid >/dev/null ; then
                  wait $pid
                  echo -n "pid-$pid:exited($?); "
               else
                  echo -n "pid-$pid:running; "
                  remainingpids+=("$pid")
               fi
            done
            pids=( ${remainingpids[*]} )
          
            lasttimeout=$timeout
            echo
          done
          

          哪个输出:

          interval-2: pid-28083:running; pid-28084:running; 
          interval-7: pid-28083:exited(24); pid-28084:running; 
          interval-11: pid-28084:exited(0); 
          

          注意:如果您愿意,您可以将 $pids 更改为字符串变量而不是数组以简化操作。

          【讨论】:

            【解决方案12】:

            一个简单的例子,类似于上面的解决方案。这不需要监视任何过程输出。下一个示例使用 tail 跟踪输出。

            $ echo '#!/bin/bash' > tmp.sh
            $ echo 'sleep 30; exit 5' >> tmp.sh
            $ chmod +x tmp.sh
            $ ./tmp.sh &
            [1] 7454
            $ pid=$!
            $ wait $pid
            [1]+  Exit 5                  ./tmp.sh
            $ echo $?
            5
            

            使用tail跟踪进程输出并在进程完成时退出。

            $ echo '#!/bin/bash' > tmp.sh
            $ echo 'i=0; while let "$i < 10"; do sleep 5; echo "$i"; let i=$i+1; done; exit 5;' >> tmp.sh
            $ chmod +x tmp.sh
            $ ./tmp.sh
            0
            1
            2
            ^C
            $ ./tmp.sh > /tmp/tmp.log 2>&1 &
            [1] 7673
            $ pid=$!
            $ tail -f --pid $pid /tmp/tmp.log
            0
            1
            2
            3
            4
            5
            6
            7
            8
            9
            [1]+  Exit 5                  ./tmp.sh > /tmp/tmp.log 2>&1
            $ wait $pid
            $ echo $?
            5
            

            【讨论】:

              【解决方案13】:

              我会稍微改变你的方法。与其每隔几秒钟检查一次命令是否仍然存在并报告一条消息,不如让另一个进程每隔几秒钟报告一次命令仍在运行,然后在命令完成时终止该进程。例如:

              #!/bin/sh cmd() { 睡眠 5; 24号出口; } cmd & # 运行长时间运行的进程 pid=$! # 记录pid # 生成一个持续报告命令仍在运行的进程 while echo "$(date): $pid 仍在运行";睡一觉;完毕 & 回声=$! # 设置一个陷阱,在进程完成时杀死记者 陷阱'杀死$ echoer' 0 # 等待进程完成 如果等待 $pid;然后 echo "cmd 成功" 别的 echo "cmd FAILED!! (返回 $?)" 菲

              【讨论】:

              • 很好的模板,感谢分享!我相信除了trap,我们也可以while kill -0 $pid 2&gt; /dev/null; do X; done,希望对以后看到这个消息的人有用;)
              猜你喜欢
              • 1970-01-01
              • 2021-03-17
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多