Ottieni il codice di uscita di un processo in background


130

Ho un comando CMD chiamato dal mio script di shell bourne principale che impiega un'eternità.

Voglio modificare lo script come segue:

  1. Eseguire il comando CMD in parallelo come processo in background (CMD & ).
  2. Nello script principale, avere un ciclo per monitorare il comando generato ogni pochi secondi. Il ciclo riporta anche alcuni messaggi a stdout che indicano l'avanzamento dello script.
  3. Esci dal ciclo quando termina il comando generato.
  4. Cattura e segnala il codice di uscita del processo generato.

Qualcuno può darmi indicazioni per farlo?


1
...e il vincitore è?
TrueY

1
@ TrueY .. bob non ha effettuato l'accesso dal giorno in cui ha posto la domanda. È improbabile che lo sapremo mai!
ghoti

Risposte:


128

1: In bash, $!contiene il PID dell'ultimo processo in background che è stato eseguito. Questo ti dirà quale processo monitorare, comunque.

4: wait <n>attende fino al processo con PID<n> sia completo (si bloccherà fino al completamento del processo, quindi potresti non volerlo chiamare finché non sei sicuro che il processo sia completato), quindi restituisce il codice di uscita del processo completato.

2, 3: pso ps | grep " $! "può dirti se il processo è ancora in esecuzione. Dipende da te come capire l'output e decidere quanto è vicino alla fine. ( ps | grepnon è a prova di idiota. Se hai tempo puoi trovare un modo più affidabile per capire se il processo è ancora in esecuzione).

Ecco uno script scheletro:

# 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=nessuno dei due grepè necessario.
In pausa fino a nuovo avviso.

54
kill -0 $!è un modo migliore per capire se un processo è ancora in esecuzione. In realtà non invia alcun segnale, controlla solo che il processo sia attivo, utilizzando una shell incorporata invece di processi esterni. Come man 2 killdice, "Se sig è 0, non viene inviato alcun segnale, ma viene comunque eseguito il controllo degli errori; questo può essere utilizzato per verificare l'esistenza di un ID di processo o di un ID di gruppo di processo."
effimero

14
@ephemient kill -0restituirà un valore diverso da zero se non si dispone dell'autorizzazione per inviare segnali a un processo in esecuzione. Sfortunatamente ritorna 1sia in questo caso che nel caso in cui il processo non esiste. Questo lo rende utile a meno che tu non sia il proprietario del processo, il che può essere il caso anche per i processi che hai creato se sudoè coinvolto uno strumento simile o se sono setuid (e possibilmente rilasciano priv).
Craig Ringer

13
waitnon restituisce il codice di uscita nella variabile $?. Restituisce solo il codice di uscita ed $?è il codice di uscita dell'ultimo programma in primo piano.
MindlessRanger

7
Per le molte persone che votano a favore del kill -0. Ecco un riferimento peer review da SO che mostra che il commento di CraigRinger è legittimo per quanto riguarda: kill -0restituirà un valore diverso da zero per i processi in esecuzione ... ma ps -prestituirà sempre 0 per qualsiasi processo in esecuzione .
Trevor Boyd Smith

58

Ecco come l'ho risolto quando avevo un bisogno simile:

# 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

Mi piace questo approccio.
Kemin Zhou

Grazie! Questo mi sembra l'approccio più semplice.
Luke Davis

4
Questa soluzione non soddisfa il requisito n. 2: un ciclo di monitoraggio per processo in background. waits fa in modo che lo script attenda fino alla fine di (ogni) processo.
DKroot

Approccio semplice e simpatico .. ho cercato questa soluzione un po 'di tempo ..
Santosh Kumar Arjunan

Questo non funziona .. o non fa quello che vuoi: non controlla gli stati di uscita dei processi in background?
conny

10

Il pid di un processo figlio in background è memorizzato in $! . È possibile memorizzare i pid di tutti i processi figli in un array, ad esempio PIDS [] .

wait [-n] [jobspec or pid …]

Attendere finché il processo figlio specificato da ciascun ID processo pid o specifica lavoro jobspec non esce e restituisce lo stato di uscita dell'ultimo comando atteso. Se viene fornita una specifica di lavoro, vengono attesi tutti i processi nel lavoro. Se non vengono forniti argomenti, vengono attesi tutti i processi figli attualmente attivi e lo stato di ritorno è zero. Se viene fornita l'opzione -n, wait attende che qualsiasi lavoro termini e restituisce il suo stato di uscita. Se né jobspec né pid specificano un processo figlio attivo della shell, lo stato di ritorno è 127.

Usa il comando wait puoi aspettare che tutti i processi figli finiscano, nel frattempo puoi ottenere lo stato di uscita di ogni processo figlio tramite $? e memorizzare lo stato in STATUS [] . Quindi puoi fare qualcosa a seconda dello stato.

Ho provato le seguenti 2 soluzioni e funzionano bene. solution01 è più conciso, mentre solution02 è un po 'complicato.

solution01

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

solution02

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

Ho provato e dimostrato che funziona bene. Puoi leggere la mia spiegazione in codice.
Terry

Per favore leggi " Come scrivo una buona risposta? " Dove troverai le seguenti informazioni: ... prova a menzionare eventuali limitazioni, ipotesi o semplificazioni nella tua risposta. La brevità è accettabile, ma le spiegazioni più complete sono migliori. La tua risposta è quindi accettabile, ma hai molte più possibilità di ottenere voti positivi se puoi elaborare il problema e la tua soluzione. :-)
Noel Widmer

1
pid=$!; PIDS[$i]=${pid}; ((i+=1))può essere scritto più semplicemente in quanto PIDS+=($!)si aggiunge semplicemente all'array senza dover utilizzare una variabile separata per l'indicizzazione o il pid stesso. La stessa cosa vale per l' STATUSarray.
codeforester

1
@codeforester, grazie per il tuo suggerimento, ho modificato il mio codice iniziale in solution01, sembra più conciso.
Terry

La stessa cosa si applica ad altri luoghi in cui si aggiungono elementi in un array.
codeforester

8

Come vedo quasi tutte le risposte utilizzano utilità esterne (principalmente ps) per interrogare lo stato del processo in background. C'è una soluzione più unixesh, che cattura il segnale SIGCHLD. Nel gestore del segnale deve essere verificato quale processo figlio è stato interrotto. Può essere fatto da kill -0 <PID>built-in (universale) o controllando l'esistenza di /proc/<PID>directory (specifico per Linux) o usando iljobs built-in (specifico. jobs -lriporta anche il pid. In questo caso il 3 ° campo dell'output può essere Stopped | Running | Done | Exit. ).

Ecco il mio esempio.

Viene chiamato il processo avviato loop.sh. Accetta -xo un numero come argomento. Per -xè esce con il codice di uscita 1. Per un numero attende num * 5 secondi. Ogni 5 secondi stampa il suo PID.

Il processo di avvio si chiama 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

Per ulteriori spiegazioni vedere: Avvio di un processo dallo script bash non riuscito


1
Dato che stiamo parlando di Bash, il ciclo for avrebbe potuto essere scritto come: for i in ${!pids[@]};using parameter espansione.
PlasmaBinturong

8
#/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
Ecco un trucco per evitare il file grep -v. Puoi limitare la ricerca all'inizio della riga: grep '^'$pidInoltre, puoi farlo ps p $pid -o pid=comunque. Inoltre, tail -fnon finirà fino a quando non lo ucciderai, quindi non penso sia un ottimo modo per dimostrarlo (almeno senza farlo notare). Potresti voler reindirizzare l'output del tuo pscomando /dev/nullo andrà sullo schermo ad ogni iterazione. Le tue exitcause vengono waitignorate - probabilmente dovrebbe essere un file break. Ma non sono il while/ pse il waitridondante?
In pausa fino a nuovo avviso.

5
Perché tutti se ne dimenticano kill -0 $pid? In realtà non invia alcun segnale, controlla solo che il processo sia attivo, utilizzando una shell incorporata invece di processi esterni.
effimero

3
Perché puoi uccidere solo un processo che possiedi:bash: kill: (1) - Operation not permitted
errant.info

2
Il loop è ridondante. Aspetta. Meno codice => meno casi limite.
Brais Gabin

@ Brais Gabin Il circuito di monitoraggio è il requisito n. 2 della domanda
DKroot

5

Modificherei leggermente il tuo approccio. Piuttosto che controllare ogni pochi secondi se il comando è ancora attivo e segnalare un messaggio, avere un altro processo che segnala ogni pochi secondi che il comando è ancora in esecuzione e quindi terminare quel processo al termine del comando. Per esempio:

#! / Bin / sh

cmd () {sleep 5; uscita 24; }

cmd & # Esegue il lungo processo in esecuzione
pid = $! # Registra il pid

# Genera un processo che segnala continuamente che il comando è ancora in esecuzione
while echo "$ (date): $ pid è ancora in esecuzione"; dormi 1; fatto &
echoer = $!

# Imposta una trappola per uccidere il giornalista quando il processo finisce
trap 'kill $ echoer' 0

# Attendi che il processo finisca
se aspetta $ pid; poi
    echo "cmd riuscito"
altro
    echo "cmd FAILED !! (restituito $?)"
fi

ottimo modello, grazie per la condivisione! Credo che invece di trap, possiamo anche fare while kill -0 $pid 2> /dev/null; do X; done, spero che sia utile per qualcun altro in futuro che leggerà questo messaggio;)
punkbit

3

Il nostro team aveva la stessa esigenza con uno script remoto eseguito da SSH che scadeva dopo 25 minuti di inattività. Ecco una soluzione con il ciclo di monitoraggio che controlla il processo in background ogni secondo, ma stampa solo ogni 10 minuti per eliminare un timeout di inattività.

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 semplice esempio, simile alle soluzioni sopra. Ciò non richiede il monitoraggio dell'output del processo. Il prossimo esempio usa tail per seguire l'output.

$ 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

Usa tail per seguire l'output del processo ed esci quando il processo è completo.

$ 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

Un'altra soluzione è monitorare i processi tramite il filesystem proc (più sicuro della combinazione ps / grep); quando avvii un processo ha una cartella corrispondente in / proc / $ pid, quindi la soluzione potrebbe essere

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

Ora puoi usare la variabile $ exit_status come preferisci.


Non funziona in bash? Syntax error: "else" unexpected (expecting "done")
Benjaoming

1

Con questo metodo, il tuo script non deve attendere il processo in background, dovrai solo monitorare un file temporaneo per lo stato di uscita.

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

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

ora, il tuo script può fare qualsiasi altra cosa mentre devi solo continuare a monitorare il contenuto di retFile (può anche contenere qualsiasi altra informazione che desideri come l'ora di uscita).

PS .: btw, ho codificato il pensiero in bash


0

Questo potrebbe estendersi oltre la tua domanda, tuttavia, se sei preoccupato per il tempo di esecuzione dei processi, potresti essere interessato a controllare lo stato dei processi in esecuzione in background dopo un intervallo di tempo. È abbastanza facile controllare quali PID figlio sono ancora in esecuzione utilizzando pgrep -P $$, tuttavia ho escogitato la seguente soluzione per controllare lo stato di uscita di quei PID che sono già scaduti:

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

quali uscite:

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

Nota: puoi passare $pidsa una variabile stringa anziché a un array per semplificare le cose, se lo desideri.


0

La mia soluzione era quella di utilizzare una pipe anonima per passare lo stato a un ciclo di monitoraggio. Non ci sono file temporanei usati per scambiare lo stato, quindi niente da pulire. Se non sei sicuro del numero di lavori in background, potrebbe essere la condizione di interruzione [ -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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.