バックグラウンドプロセスの終了コードを取得する


129

私のメインボーンシェルスクリプトから呼び出されたコマンドCMDが永久にかかります。

スクリプトを次のように変更します。

  1. コマンドCMDをバックグラウンドプロセスとして並行して実行します(CMD &)。
  2. メインスクリプトで、生成されたコマンドを数秒ごとに監視するループを作成します。ループはまた、スクリプトの進行状況を示すいくつかのメッセージをstdoutにエコーします。
  3. 生成されたコマンドが終了したら、ループを終了します。
  4. 生成されたプロセスの終了コードをキャプチャして報告します。

誰かがこれを達成するための指針を教えてもらえますか?


1
...そして勝者は?
TrueY

1
@TrueY .. bobは、質問した日からログインしていません。私たちが知ることはほとんどありません!
ghoti 2018

回答:


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も必要ありません。
追って通知があるまで一時停止。

54
kill -0 $!プロセスがまだ実行されているかどうかを確認するためのより良い方法です。実際にはシグナルを送信せず、外部プロセスの代わりに組み込みのシェルを使用して、プロセスが生きていることを確認するだけです。ASはman 2 kill言います、「場合sigが 0である、その後は信号が送信されませんが、エラーチェックがまだ実行され、これは、プロセスIDまたはプロセスグループIDの存在を確認するために使用することができます。」
ephemient

14
kill -0実行中のプロセスにシグナルを送信する権限がない場合、@ ephemient はゼロ以外を返します。残念ながら1、この場合とプロセスが存在しない場合の両方で戻ります。これは、あなたがプロセスを所有していない場合除き、便利ですsudo。たとえば、ツールが含まれている場合や、プロセスがsetuidである場合(そして場合によっては特権を削除する場合)でも、プロセスを作成できます。
クレイグリンガー2013年

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 2017

ありがとう!これは私には最も単純なアプローチのようです。
ルークデイビス

4
このソリューションは要件#2を満たしていません:バックグラウンドプロセスごとの監視ループ。wait■(各)プロセスの最後までスクリプトを待機させます。
DKroot '19

シンプルで素敵なアプローチが..かなりいつかこの解決策を探している
Santosh Kumar氏アルジュナン

これは動作しません..または期待どおりに動作しません:バックグラウンドプロセスの終了ステータスをチェックしませんか?
2018年

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

私はそれがうまく動くことを試み、証明した。私の説明はコードで読むことができます。
Terry

良い答えはどのように書きますか?読んでください次の情報が記載されています。...答えの中で制限、仮定、または簡略化について言及してください。簡潔さは許容されますが、完全な説明の方が優れています。したがって、回答は許容されますが、問題と解決策について詳しく説明できれば、賛成票を獲得できる可能性が高くなります。:-)
Noel Widmer 2017

1
pid=$!; PIDS[$i]=${pid}; ((i+=1))PIDS+=($!)インデックスやpid自体に個別の変数を使用する必要なく、配列に追加するだけの簡単な方法で記述できます。同じことがSTATUS配列にも当てはまります。
codeforester

1
@codeforester、ありがとうございました。最初のコードをsolution01に変更しました。より簡潔に見えます。
テリー

同じことが、配列に追加する他の場所にも当てはまります。
codeforester 2018年

8

私が見るように、ほとんどすべての回答psは、バックグラウンドプロセスの状態をポーリングするために外部ユーティリティ(主に)を使用します。SIGCHLDシグナルをキャッチする、より多くのunixeshソリューションがあります。シグナルハンドラーでは、どの子プロセスが停止したかを確認する必要があります。これは、kill -0 <PID>組み込み(ユニバーサル)、/proc/<PID>ディレクトリの存在の確認(Linux固有)、またはjobs組み込み(明確な。jobs -lpidも報告します。この場合、出力の3番目のフィールドは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

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/ pswait冗長ではありませんか?
追って通知があるまで一時停止。

5
なぜ誰もが忘れているのkill -0 $pidですか?実際にはシグナルを送信せず、外部プロセスの代わりに組み込みのシェルを使用して、プロセスが生きていることを確認するだけです。
ephemient

3
自分が所有しているプロセスしかbash: kill: (1) - Operation not permitted
削除

2
ループは冗長です。ちょっと待って。コードが少ない=>エッジケースが少ない。
Brais Gabin 2016年

@Brais Gabin監視ループは質問の要件#2です
DKroot '19

5

私はあなたのアプローチを少し変えます。コマンドがまだ生きているかどうかを数秒ごとにチェックしてメッセージを報告するのではなく、コマンドがまだ実行中であることを数秒ごとに報告し、コマンドが終了したらそのプロセスを強制終了する別のプロセスを用意します。例えば:

#!/ bin / sh

cmd(){スリープ5; 24番出口; }

cmd&#長時間実行プロセスを実行する
pid = $!#pidを記録

#コマンドがまだ実行中であることを継続的に報告するプロセスを生成します
while echo "$(date):$ pid is still running"; 寝る1; 完了&
echoer = $!

#プロセスが終了したときにレポーターを強制終了するトラップを設定します
トラップ 'kill $ echoer' 0

#プロセスが完了するのを待ちます
$ pidを待つ場合; その後
    エコー "cmd成功"
そうしないと
    echo "cmd FAILED !!(returned $?)"
fi

素晴らしいテンプレート、共有してくれてありがとう!トラップの代わりに、私たちもできると信じていwhile kill -0 $pid 2> /dev/null; do X; doneます。このメッセージを読む将来の誰かにとって役立つことを願っています;)
punkbit

3

私たちのチームは、25分間何も操作しないとタイムアウトになるリモートSSH実行スクリプトを使用して同じニーズを抱えていました。これは、監視ループが毎秒バックグラウンドプロセスをチェックするソリューションですが、非アクティブタイムアウトを抑制するために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")
2016

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.