Obtener el código de salida de un proceso en segundo plano


130

Tengo un comando CMD llamado desde mi script de shell bourne principal que tarda una eternidad.

Quiero modificar el script de la siguiente manera:

  1. Ejecute el comando CMD en paralelo como un proceso en segundo plano ( CMD &).
  2. En el script principal, tenga un bucle para monitorear el comando generado cada pocos segundos. El bucle también hace eco de algunos mensajes en stdout que indican el progreso del script.
  3. Salga del bucle cuando finalice el comando generado.
  4. Capture e informe el código de salida del proceso generado.

¿Alguien puede darme consejos para lograr esto?


1
...¿y el ganador es?
TrueY

1
@TrueY .. bob no ha iniciado sesión desde el día en que hizo la pregunta. ¡Es poco probable que lo sepamos!
ghoti

Respuestas:


126

1: en bash, $!contiene el PID del último proceso en segundo plano que se ejecutó. De todos modos, eso te dirá qué proceso monitorear.

4: wait <n>espera hasta que <n>se complete el proceso con PID (se bloqueará hasta que se complete el proceso, por lo que es posible que no desee llamar a esto hasta que esté seguro de que el proceso ha finalizado), y luego devuelve el código de salida del proceso completado.

2, 3: pso ps | grep " $! "puede decirle si el proceso aún se está ejecutando. Depende de usted cómo entender el resultado y decidir qué tan cerca está de terminar. ( ps | grepno es a prueba de idiotas. Si tiene tiempo, puede encontrar una forma más sólida de saber si el proceso aún se está ejecutando).

Aquí hay un script esqueleto:

# simulate a long process that will have an identifiable exit code
(sleep 15 ; /bin/false) &
my_pid=$!

while   ps | grep " $my_pid "     # might also need  | grep -v grep  here
do
    echo $my_pid is still in the ps output. Must still be running.
    sleep 3
done

echo Oh, it looks like the process is done.
wait $my_pid
# The variable $? always holds the exit code of the last command to finish.
# Here it holds the exit code of $my_pid, since wait exits with that code. 
my_status=$?
echo The exit status of the process was $my_status

15
ps -p $my_pid -o pid=ninguno grepes necesario.
Pausado hasta nuevo aviso.

54
kill -0 $!es una mejor manera de saber si un proceso aún se está ejecutando. En realidad, no envía ninguna señal, solo verifica que el proceso esté activo, utilizando un shell incorporado en lugar de procesos externos. Como man 2 killdice, "Si sig es 0, entonces no se envía ninguna señal, pero aún se realiza la verificación de errores; esto se puede usar para verificar la existencia de una ID de proceso o ID de grupo de proceso".
ephemient

14
@ephemient kill -0devolverá un valor distinto de cero si no tiene permiso para enviar señales a un proceso que se está ejecutando. Desafortunadamente, regresa 1tanto en este caso como en el caso donde el proceso no existe. Esto lo hace útil a menos que no sea el propietario del proceso, que puede ser el caso incluso para los procesos que creó si una herramienta como esta sudoestá involucrada o si son setuid (y posiblemente eliminen privs).
Craig Ringer

13
waitno devuelve el código de salida en la variable $?. Simplemente devuelve el código de salida y $?es el código de salida del último programa en primer plano.
MindlessRanger

77
Para las muchas personas que votan por el kill -0. Aquí hay una referencia revisada por pares de SO que muestra que el comentario de CraigRinger es legítimo con respecto a: kill -0devolverá un valor distinto de cero para los procesos en ejecución ... pero ps -psiempre devolverá 0 para cualquier proceso en ejecución .
Trevor Boyd Smith

57

Así es como lo resolví cuando tenía una necesidad similar:

# Some function that takes a long time to process
longprocess() {
        # Sleep up to 14 seconds
        sleep $((RANDOM % 15))
        # Randomly exit with 0 or 1
        exit $((RANDOM % 2))
}

pids=""
# Run five concurrent processes
for i in {1..5}; do
        ( longprocess ) &
        # store PID of process
        pids+=" $!"
done

# Wait for all processes to finish, will take max 14s
# as it waits in order of launch, not order of finishing
for p in $pids; do
        if wait $p; then
                echo "Process $p success"
        else
                echo "Process $p fail"
        fi
done

Me gusta este enfoque.
Kemin Zhou

¡Gracias! Este me parece el enfoque más simple.
Luke Davis el

44
Esta solución no satisface el requisito # 2: un ciclo de monitoreo por proceso en segundo plano. waits hace que el script espere hasta el final de (cada) proceso.
DKroot

Enfoque simple y agradable ... he estado buscando esta solución en algún momento ...
Santosh Kumar Arjunan

Esto no funciona ... o no hace lo que quiere: ¿no verifica los procesos en segundo plano?
conny

10

El pid de un proceso secundario en segundo plano se almacena en $! . Puede almacenar todos los pids de los procesos secundarios en una matriz, por ejemplo, PIDS [] .

wait [-n] [jobspec or pid …]

Espere hasta que el proceso secundario especificado por cada ID de proceso pid o especificación de trabajo se cierre y regrese el estado de salida del último comando esperado. Si se proporciona una especificación de trabajo, se esperan todos los procesos en el trabajo. Si no se proporcionan argumentos, se esperan todos los procesos secundarios actualmente activos y el estado de retorno es cero. Si se proporciona la opción -n, esperar espera a que finalice cualquier trabajo y devuelve su estado de salida. Si ni jobspec ni pid especifican un proceso secundario activo del shell, el estado de retorno es 127.

Utilice el comando wait , puede esperar a que finalicen todos los procesos secundarios, mientras que puede obtener el estado de salida de cada proceso secundario a través de $? y almacenar el estado en ESTADO [] . Entonces puedes hacer algo dependiendo del estado.

He intentado las siguientes 2 soluciones y funcionan bien. solution01 es más conciso, mientras que solution02 es un poco complicado.

solución01

#!/bin/bash

# start 3 child processes concurrently, and store each pid into array PIDS[].
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
  ./${app} &
  PIDS+=($!)
done

# wait for all processes to finish, and store each process's exit code into array STATUS[].
for pid in ${PIDS[@]}; do
  echo "pid=${pid}"
  wait ${pid}
  STATUS+=($?)
done

# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
  if [[ ${st} -ne 0 ]]; then
    echo "$i failed"
  else
    echo "$i finish"
  fi
  ((i+=1))
done

solución02

#!/bin/bash

# start 3 child processes concurrently, and store each pid into array PIDS[].
i=0
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
  ./${app} &
  pid=$!
  PIDS[$i]=${pid}
  ((i+=1))
done

# wait for all processes to finish, and store each process's exit code into array STATUS[].
i=0
for pid in ${PIDS[@]}; do
  echo "pid=${pid}"
  wait ${pid}
  STATUS[$i]=$?
  ((i+=1))
done

# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
  if [[ ${st} -ne 0 ]]; then
    echo "$i failed"
  else
    echo "$i finish"
  fi
  ((i+=1))
done

Lo intenté y probé que funciona bien. Puedes leer mi explicación en código.
Terry

Lea " ¿Cómo escribo una buena respuesta? ", Donde encontrará la siguiente información: ... trate de mencionar cualquier limitación, suposición o simplificación en su respuesta. La brevedad es aceptable, pero las explicaciones más completas son mejores. Por lo tanto, su respuesta es aceptable, pero tiene muchas más posibilidades de obtener votos positivos si puede explicar el problema y su solución. :-)
Noel Widmer

1
pid=$!; PIDS[$i]=${pid}; ((i+=1))se puede escribir de manera más simple, ya PIDS+=($!)que simplemente se agrega a la matriz sin tener que usar una variable separada para la indexación o el propio pid. Lo mismo se aplica a la STATUSmatriz.
codeforester

1
@codeforester, gracias por su sugerencia, he modificado mi código inicial en la solución01, parece más conciso.
Terry

Lo mismo se aplica a otros lugares donde está agregando cosas a una matriz.
codeforester

8

Como veo, casi todas las respuestas usan utilidades externas (principalmente ps) para sondear el estado del proceso en segundo plano. Hay una solución más unixesh, capturando la señal SIGCHLD. En el controlador de señal se debe verificar qué proceso secundario se detuvo. Se puede hacer kill -0 <PID>incorporando (universal) o comprobando la existencia de un /proc/<PID>directorio (específico de Linux) o utilizando el jobsincorporado (específico. jobs -lTambién informa el pid. En este caso, el tercer campo de la salida se puede Detener | En ejecución | Listo | Salir. )

Aquí está mi ejemplo.

Se llama al proceso lanzado loop.sh. Acepta -xo un número como argumento. For -xes salidas con código de salida 1. Para un número espera num * 5 segundos. En cada 5 segundos imprime su PID.

El proceso del iniciador se llama launch.sh:

#!/bin/bash

handle_chld() {
    local tmp=()
    for((i=0;i<${#pids[@]};++i)); do
        if [ ! -d /proc/${pids[i]} ]; then
            wait ${pids[i]}
            echo "Stopped ${pids[i]}; exit code: $?"
        else tmp+=(${pids[i]})
        fi
    done
    pids=(${tmp[@]})
}

set -o monitor
trap "handle_chld" CHLD

# Start background processes
./loop.sh 3 &
pids+=($!)
./loop.sh 2 &
pids+=($!)
./loop.sh -x &
pids+=($!)

# Wait until all background processes are stopped
while [ ${#pids[@]} -gt 0 ]; do echo "WAITING FOR: ${pids[@]}"; sleep 2; done
echo STOPPED

Para obtener más explicaciones, consulte: Error al iniciar un proceso desde el script bash


1
Como estamos hablando de Bash, el bucle for podría haberse escrito como: for i in ${!pids[@]};usando la expansión de parámetros.
PlasmaBinturong

7
#/bin/bash

#pgm to monitor
tail -f /var/log/messages >> /tmp/log&
# background cmd pid
pid=$!
# loop to monitor running background cmd
while :
do
    ps ax | grep $pid | grep -v grep
    ret=$?
    if test "$ret" != "0"
    then
        echo "Monitored pid ended"
        break
    fi
    sleep 5

done

wait $pid
echo $?

2
Aquí hay un truco para evitar el grep -v . Puede limitar la búsqueda al comienzo de la línea: grep '^'$pidademás, puede hacerlo ps p $pid -o pid=, de todos modos. Además, tail -fno va a terminar hasta que lo mates, así que no creo que sea una muy buena manera de demostrar esto (al menos sin señalarlo). Es posible que desee redirigir la salida de su pscomando /dev/nullo irá a la pantalla en cada iteración. Sus exitcausas waitse omiten, probablemente debería ser a break. ¿Pero no son los while/ psy los waitredundantes?
Pausado hasta nuevo aviso.

55
¿Por qué todos se olvidan kill -0 $pid? En realidad, no envía ninguna señal, solo verifica que el proceso esté activo, utilizando un shell incorporado en lugar de procesos externos.
ephemient

3
Debido a que solo puede matar un proceso de su propiedad:bash: kill: (1) - Operation not permitted
errant.info

2
El bucle es redundante. Solo espera Menos código => menos casos extremos.
Brais Gabin el

@Brais Gabin El ciclo de monitoreo es el requisito n. ° 2 de la pregunta
DKroot

5

Cambiaría tu enfoque ligeramente. En lugar de verificar cada pocos segundos si el comando aún está vivo e informar un mensaje, haga otro proceso que informe cada pocos segundos que el comando aún se está ejecutando y luego elimine ese proceso cuando finalice el comando. Por ejemplo:

#! / bin / sh

cmd () {dormir 5; salida 24; }

cmd & # Ejecuta el proceso de ejecución larga
pid = $! # Grabar el pid

# Genera un proceso que informa de forma continua que el comando aún se está ejecutando
while echo "$ (fecha): $ pid todavía se está ejecutando"; duerme 1; hecho &
echoer = $!

# Establecer una trampa para matar al reportero cuando finalice el proceso
trampa 'kill $ echoer' 0

# Espera a que termine el proceso
si espera $ pid; luego
    echo "cmd tuvo éxito"
más
    echo "cmd FAILED !! (devuelto $?)"
fi

gran plantilla, gracias por compartir! Creo que en lugar de trampa, también podemos hacerlo while kill -0 $pid 2> /dev/null; do X; done, espero que sea útil para alguien más en el futuro que lea este mensaje;)
punkbit

3

Nuestro equipo tenía la misma necesidad con un script remoto ejecutado por SSH que estaba agotando el tiempo de espera después de 25 minutos de inactividad. Aquí hay una solución con el ciclo de monitoreo que verifica el proceso en segundo plano cada segundo, pero imprime solo cada 10 minutos para suprimir un tiempo de inactividad.

long_running.sh & 
pid=$!

# Wait on a background job completion. Query status every 10 minutes.
declare -i elapsed=0
# `ps -p ${pid}` works on macOS and CentOS. On both OSes `ps ${pid}` works as well.
while ps -p ${pid} >/dev/null; do
  sleep 1
  if ((++elapsed % 600 == 0)); then
    echo "Waiting for the completion of the main script. $((elapsed / 60))m and counting ..."
  fi
done

# Return the exit code of the terminated background process. This works in Bash 4.4 despite what Bash docs say:
# "If neither jobspec nor pid specifies an active child process of the shell, the return status is 127."
wait ${pid}

2

Un ejemplo simple, similar a las soluciones anteriores. Esto no requiere monitorear ningún resultado del proceso. El siguiente ejemplo usa tail para seguir la salida.

$ echo '#!/bin/bash' > tmp.sh
$ echo 'sleep 30; exit 5' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh &
[1] 7454
$ pid=$!
$ wait $pid
[1]+  Exit 5                  ./tmp.sh
$ echo $?
5

Use tail para seguir la salida del proceso y salga cuando el proceso se haya completado.

$ echo '#!/bin/bash' > tmp.sh
$ echo 'i=0; while let "$i < 10"; do sleep 5; echo "$i"; let i=$i+1; done; exit 5;' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh
0
1
2
^C
$ ./tmp.sh > /tmp/tmp.log 2>&1 &
[1] 7673
$ pid=$!
$ tail -f --pid $pid /tmp/tmp.log
0
1
2
3
4
5
6
7
8
9
[1]+  Exit 5                  ./tmp.sh > /tmp/tmp.log 2>&1
$ wait $pid
$ echo $?
5

1

Otra solución es monitorear procesos a través del sistema de archivos proc (más seguro que el combo ps / grep); cuando inicia un proceso, tiene una carpeta correspondiente en / proc / $ pid, por lo que la solución podría ser

#!/bin/bash
....
doSomething &
local pid=$!
while [ -d /proc/$pid ]; do # While directory exists, the process is running
    doSomethingElse
    ....
else # when directory is removed from /proc, process has ended
    wait $pid
    local exit_status=$?
done
....

Ahora puede usar la variable $ exit_status como quiera.


¿No funciona en bash? Syntax error: "else" unexpected (expecting "done")
benjaoming

1

Con este método, su script no tiene que esperar el proceso en segundo plano, solo tendrá que monitorear un archivo temporal para ver el estado de salida.

FUNCmyCmd() { sleep 3;return 6; };

export retFile=$(mktemp); 
FUNCexecAndWait() { FUNCmyCmd;echo $? >$retFile; }; 
FUNCexecAndWait&

ahora, su script puede hacer cualquier otra cosa mientras solo tiene que seguir monitoreando el contenido de retFile (también puede contener cualquier otra información que desee, como la hora de salida).

PD .: por cierto, codifiqué pensando en bash


0

Esto puede extenderse más allá de su pregunta, sin embargo, si le preocupa la cantidad de tiempo que se ejecutan los procesos, puede estar interesado en verificar el estado de los procesos en segundo plano en ejecución después de un intervalo de tiempo. Es bastante fácil verificar qué PID secundarios todavía se están utilizando pgrep -P $$, sin embargo, se me ocurrió la siguiente solución para verificar el estado de salida de esos PID que ya han expirado:

cmd1() { sleep 5; exit 24; }
cmd2() { sleep 10; exit 0; }

pids=()
cmd1 & pids+=("$!")
cmd2 & pids+=("$!")

lasttimeout=0
for timeout in 2 7 11; do
  echo -n "interval-$timeout: "
  sleep $((timeout-lasttimeout))

  # you can only wait on a pid once
  remainingpids=()
  for pid in ${pids[*]}; do
     if ! ps -p $pid >/dev/null ; then
        wait $pid
        echo -n "pid-$pid:exited($?); "
     else
        echo -n "pid-$pid:running; "
        remainingpids+=("$pid")
     fi
  done
  pids=( ${remainingpids[*]} )

  lasttimeout=$timeout
  echo
done

que salidas:

interval-2: pid-28083:running; pid-28084:running; 
interval-7: pid-28083:exited(24); pid-28084:running; 
interval-11: pid-28084:exited(0); 

Nota: Puede cambiar $pidsa una variable de cadena en lugar de a una matriz para simplificar las cosas si lo desea.


0

Mi solución fue usar una tubería anónima para pasar el estado a un circuito de monitoreo. No se utilizan archivos temporales para intercambiar estado, por lo que no hay nada que limpiar. Si no estaba seguro del número de trabajos en segundo plano, la condición de interrupción podría ser [ -z "$(jobs -p)" ].

#!/bin/bash

exec 3<> <(:)

{ sleep 15 ; echo "sleep/exit $?" >&3 ; } &

while read -u 3 -t 1 -r STAT CODE || STAT="timeout" ; do
    echo "stat: ${STAT}; code: ${CODE}"
    if [ "${STAT}" = "sleep/exit" ] ; then
        break
    fi
done
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.