código de retorno confiável do processo em segundo plano

12

Vamos assumir a seguinte parte do código do bash:

foo > logfile 2>&1 &
foo_pid=$!

while ps -p$foo_pid
do
    ping -c 1 localhost
done

wait $foo_pid

if [[ $? == 0 ]]
then
    echo "foo success"
fi

É seguro assumir que $?realmente contém o código de retorno fooe não o código de retorno ping? Se a resposta para essa pergunta for: "Você não pode assumir isso". então como posso modificar esse trecho de código para garantir que $?sempre contenha o código de retorno de foo?

Christopher Schmidt
fonte

Respostas:

12

Com bash, você terá essa garantia, a menos que tenha iniciado outro trabalho em segundo plano (e tenha cuidado para que os trabalhos em segundo plano possam ser iniciados, &mas também com coproce com a substituição do processo) entre o foo &e o wait.

O POSIX exige que um shell se lembre do status de saída de pelo menos 25 trabalhos depois que eles saem , mas se bashlembra muito mais do que isso.

Agora, se você fizer:

foo & pid=$!
...
bar &
wait "$pid"

Você não tem garantia de que barnão receberá o mesmo pid que foo(se footiver terminado quando o horário barcomeçar), portanto, mesmo que seja improvável, wait "$pid"poderá fornecer o status de saída de bar.

Você pode reproduzi-lo com:

bash -c '(exit 12; foo) & pid=$!
         while : bar & [ "$pid" != "$!" ]; do :;done
         wait "$pid"; echo "$?"'

que (eventualmente) lhe dará em 0vez de 12.

Para evitar o problema, uma maneira seria escrevê-lo como:

{
  foo_pid=$!

  while ps -p "$foo_pid"
  do
      ping -c 1 localhost
  done

  bar &
  ...

  read <&3 ret
  if [ "$ret" = 0 ]; then
    echo foo was sucessful.
  fi
} 3< <(foo > logfile 2>&1; echo "$?")
Stéphane Chazelas
fonte
4

Sim, você pode confiar wait "$!"para obter o status de um trabalho em segundo plano. Ao executar como um script, o bash não coleta automaticamente os trabalhos em segundo plano concluídos. Portanto, se você executar wait, ele reunirá o trabalho no momento em que waitfor chamado.

Você pode testar isso com um script simples:

#!/bin/bash
sh -c 'sleep 1; exit 22' &
sleep 5
echo "FG: $?"
wait %1
echo "BG: $?"

Qual saída:

FG: 0
BG: 22
Patrick
fonte
A parte principal dessa declaração foi o começo "quando executado como um script". Quando interativo, waitnão funciona. O processo é coletado e o status de saída descartado logo antes do prompt ser exibido (por padrão).
22414 Patrick
Eu apenas tentei no bash 4.2.37, 4.1.2 e 3.2.48. Todos eles se comportam exatamente da mesma maneira (copiar / colar literalmente o código na minha resposta). A wait %1falha falha com "nenhum trabalho", pois o processo em segundo plano é coletado imediatamente após a conclusão do "sono 5".
22414 Patrick Patrick
Ah ok, desculpe, eu entendi agora. Eu tinha sentido sua falta %1no lugar de $!.
Stéphane Chazelas
Observe que bash -c '(sleep 1;exit 5) & sleep 2; wait %1; echo $?'(também não interativo) falha ao obter o status de saída desse trabalho morto. Parece um bug.
Stéphane Chazelas
0

Eu acredito que sua suposição está correta. Aqui está um extrato man bashsobre a espera de processos em segundo plano.

Se n especificar um processo ou trabalho inexistente, o status de retorno será 127. Caso contrário, o status de retorno será o status de saída do último processo ou trabalho aguardado.

Então talvez você deva procurar 127

Há uma pergunta semelhante com uma resposta completamente diferente do que poderia ajudar.

Script Bash aguarde processos e obtenha código de retorno

editar 1

Inspirado nos comentários e respostas de @ Stephane, expandi seu script. Posso iniciar cerca de 34 processos em segundo plano antes de começar a perder o controle.

tback

$ cat tback 
plist=()
elist=()
slist=([1]=12 [2]=15 [3]=17 [4]=19 [5]=21 [6]=23)
count=30

#start background tasksto monitor
for i in 1 2 3 4
do
  #echo pid $i ${plist[$i]} ${slist[$i]}
  (echo $BASHPID-${slist[$i]} running; exit ${slist[$i]}) & 
  plist[$i]=$!
done

echo starting $count background echos to test history
for i in `eval echo {1..$count}`
do
  echo -n "." &
  elist[$i]=$! 
done
# wait for each background echo to complete
for i in `eval echo {1..$count}`
do
  wait ${elist[$i]}
  echo -n $? 
done
echo ""
# Now wait for each monitored process and check return status with expected
failed=0
for i in 1 2 3 4
do
  wait ${plist[$i]}
  rv=$?
  echo " pid ${plist[$i]} returns $rv should be ${slist[$i]}"
  if [[ $rv != ${slist[$i]} ]] 
  then
    failed=1
  fi
done

wait
echo "Complete $failed"
if [[ $failed = "1" ]]
then
  echo Failed
else
  echo Success
fi
exit $failed
$ 

no meu sistema produz

$ bash tback
14553-12 running
14554-15 running
14555-17 running
starting 30 background echos to test history
14556-19 running
..............................000000000000000000000000000000
 pid 14553 returns 12 should be 12
 pid 14554 returns 15 should be 15
 pid 14555 returns 17 should be 17
 pid 14556 returns 19 should be 19
Complete 0
Success
X Tian
fonte
1
Não, veja meu comentário na resposta do trema e tente você mesmo com #bash -c '(exit 12) & sleep 1; wait "$!"; echo "$?"'
Stéphane Chazelas
Eu nunca vi bashuma pista solta (mesmo depois de iniciar milhares de empregos), meu exemplo foi demonstrar a reutilização do pid, que também pode ser o que você observou no seu caso.
Stéphane Chazelas