获取后台进程的退出代码


129

我有一个从我的主要bourne shell脚本调用的命令CMD,它永远要花时间。

我想按如下方式修改脚本:

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

有人可以给我指点一下吗?


1
...最终获胜者是?
TrueY

1
自从他问问题的那一天起,@ TrueY .. bob尚未登录。我们不太可能知道!
ghoti

Answers:


126

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

15
ps -p $my_pid -o pid=都不grep需要。
暂停,直到另行通知。

53
kill -0 $!是一种更好的判断进程是否仍在运行的方法。它实际上不发送任何信号,仅使用内置的Shell而不是外部进程检查该进程是否仍在运行。如前所述man 2 kill,“如果sig为0,则不发送信号,但仍执行错误检查;这可用于检查是否存在进程ID或进程组ID。”
迅速

14
kill -0如果您无权向正在运行的进程发送信号,则@ephemient 将返回非零值。不幸的是,1在这种情况下以及在该过程不存在的情况下,它都会返回。除非您不拥有该进程,否则这将非常有用-如果sudo涉及到类似工具或它们是setuid(并且可能会删除priv),即使对于您创建的进程也是如此。
Craig Ringer

13
wait不会在变量中返回退出代码$?。它只是返回退出代码,并且$?是最新前台程序的退出代码。
MindlessRanger 2015年

7
对于许多人投票赞成kill -0。这是SO的同行评审参考,显示CraigRinger的评论是合法的:kill -0正在运行的进程将返回非零...但ps -p对于任何正在运行的进程将始终返回0
Trevor Boyd Smith

57

这是我有类似需求时解决的方法:

# 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

我喜欢这种方法。
Kemin Zhou

谢谢!在我看来,这是最简单的方法。
卢克·戴维斯

4
此解决方案无法满足要求2:每个后台进程都有一个监视循环。wait导致脚本等待(每个)过程的最后。
DKroot

简单而不错的方法..一段时间以来一直在寻找这种解决方案..
Santosh Kumar Arjunan

这不起作用..或不执行您想要的操作:它不检查后台进程的退出状态吗?
conny

10

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

wait [-n] [jobspec or pid …]

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

使用wait命令可以等待所有子进程完成,同时可以通过$?获得每个子进程的退出状态并将状态存储到STATUS []中。然后,您可以根据状态执行某些操作。

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

solution01

#!/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

solution02

#!/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

我已经尝试并证明它运行良好。您可以通过代码阅读我的解释。
特里

请阅读“ 我如何写一个好的答案? ”,您将在其中找到以下信息:...尝试在答案中提及任何限制,假设或简化。简短是可以接受的,但是更充分的解释会更好。因此,您的回答是可以接受的,但是如果您可以详细说明问题和解决方案,则您获得投票的机会就会更大。:-)
Noel Widmer

1
pid=$!; PIDS[$i]=${pid}; ((i+=1))可以更简单地编写,因为PIDS+=($!)它可以简单地追加到数组,而不必使用单独的变量进行索引或pid本身。同样的事情也适用于STATUS数组。
codeforester

1
@codeforester,谢谢您的建议,我已将其初始代码修改为solution01,它看起来更加简洁。
特里

同样的事情也适用于将事物添加到数组中的其他地方。
codeforester

8

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

这是我的例子。

启动的过程称为loop.sh。它接受-x或数字作为参数。For -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

有关更多说明,请参见:从bash脚本启动进程失败


1
由于我们在谈论Bash,因此for循环可以写为:for i in ${!pids[@]};使用参数扩展。
PlasmaBinturong '18

7
#/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 $?

2
这是避免使用的技巧grep -v。您可以将搜索限制在行首:grep '^'$pid另外ps p $pid -o pid=,无论如何都可以进行搜索。另外,tail -f直到您将其杀死,它才会结束,因此,我认为这不是演示它的一种好方法(至少没有指出这一点)。您可能希望将ps命令的输出重定向到,/dev/null否则每次迭代都会转到屏幕。您的exit原因wait被跳过-可能应该是break。但是while/ ps和不是wait多余的吗?
暂停,直到另行通知。

5
为什么每个人都会忘记kill -0 $pid?它实际上不发送任何信号,仅使用内置的Shell而不是外部进程检查该进程是否仍在运行。
迅速

3
因为您只能杀死自己拥有的进程:bash: kill: (1) - Operation not permitted
errant.info 2013年

2
循环是多余的。等一下 更少的代码=>更少的边缘情况。
Brais Gabin

@Brais Gabin监视循环是问题的要求#2
DKroot

5

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

#!/ bin / sh

cmd(){睡眠5; 出口24; }

cmd&#运行长时间运行的进程
pid = $!#记录pid

#生成一个进程,最终报告该命令仍在运行
而echo“ $(date):$ pid仍在运行”; 睡一觉 完成&
echoer = $!

#设置陷阱以在过程结束时杀死报告程序
陷阱'kill $ echoer'0

#等待过程完成
如果等待$ pid; 然后
    回声“ cmd成功”
其他
    echo“ cmd FAILED !!(返回$?)”
科幻

很棒的模板,谢谢分享!我相信,除了陷阱,我们也可以这样做while kill -0 $pid 2> /dev/null; do X; done,希望它对以后阅读此消息的其他人有用;)
punkbit

3

我们的团队对使用远程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}

2

一个简单的例子,类似于上面的解决方案。这不需要监视任何过程输出。下一个示例使用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

1

另一个解决方案是通过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")
benjaoming

1

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

FUNCmyCmd() { sleep 3;return 6; };

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

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

PS .:顺便说一句,我用bash编写思维代码


0

这可能超出了您的问题,但是,如果您担心进程正在运行的时间长短,您可能有兴趣在一段时间后检查运行中的后台进程的状态。检查哪个子PID仍在运行很容易pgrep -P $$,但是我想出了以下解决方案来检查那些已经过期的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为字符串变量而不是数组以简化操作。


0

我的解决方案是使用匿名管道将状态传递到监视循环。没有用于交换状态的临时文件,因此无需清除。如果您不确定后台作业的数量,则中断条件可能为[ -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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.