web-dev-qa-db-ja.com

並列バックグラウンドプロセス(サブシェル)の終了コードを収集する

次のようなbashスクリプトがあるとします。

echo "x" &
echo "y" &
echo "z" &
.....
echo "Z" &
wait

サブシェル/サブプロセスの終了コードを収集する方法はありますか?これを行う方法を探していて、何も見つからない。 これらのサブシェルを並行して実行する必要があります。そうでない場合は、これが簡単になります。

genericソリューションを探しています(並行して実行するサブプロセスの数が不明/動的である)。

19
Alexander Mills

HandleJobsを使用するAlexander Millsの答えは素晴らしい出発点を与えてくれましたが、このエラーも与えてくれました

警告:run_pending_traps:trap_list [17]の不正な値:0x461010

bashの競合状態の問題である可能性があります

代わりに、私は各子のpidを保存し、各子の終了コードを具体的に待機して取得しました。サブプロセスが関数内でサブプロセスを生成し、子プロセスを待機するつもりで親プロセスを待機するリスクを回避するという点で、この方がわかりやすいと思います。トラップを使用しないため、何が起こるかがより明確になります。

#!/usr/bin/env bash

# it seems it does not work well if using echo for function return value, and calling inside $() (is a subprocess spawned?) 
function wait_and_get_exit_codes() {
    children=("$@")
    EXIT_CODE=0
    for job in "${children[@]}"; do
       echo "PID => ${job}"
       CODE=0;
       wait ${job} || CODE=$?
       if [[ "${CODE}" != "0" ]]; then
           echo "At least one test failed with exit code => ${CODE}" ;
           EXIT_CODE=1;
       fi
   done
}

DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
    )

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

children_pids=()
for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    children_pids+=("$!")
    echo "$i ith command has been issued as a background job"
done
# wait; # wait for all subshells to finish - its still valid to wait for all jobs to finish, before processing any exit-codes if we wanted to
#EXIT_CODE=0;  # exit code of overall script
wait_and_get_exit_codes "${children_pids[@]}"

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end
6
arberg

PIDとともに waitを使用します は次のようになります:

各プロセスIDで指定された子プロセスpidまたはジョブ仕様jobspec終了し、最後に待機したコマンドの終了ステータスを返します。

進行中に、各プロセスのPIDを保存する必要があります。

echo "x" & X=$!
echo "y" & Y=$!
echo "z" & Z=$!

set -mを使用してスクリプトでジョブ制御を有効にし、%nジョブ仕様を使用することもできますが、ほとんどの場合、したくない- ジョブ制御には他の多くの副作用があります =。

waitは、プロセスが終了したときと同じコードを返します。後で(合理的な)任意の時点でwait $Xを使用して、$?として最終コードにアクセスするか、単にtrue/falseとして使用できます。

echo "x" & X=$!
echo "y" & Y=$!
...
wait $X
echo "job X returned $?"

waitは、コマンドが完了するまで、コマンドが完了するまで一時停止します。

そのような停止を回避したい場合は、 trapSIGCHLDに設定 、終了の数を数え、すべてのwaitsを処理できます。彼らがすべて終わったらすぐに。おそらく、ほとんどいつでもwaitを単独で使用することで問題を回避できます。

21
Michael Homer

コマンドを特定する良い方法がある場合は、その終了コードをtmpファイルに出力してから、目的の特定のファイルにアクセスできます。

#!/bin/bash

for i in `seq 1 5`; do
    ( sleep $i ; echo $? > /tmp/cmd__${i} ) &
done

wait

for i in `seq 1 5`; do # or even /tmp/cmd__*
    echo "process $i:"
    cat /tmp/cmd__${i}
done

Tmpファイルを削除することを忘れないでください。

5
Rolf

compound commandを使用します-ステートメントを括弧で囲みます。

( echo "x" ; echo X: $? ) &
( true ; echo TRUE: $? ) &
( false ; echo FALSE: $? ) &

出力を与えます

x
X: 0
TRUE: 0
FALSE: 1

複数のコマンドを並行して実行する本当に異なる方法は、 GNU Parallel を使用することです。実行するコマンドのリストを作成し、ファイルlistに入れます。

cat > list
sleep 2 ; exit 7
sleep 3 ; exit 55
^D

すべてのコマンドを並行して実行し、ファイルjob.logに終了コードを収集します。

cat list | parallel -j0 --joblog job.log
cat job.log

そして出力は:

Seq     Host    Starttime       JobRuntime      Send    Receive Exitval Signal  Command
1       :       1486892487.325       1.976      0       0       7       0       sleep 2 ; exit 7
2       :       1486892487.326       3.003      0       0       55      0       sleep 3 ; exit 55
4
hschou

これは、探している一般的なスクリプトです。唯一の欠点は、コマンドが引用符で囲まれていることです。つまり、IDEによる構文の強調表示は実際には機能しません。それ以外の場合は、他のいくつかの答えを試しましたが、これが最良の答えです。この答えwait <pid> @Michaelによって与えられましたが、trapコマンドを使用することでさらに一歩進んでいます。

#!/usr/bin/env bash

set -m # allow for job control
EXIT_CODE=0;  # exit code of overall script

function handleJobs() {
     for job in `jobs -p`; do
         echo "PID => ${job}"
         CODE=0;
         wait ${job} || CODE=$?
         if [[ "${CODE}" != "0" ]]; then
         echo "At least one test failed with exit code => ${CODE}" ;
         EXIT_CODE=1;
         fi
     done
}

trap 'handleJobs' CHLD  # trap command is the key part
DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
)

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    echo "$i ith command has been issued as a background job"
done

wait; # wait for all subshells to finish

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end

正しい軌道に乗ってくれた@michael homerに感謝しますが、trapコマンドを使用することがAFAICTの最良のアプローチです。

2
Alexander Mills

@rolfの回答の別のバリエーション:

終了ステータスを保存する別の方法は、次のようなものです

mkdir /tmp/status_dir

そして、各スクリプトを持っています

script_name="${0##*/}"  ## strip path from script name
tmpfile="/tmp/status_dir/${script_name}.$$"
do something
rc=$?
echo "$rc" > "$tmpfile"

これにより、ステータスファイルごとに一意の名前が付けられ、それを作成したスクリプトの名前とそのプロセスID(同じスクリプトの複数のインスタンスが実行されている場合)が保存され、後で参照できるように、それらすべてが同じ場所なので、完了したらサブディレクトリ全体を削除できます。

次のようにすることで、各スクリプトから複数のステータスを保存することもできます

tmpfile="$(/bin/mktemp -q "/tmp/status_dir/${script_name}.$$.XXXXXX")"

以前と同じようにファイルを作成しますが、一意のランダム文字列を追加します。

または、同じファイルにさらにステータス情報を追加することもできます。

1
Joe

script3は、script1およびscript2は成功し、script1およびscript2は並行して実行されます:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0  ]];then
./script3
fi
1
venkata