código de retorno confiable del proceso en segundo plano

13

Asumamos la siguiente pieza de código 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

¿Es seguro asumir que de $?hecho contiene el código de retorno de fooy no el código de retorno de ping? Si la respuesta a esa pregunta es: "No puedes asumir eso". entonces, ¿cómo puedo modificar este código para asegurarme de que $?siempre contiene el código de retorno de foo?

Christopher Schmidt
fuente

Respuestas:

12

Con bash, tendrá esa garantía a menos que haya comenzado otro trabajo en segundo plano (y tenga en cuenta que los trabajos en segundo plano pueden iniciarse &pero también con coprocy con la sustitución de procesos) entre el foo &y el wait.

POSIX requiere que un shell recuerde el estado de salida de al menos 25 trabajos después de que se hayan ido , pero bashrecuerda mucho más que eso.

Ahora, si lo haces:

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

No tiene ninguna garantía de que barno se le dará el mismo pid que foo(si fooha terminado antes de que barcomience), por lo que, aunque es poco probable, wait "$pid"puede darle el estado de salida de bar.

Puedes reproducirlo con:

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

que (eventualmente) te dará en 0lugar de 12.

Para evitar el problema, una forma sería escribirlo 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
fuente
4

Sí, puede confiar wait "$!"para obtener el estado de un trabajo en segundo plano. Cuando se ejecuta como un script, bash no recopila automáticamente los trabajos en segundo plano completados. Por lo tanto, si ejecuta wait, reunirá el trabajo en el momento en que waitse llama.

Puede probar esto con un script simple:

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

Lo que dará salida:

FG: 0
BG: 22
Patricio
fuente
La parte clave de esa declaración fue el comienzo, "cuando se ejecuta como un script". Cuando es interactivo, waitno funciona. El proceso se recopila y el estado de salida se descarta justo antes de que se muestre la solicitud (de forma predeterminada).
Patrick
Acabo de probarlo en bash 4.2.37, 4.1.2 y 3.2.48. Todos se comportan exactamente igual (copiar / pegar literalmente el código en mi respuesta). El wait %1error con "no existe tal trabajo" ya que el proceso en segundo plano se recopila inmediatamente después de que se completa el "sueño 5".
Patrick
Ah ok, lo siento, lo entiendo ahora. Te había extrañado %1en lugar de $!.
Stéphane Chazelas
Tenga en cuenta que bash -c '(sleep 1;exit 5) & sleep 2; wait %1; echo $?'(también no es interactivo) no puede obtener el estado de salida de ese trabajo muerto. Suena como un error.
Stéphane Chazelas
esto no funcionó para mí dentro de una receta Makefile hasta que la incluí set +e. Parece que la set -efunción de bash mata el script tan pronto como se wait
genera
0

Creo que su suposición es correcta. Aquí hay un extracto de la man bashespera de procesos en segundo plano.

Si n especifica un proceso o trabajo inexistente, el estado de retorno es 127. De lo contrario, el estado de retorno es el estado de salida del último proceso o trabajo esperado.

Entonces tal vez deberías comprobar 127

Hay una pregunta similar con una respuesta completamente diferente de la que podría ayudar.

La secuencia de comandos Bash espera los procesos y obtiene el código de retorno

editar 1

Inspirado por los comentarios y respuestas de @ Stephane, he ampliado su guión. Puedo comenzar unos 34 procesos en segundo plano antes de que comience a perder la pista.

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
$ 

en mi sistema produce

$ 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
fuente
1
No, véase mi comentario a la respuesta de Umlaute , y probarlo por ti mismo conbash -c '(exit 12) & sleep 1; wait "$!"; echo "$?"'
Stéphane Chazelas
Nunca he visto bashuna pista suelta (incluso después de comenzar miles de trabajos), mi ejemplo demuestra que el pid está siendo reutilizado, lo que también puede ser lo que observó en su caso.
Stéphane Chazelas