비동기 작업을 실행하고 bash에서 종료 코드와 출력을 검색합니다.

비동기 작업을 실행하고 bash에서 종료 코드와 출력을 검색합니다.

여러 bash 명령을 비동기식으로 실행해야 하며, 명령이 완료되면 종료 코드와 출력을 기반으로 작업을 수행해야 합니다. 실제 사용 사례에서는 이러한 작업이 얼마나 오래 실행될지 예측할 수 없습니다.

이 문제를 해결하기 위해 저는 다음 알고리즘을 사용하게 되었습니다.

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.

추신: 이 질문을 Super User에 게시했지만 다시 생각해보면 Unix & Linux Stack Exchange에 더 적합할 수도 있습니다.

답변1

이것은 버그/제한 사항입니다. bash는 's 값을 $!다른 변수에 저장하는지 여부에 관계없이 마지막 프로세스가 교체될 때까지만 기다리는 것을 허용합니다.

더 간단한 테스트 사례:

$ 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실제로 그것이 껍질의 자식임을 발견하고 실제로 그것을 수확하고 있음 strace을 보여 주었 음에도 불구하고 .bash

그러나 어쨌든 $!PID를 마지막 프로세스 교체로 설정하는 것은 문서화되지 않은 기능이며(이전 버전에서는 사용되지 않았습니다) 몇 가지 제한 사항이 있습니다..


이는 bash가 last_procsub_child변수의 마지막 프로세스 대체만 추적하기 때문에 발생합니다. waitpid를 찾는 곳 은 다음과 같습니다 .

-- 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 ();

답변2

이것이 내가 생각해 낸 것입니다.

run먼저, 귀하의 경우에는 완전히 다른 더미 스크립트입니다.

#!/bin/bash

sleep $1;
exit $2

다음으로 스크립트는 작업을 백그라운드에 bg배치 하고 적절하게 리디렉션합니다.run

#!/bin/bash

echo $$

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

마지막으로 driver모든 것을 제어하는 ​​스크립트입니다. 이것은 물론 다른 두 스크립트가 아니라 실제로 실행할 스크립트입니다. 내부 의견이 도움이 될 것이지만 테스트해 본 결과 제대로 작동하는 것 같습니다.

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

관련 정보