Exit-Code eines Hintergrundprozesses abrufen

129

Ich habe einen Befehl CMD, der von meinem Haupt-Bourne-Shell-Skript aufgerufen wird und ewig dauert.

Ich möchte das Skript wie folgt ändern:

  1. Führen Sie den Befehl CMD parallel als Hintergrundprozess aus ( CMD &).
  2. Haben Sie im Hauptskript eine Schleife, um den erzeugten Befehl alle paar Sekunden zu überwachen. Die Schleife gibt auch einige Nachrichten an stdout zurück, die den Fortschritt des Skripts anzeigen.
  3. Verlassen Sie die Schleife, wenn der erzeugte Befehl beendet wird.
  4. Erfassen und melden Sie den Exit-Code des erzeugten Prozesses.

Kann mir jemand Hinweise geben, um dies zu erreichen?

Bob
quelle
1
...und der Gewinner ist?
TrueY
1
@TrueY .. bob hat sich seit dem Tag, an dem er die Frage gestellt hat, nicht mehr angemeldet. Wir werden es wahrscheinlich nie erfahren!
Ghoti

Antworten:

126

1: Enthält in bash $!die PID des zuletzt ausgeführten Hintergrundprozesses. Das wird Ihnen sowieso sagen, welchen Prozess Sie überwachen müssen.

4: wait <n>Wartet, bis der Prozess mit PID <n>abgeschlossen ist (er wird blockiert, bis der Prozess abgeschlossen ist, sodass Sie ihn möglicherweise erst aufrufen möchten, wenn Sie sicher sind, dass der Prozess abgeschlossen ist), und gibt dann den Exit-Code des abgeschlossenen Prozesses zurück.

2, 3: psoder ps | grep " $! "kann Ihnen sagen, ob der Prozess noch läuft. Es liegt an Ihnen, wie Sie die Ausgabe verstehen und entscheiden, wie nah sie am Ende ist. ( ps | grepist nicht idiotensicher. Wenn Sie Zeit haben, können Sie eine robustere Methode finden, um festzustellen, ob der Prozess noch ausgeführt wird.)

Hier ist ein Skelett-Skript:

# 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
Mob
quelle
15
ps -p $my_pid -o pid=beides grepwird nicht benötigt.
Bis auf weiteres angehalten.
53
kill -0 $!ist eine bessere Methode, um festzustellen, ob ein Prozess noch ausgeführt wird. Es sendet eigentlich kein Signal, sondern überprüft nur, ob der Prozess aktiv ist, und verwendet eine integrierte Shell anstelle externer Prozesse. Wie man 2 killgesagt: "Wenn sig 0 ist, wird kein Signal gesendet, aber es wird immer noch eine Fehlerprüfung durchgeführt. Dies kann verwendet werden, um das Vorhandensein einer Prozess-ID oder einer Prozessgruppen-ID zu überprüfen."
Ephemient
14
@ephemient kill -0gibt einen Wert ungleich Null zurück, wenn Sie nicht berechtigt sind, Signale an einen laufenden Prozess zu senden. Leider kehrt es 1sowohl in diesem Fall als auch in dem Fall zurück, in dem der Prozess nicht existiert. Dies ist nützlich, es sei denn, Sie besitzen den Prozess nicht. Dies kann auch bei Prozessen der Fall sein, die Sie erstellt haben, wenn ein Tool wie dieses sudobeteiligt ist oder wenn sie setuid sind (und möglicherweise Privilegien löschen).
Craig Ringer
13
waitgibt den Exit-Code in der Variablen nicht zurück $?. Es gibt nur den Exit-Code zurück und $?ist der Exit-Code des neuesten Vordergrundprogramms.
MindlessRanger
7
Für die vielen Menschen , die Abstimmung auf kill -0. Hier ist eine Peer-Review-Referenz von SO, die zeigt, dass CraigRingers Kommentar in Bezug auf Folgendes legitim ist: Gibt kill -0für laufende Prozesse einenps -p Wert ungleich Null zurück ... für jeden laufenden Prozess wird jedoch immer 0 zurückgegeben .
Trevor Boyd Smith
57

So habe ich es gelöst, als ich ein ähnliches Bedürfnis hatte:

# 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
Björn
quelle
Ich mag diesen Ansatz.
Kemin Zhou
Vielen Dank! Dieser scheint mir der einfachste Ansatz zu sein.
Luke Davis
4
Diese Lösung erfüllt nicht die Anforderung Nr. 2: eine Überwachungsschleife pro Hintergrundprozess. waits veranlasst das Skript, bis zum Ende jedes Prozesses zu warten.
DKroot
Einfacher und netter Ansatz .. habe schon seit einiger Zeit nach dieser Lösung gesucht ..
Santosh Kumar Arjunan
Dies funktioniert nicht .. oder macht nicht das, was Sie wollen: Es prüft nicht, ob Hintergrundprozesse den Status beenden?
Conny
10

Die PID eines untergeordneten Hintergrundprozesses wird in $ gespeichert ! . Sie können die Pids aller untergeordneten Prozesse in einem Array speichern, z . B. PIDS [] .

wait [-n] [jobspec or pid …]

Warten Sie, bis der untergeordnete Prozess, der von jeder Prozess-ID-PID oder Jobspezifikation angegeben wird, beendet wird, und geben Sie den Beendigungsstatus des zuletzt warteten Befehls zurück. Wenn eine Jobspezifikation angegeben ist, wird auf alle Prozesse im Job gewartet. Wenn keine Argumente angegeben werden, wird auf alle derzeit aktiven untergeordneten Prozesse gewartet, und der Rückgabestatus ist Null. Wenn die Option -n angegeben ist, wartet wait auf das Beenden eines Jobs und gibt seinen Beendigungsstatus zurück. Wenn weder jobspec noch pid einen aktiven untergeordneten Prozess der Shell angeben, lautet der Rückgabestatus 127.

Mit dem Befehl wait können Sie warten, bis alle untergeordneten Prozesse abgeschlossen sind. In der Zwischenzeit können Sie den Exit-Status aller untergeordneten Prozesse über $? Abrufen. und speichern Sie den Status in STATUS [] . Dann können Sie je nach Status etwas tun.

Ich habe die folgenden 2 Lösungen ausprobiert und sie laufen gut. Lösung01 ist prägnanter, während Lösung02 etwas kompliziert ist.

Lösung01

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

Lösung02

#!/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
Terry
quelle
Ich habe versucht und bewiesen, dass es gut läuft. Sie können meine Erklärung im Code lesen.
Terry
Bitte lesen Sie " Wie schreibe ich eine gute Antwort? ", Wo Sie die folgenden Informationen finden: ... versuchen Sie, Einschränkungen, Annahmen oder Vereinfachungen in Ihrer Antwort zu erwähnen. Kürze ist akzeptabel, aber ausführlichere Erklärungen sind besser. Ihre Antwort ist daher akzeptabel, aber Sie haben viel bessere Chancen, positive Stimmen zu erhalten, wenn Sie das Problem und Ihre Lösung näher erläutern können. :-)
Noel Widmer
1
pid=$!; PIDS[$i]=${pid}; ((i+=1))kann einfacher geschrieben werden als das, PIDS+=($!)was einfach an das Array angehängt wird, ohne dass eine separate Variable für die Indizierung oder die PID selbst verwendet werden muss. Gleiches gilt für das STATUSArray.
Codeforester
1
@codeforester, danke für deinen Vorschlag, ich habe meinen Anfangscode in solution01 geändert, er sieht prägnanter aus.
Terry
Das Gleiche gilt für andere Stellen, an denen Sie einem Array Dinge hinzufügen.
Codeforester
8

Wie ich sehe, verwenden fast alle Antworten (meistens ps) externe Dienstprogramme, um den Status des Hintergrundprozesses abzufragen. Es gibt eine unixesh-Lösung, die das SIGCHLD-Signal abfängt. Im Signalhandler muss überprüft werden, welcher untergeordnete Prozess gestoppt wurde. Dies kann durch Eingebautes kill -0 <PID>(Universal) oder Überprüfen des Vorhandenseins eines /proc/<PID>Verzeichnisses (Linux-spezifisch) oder Verwenden desjobs eingebautenSpezifisch. jobs -lmeldet auch die pid. In diesem Fall kann das 3. Feld der Ausgabe gestoppt | Ausführen | Fertig | Beenden sein. ).

Hier ist mein Beispiel.

Der gestartete Prozess wird aufgerufen loop.sh. Es akzeptiert -xoder eine Zahl als Argument. Für -xis-Exits mit Exit-Code 1. Für eine Nummer wartet num * 5 Sekunden. Alle 5 Sekunden wird die PID gedruckt.

Der Launcher-Prozess heißt 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

Weitere Erläuterungen finden Sie unter: Das Starten eines Prozesses über ein Bash-Skript ist fehlgeschlagen

TrueY
quelle
1
Da es sich um Bash handelt, könnte die for-Schleife wie folgt geschrieben worden sein: for i in ${!pids[@]};Verwenden der Parametererweiterung.
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 $?
Abu Aqil
quelle
2
Hier ist ein Trick, um das zu vermeiden grep -v. Sie können die Suche auf den Zeilenanfang beschränken: grep '^'$pidAußerdem können Sie dies ps p $pid -o pid=trotzdem tun . Es tail -fwird auch nicht enden, bis du es tötest, also denke ich nicht, dass es eine sehr gute Möglichkeit ist, dies zu demonstrieren (zumindest ohne darauf hinzuweisen). Möglicherweise möchten Sie die Ausgabe Ihres psBefehls umleiten , /dev/nulloder er wird bei jeder Iteration auf dem Bildschirm angezeigt. Ihre exitUrsachen waitwerden übersprungen - es sollte wahrscheinlich eine sein break. Aber sind das while/ psund das nicht waitüberflüssig?
Bis auf weiteres angehalten.
5
Warum vergisst jeder kill -0 $pid? Es sendet eigentlich kein Signal, sondern überprüft nur, ob der Prozess aktiv ist, und verwendet eine integrierte Shell anstelle externer Prozesse.
Ephemient
3
Weil Sie nur einen Prozess bash: kill: (1) - Operation not permitted
beenden
2
Die Schleife ist redundant. Warte einfach. Weniger Code => weniger Randfälle.
Brais Gabin
@Brais Gabin Die Überwachungsschleife ist Anforderung Nr. 2 der Frage
DKroot
5

Ich würde Ihren Ansatz leicht ändern. Anstatt alle paar Sekunden zu überprüfen, ob der Befehl noch aktiv ist und eine Nachricht meldet, sollten Sie einen anderen Prozess verwenden, der alle paar Sekunden meldet, dass der Befehl noch ausgeführt wird, und diesen Prozess dann beenden, wenn der Befehl beendet ist. Beispielsweise:

#! / bin / sh

cmd () {Schlaf 5; Ausfahrt 24; }}

cmd & # Führen Sie den lang laufenden Prozess aus
pid = $! # Notiere die PID

# Erstellen Sie einen Prozess, der ständig meldet, dass der Befehl noch ausgeführt wird
während echo "$ (Datum): $ pid läuft noch"; schlafe 1; getan &
echoer = $!

# Stellen Sie eine Falle ein, um den Reporter zu töten, wenn der Vorgang abgeschlossen ist
Trap 'Kill $ Echoer' 0

# Warten Sie, bis der Vorgang abgeschlossen ist
wenn warten $ pid; dann
    Echo "cmd erfolgreich"
sonst
    echo "cmd FAILED !! (zurückgegeben $?)"
fi
William Pursell
quelle
tolle Vorlage, danke fürs Teilen! Ich glaube, dass wir anstelle von Trap auch while kill -0 $pid 2> /dev/null; do X; donehoffen können , dass es für jemanden in der Zukunft nützlich ist, der diese Nachricht liest;)
Punkbit
3

Unser Team hatte das gleiche Bedürfnis mit einem von SSH ausgeführten Remote-Skript, dessen Zeit nach 25 Minuten Inaktivität abgelaufen war. Hier ist eine Lösung, bei der die Überwachungsschleife den Hintergrundprozess jede Sekunde überprüft, jedoch nur alle 10 Minuten druckt, um ein Zeitlimit für Inaktivität zu unterdrücken.

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}
DKroot
quelle
2

Ein einfaches Beispiel, ähnlich den obigen Lösungen. Dies erfordert keine Überwachung der Prozessausgabe. Im nächsten Beispiel wird Tail verwendet, um der Ausgabe zu folgen.

$ 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

Verwenden Sie tail, um die Prozessausgabe zu verfolgen und zu beenden, wenn der Prozess abgeschlossen ist.

$ 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
Darren Weber
quelle
1

Eine andere Lösung besteht darin, Prozesse über das proc-Dateisystem zu überwachen (sicherer als die ps / grep-Kombination). Wenn Sie einen Prozess starten, befindet sich in / proc / $ pid ein entsprechender Ordner. Die Lösung könnte also sein

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

Jetzt können Sie die Variable $ exit_status verwenden, wie Sie möchten.

Искрен Хаджинедев
quelle
Funktioniert nicht in Bash? Syntax error: "else" unexpected (expecting "done")
benjaoming
1

Mit dieser Methode muss Ihr Skript nicht auf den Hintergrundprozess warten, sondern nur eine temporäre Datei auf den Exit-Status überwachen.

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

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

Jetzt kann Ihr Skript alles andere tun, während Sie nur den Inhalt von retFile überwachen müssen (es kann auch andere gewünschte Informationen enthalten, z. B. die Beendigungszeit).

PS.: Übrigens habe ich das Denken in Bash codiert

Wassermann-Kraft
quelle
0

Dies kann über Ihre Frage hinausgehen. Wenn Sie jedoch Bedenken haben, wie lange Prozesse ausgeführt werden, sind Sie möglicherweise daran interessiert, den Status der Ausführung von Hintergrundprozessen nach einem bestimmten Zeitraum zu überprüfen. Es ist einfach genug zu überprüfen, welche untergeordneten PIDs noch ausgeführt werden pgrep -P $$. Ich habe jedoch die folgende Lösung gefunden, um den Exit-Status der bereits abgelaufenen PIDs zu überprüfen:

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

welche Ausgänge:

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

Hinweis: Sie können $pidszu einer Zeichenfolgenvariablen anstatt zu einem Array wechseln , um die Dinge zu vereinfachen, wenn Sie möchten.

errant.info
quelle
0

Meine Lösung bestand darin, eine anonyme Pipe zu verwenden, um den Status an eine Überwachungsschleife zu übergeben. Es werden keine temporären Dateien zum Austauschen des Status verwendet, sodass keine Bereinigung erforderlich ist. Wenn Sie sich über die Anzahl der Hintergrundjobs nicht sicher waren, könnte die Unterbrechungsbedingung sein [ -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
James Dingwall
quelle