web-dev-qa-db-ja.com

非同期タスクを実行し、その終了コードを取得してbashに出力する

一連のbashコマンドを非同期で実行する必要があり、1つが完了するとすぐに、その終了コードと出力に従ってアクションを実行する必要があります。実際の使用例では、これらのタスクがどれだけ長く実行されるかを予測できないことに注意してください。

この問題を解決するために、私は次のアルゴリズムで終わりました:

For each task to be run:
    Run the task asynchronously;
    Append the task to the list of running tasks.
End For.

While there still are tasks in the list of running tasks:
    For each task in the list of running tasks:
        If the task has ended:
            Retrieve the task's exit code and output;
            Remove the task from the list of running tasks.
        End If.
    End For
End While.

これにより、次のbashスクリプトが得られます。

  1 #!/bin/bash
  2 
  3 # bg.sh
  4 
  5 # Executing commands asynchronously, retrieving their exit codes and outputs upon completion.
  6 
  7 asynch_cmds=
  8 
  9 echo -e "Asynchronous commands:\nPID    FD"
 10 
 11 for i in {1..10}; do
 12         exec {fd}< <(sleep $(( i * 2 )) && echo $RANDOM && exit $i) # Dummy asynchronous task, standard output's stream is redirected to the current Shell
 13         asynch_cmds+="$!:$fd " # Append the task's PID and FD to the list of running tasks
 14         
 15         echo "$!        $fd"
 16 done    
 17 
 18 echo -e "\nExit codes and outputs:\nPID       FD      EXIT    OUTPUT"
 19 
 20 while [[ ${#asynch_cmds} -gt 0 ]]; do # While the list of running tasks isn't empty
 21         
 22         for asynch_cmd in $asynch_cmds; do # For each to in thhe list
 23                 
 24                 pid=${asynch_cmd%:*} # Task's PID
 25                 fd=${asynch_cmd#*:} # Task's FD
 26                 
 27                 if ! kill -0 $pid 2>/dev/null; then # If the task ended
 28                         
 29                         wait $pid # Retrieving the task's exit code
 30                         echo -n "$pid   $fd     $?      "
 31                         
 32                         echo "$(cat <&$fd)" # Retrieving the task's output
 33                         
 34                         asynch_cmds=${asynch_cmds/$asynch_cmd /} # Removing the task from the list
 35                 fi
 36         done
 37 done

出力は、waitが各タスクの終了コードを取得しようとして失敗したことを示していますただし、最後に実行されるものを除きます

Asynchronous commands:
PID     FD
4348    10
4349    11
4351    12
4353    13
4355    14
4357    15
4359    16
4361    17
4363    18
4365    19

Exit codes and outputs:
PID     FD  EXIT OUTPUT
./bg.sh: line 29: wait: pid 4348 is not a child of this Shell
4348    10  127  16010
./bg.sh: line 29: wait: pid 4349 is not a child of this Shell
4349    11  127  8341
./bg.sh: line 29: wait: pid 4351 is not a child of this Shell
4351    12  127  13814
./bg.sh: line 29: wait: pid 4353 is not a child of this Shell
4353    13  127  3775
./bg.sh: line 29: wait: pid 4355 is not a child of this Shell
4355    14  127  2309
./bg.sh: line 29: wait: pid 4357 is not a child of this Shell
4357    15  127  32203
./bg.sh: line 29: wait: pid 4359 is not a child of this Shell
4359    16  127  5907
./bg.sh: line 29: wait: pid 4361 is not a child of this Shell
4361    17  127  31849
./bg.sh: line 29: wait: pid 4363 is not a child of this Shell
4363    18  127  28920
4365    19  10   28810

コマンドの出力は問題なく取得されますが、これがどこにあるのかわかりませんis not a child of this Shellエラーが発生しています。 waitは最後のコマンドの終了コードを非同期で実行できるので、私は何か間違ったことをしているに違いありません。

このエラーの原因を誰かが知っていますか?この問題に対する私の解決策には欠陥がありますか、それともbashの動作を誤解していますか? waitの動作を理解するのに苦労しています。

PS:私はこの質問をスーパーユーザーに投稿しましたが、考え直してみると、Unix&Linux Stack Exchangeに適しているかもしれません。

4
Informancien

これはバグ/制限です。 bashは、$!の値を別の変数に保存するかどうかに関係なく、最後のプロセス置換を待つことのみを許可します。

よりシンプルなテストケース:

$ cat script
exec 7< <(sleep .2); pid7=$!
exec 8< <(sleep .2); pid8=$!
echo $pid7 $pid8
echo $(pgrep -P $$)
wait $pid7
wait $pid8

$ bash script
6030 6031
6030 6031
/tmp/sho: line 9: wait: pid 6030 is not a child of this Shell

pgrep -Pは実際にはこれをシェルの子として検出し、stracebashが実際にそれを取得していることを示しています。

しかし、とにかく、$!も最後のプロセス置換のPIDに設定されているため、ドキュメントに記載されていない機能であり(iircは古いバージョンでは機能しませんでした)、一部の gotchas が適用されます。 。


これは、bashがlast_procsub_child変数の最後のプロセス置換のみを追跡するために発生します。これがwaitがpidを探す場所です:

-- jobs.c --
/* Return the pipeline that PID belongs to.  Note that the pipeline
   doesn't have to belong to a job.  Must be called with SIGCHLD blocked.
   If JOBP is non-null, return the index of the job containing PID.  */
static PROCESS *
find_pipeline (pid, alive_only, jobp)
     pid_t pid;
     int alive_only;
     int *jobp;         /* index into jobs list or NO_JOB */
{
     ...
  /* Now look in the last process substitution pipeline, since that sets $! */
  if (last_procsub_child)
    {

しかし、それは新しいproc substが作成されるときに破棄されます:

-- subst.c --
static char *
process_substitute (string, open_for_read_in_child)
     char *string;
     int open_for_read_in_child;
{
   ...
      if (last_procsub_child)
        discard_last_procsub_child ();
3
mosvy

これが私が思いついたものです。

最初に、ダミーのrunスクリプトを使用します。これは、あなたの場合はまったく異なるものになります。

#!/bin/bash

sleep $1;
exit $2

次に、適切なリダイレクトを使用して、bgジョブをバックグラウンドに配置するrunスクリプト:

#!/bin/bash

echo $$

( ( touch $$.running; "$@" > $$.out 2>$$.err ; echo $? > $$.exitcode ) & )

最後に、全体を制御するdriverスクリプト。これは実際に実行するスクリプトであり、他の2つのスクリプトではありません。内のコメントは役立つはずですが、私はそれをテストしましたが、うまく機能しているようです。

#!/bin/bash

# first run all commands via "bg"
./bg ./run 10 0
./bg ./run 5 5
./bg ./run 2 2
./bg ./run 0 0
# ... and so on

while :
do
    shopt -s nullglob
    for i in *.exitcode
    do
        j=$(basename $i .exitcode)
        # now process $j.out, $j.err, $j.exitcode however you want; most
        # importantly, *move* at least the exitcode file out of this directory
        echo $j had exit code of `cat $i`
        rm $j.*
    done

    shopt -u nullglob
    ls *.running >/dev/null 2>&1 || exit
    sleep 1
done
1
sitaram