Wie kann man einen untergeordneten Prozess nach einer bestimmten Zeitüberschreitung in Bash beenden?


178

Ich habe ein Bash-Skript, das einen untergeordneten Prozess startet, der von Zeit zu Zeit und ohne ersichtlichen Grund abstürzt (tatsächlich hängt) (Closed Source, daher kann ich nicht viel dagegen tun). Aus diesem Grund möchte ich diesen Prozess für eine bestimmte Zeit starten und beenden können, wenn er nach einer bestimmten Zeit nicht erfolgreich zurückgegeben wurde.

Gibt es eine einfache und robuste Möglichkeit, dies mit Bash zu erreichen?

PS: Sagen Sie mir, ob diese Frage besser für Serverfehler oder Superuser geeignet ist.



Sehr vollständige Antwort hier: stackoverflow.com/a/58873049/2635443
Orsiris de Jong

Antworten:


260

(Wie in: BASH FAQ-Eintrag Nr. 68 zu sehen: "Wie führe ich einen Befehl aus und lasse ihn nach N Sekunden abbrechen (Timeout)?" )

Wenn es Ihnen nichts ausmacht, etwas herunterzuladen, verwenden Sie timeout( sudo apt-get install timeout) und verwenden Sie es wie folgt: (Die meisten Systeme haben es bereits installiert, andernfalls verwenden Sie sudo apt-get install coreutils)

timeout 10 ping www.goooooogle.com

Wenn Sie etwas nicht herunterladen möchten, führen Sie das interne Timeout aus:

( cmdpid=$BASHPID; (sleep 10; kill $cmdpid) & exec ping www.goooooogle.com )

Wenn Sie eine Zeitüberschreitung für längeren Bash-Code durchführen möchten, verwenden Sie die zweite Option als solche:

( cmdpid=$BASHPID; 
    (sleep 10; kill $cmdpid) \
   & while ! ping -w 1 www.goooooogle.com 
     do 
         echo crap; 
     done )

8
Re Ignacios Antwort für den Fall, dass sich jemand fragt, was ich getan habe: Der cmdpid=$BASHPIDwird nicht die PID der aufrufenden Shell nehmen, sondern die (erste) Subshell, mit der begonnen wird (). Das (sleepDing ... ruft eine zweite Unterschale innerhalb der ersten Unterschale auf, um 10 Sekunden im Hintergrund zu warten und die erste Unterschale zu töten, die nach dem Starten des Killer-Unterschalenprozesses ihre Arbeitslast ausführt ...
jamadagni

17
timeoutist Teil von GNU coreutils und sollte daher bereits in allen GNU-Systemen installiert sein.
Sameer

1
@Sameer: ​​Nur ab Version 8.
Ignacio Vazquez-Abrams

3
Ich bin mir da nicht 100% sicher, aber soweit ich weiß (und ich weiß, was meine Manpage mir gesagt hat), timeoutist es jetzt Teil der Coreutils.
Benaryorg

5
Dieser Befehl wird nicht vorzeitig beendet. Der Prozess wird immer zum Zeitpunkt des Zeitlimits abgebrochen - aber die Situation, in der kein Zeitlimit aufgetreten ist, wird nicht behandelt.
Hawkeye

28
# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) &

oder um auch die Exit-Codes zu erhalten:

# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) & waiter=$!
# wait on our worker process and return the exitcode
exitcode=$(wait $pid && echo $?)
# kill the waiter subshell, if it still runs
kill -9 $waiter 2>/dev/null
# 0 if we killed the waiter, cause that means the process finished before the waiter
finished_gracefully=$?

8
Sie sollten nicht verwenden, kill -9bevor Sie Signale versuchen, die ein Prozess zuerst verarbeiten kann.
Bis auf weiteres angehalten.

Es stimmt, ich wollte jedoch eine schnelle Lösung und ging einfach davon aus, dass er den Prozess sofort
Dan

8
Das ist eigentlich eine sehr schlechte Lösung. Was ist, wenn dosmthin 2 Sekunden beendet wird, ein anderer Prozess die alte PID übernimmt und Sie die neue töten?
Teleporting Ziege

Das PID-Recycling funktioniert, indem es die Grenze erreicht und umwickelt. Es ist sehr unwahrscheinlich, dass ein anderer Prozess die PID innerhalb der verbleibenden 8 Sekunden wiederverwendet, es sei denn, das System läuft vollständig durcheinander.
Kittydoor

13
sleep 999&
t=$!
sleep 10
kill $t

Es entsteht übermäßiges Warten. Was ist, wenn ein echter Befehl ( sleep 999hier) oft schneller beendet wird als der auferlegte Schlaf ( sleep 10)? Was ist, wenn ich ihm eine Chance von bis zu 1 Minute, 5 Minuten geben möchte? Was ist, wenn ich eine Reihe solcher Fälle in meinem Skript habe :)
it3xl

3

Ich hatte auch diese Frage und fand zwei weitere Dinge sehr nützlich:

  1. Die SECONDS-Variable in bash.
  2. Der Befehl "pgrep".

Also benutze ich so etwas in der Kommandozeile (OSX 10.9):

ping www.goooooogle.com & PING_PID=$(pgrep 'ping'); SECONDS=0; while pgrep -q 'ping'; do sleep 0.2; if [ $SECONDS = 10 ]; then kill $PING_PID; fi; done

Da dies eine Schleife ist, habe ich "sleep 0.2" eingefügt, um die CPU kühl zu halten. ;-);

(Übrigens: Ping ist sowieso ein schlechtes Beispiel, Sie würden einfach die eingebaute Option "-t" (Timeout) verwenden.)


1

Angenommen, Sie haben eine PID-Datei (oder können diese leicht erstellen), um die PID des Kindes zu verfolgen, können Sie ein Skript erstellen, das die Modtime der PID-Datei überprüft und den Prozess nach Bedarf beendet / erneut startet. Dann legen Sie das Skript einfach in crontab ab, damit es ungefähr in dem von Ihnen benötigten Zeitraum ausgeführt wird.

Lassen Sie mich wissen, wenn Sie weitere Details benötigen. Wenn das nicht so klingt, als würde es Ihren Bedürfnissen entsprechen, was ist dann mit Emporkömmling?


1

Eine Möglichkeit besteht darin, das Programm in einer Subshell auszuführen und mit dem Befehl über eine Named Pipe mit der Subshell zu kommunizieren read. Auf diese Weise können Sie den Exit-Status des ausgeführten Prozesses überprüfen und diesen über die Pipe zurückmelden.

Hier ist ein Beispiel für die Zeitüberschreitung des yesBefehls nach 3 Sekunden. Es erhält die PID des Prozesses mit pgrep(funktioniert möglicherweise nur unter Linux). Es gibt auch ein Problem bei der Verwendung einer Pipe, da ein Prozess, der eine Pipe zum Lesen öffnet, hängen bleibt, bis sie auch zum Schreiben geöffnet wird, und umgekehrt. Um zu verhindern, dass der readBefehl hängt, habe ich die Pipe zum Lesen mit einer Hintergrund-Subshell "eingeklemmt". (Eine andere Möglichkeit, ein Einfrieren zum Öffnen der Pipe mit Lese- und Schreibzugriff zu verhindern, dh read -t 5 <>finished.pipedies funktioniert möglicherweise auch nur unter Linux.)

rm -f finished.pipe
mkfifo finished.pipe

{ yes >/dev/null; echo finished >finished.pipe ; } &
SUBSHELL=$!

# Get command PID
while : ; do
    PID=$( pgrep -P $SUBSHELL yes )
    test "$PID" = "" || break
    sleep 1
done

# Open pipe for writing
{ exec 4>finished.pipe ; while : ; do sleep 1000; done } &  

read -t 3 FINISHED <finished.pipe

if [ "$FINISHED" = finished ] ; then
  echo 'Subprocess finished'
else
  echo 'Subprocess timed out'
  kill $PID
fi

rm finished.pipe

0

Hier ist ein Versuch, der versucht, das Beenden eines Prozesses zu vermeiden, nachdem er bereits beendet wurde, wodurch die Wahrscheinlichkeit verringert wird, dass ein anderer Prozess mit derselben Prozess-ID beendet wird (obwohl es wahrscheinlich unmöglich ist, diese Art von Fehler vollständig zu vermeiden).

run_with_timeout ()
{
  t=$1
  shift

  echo "running \"$*\" with timeout $t"

  (
  # first, run process in background
  (exec sh -c "$*") &
  pid=$!
  echo $pid

  # the timeout shell
  (sleep $t ; echo timeout) &
  waiter=$!
  echo $waiter

  # finally, allow process to end naturally
  wait $pid
  echo $?
  ) \
  | (read pid
     read waiter

     if test $waiter != timeout ; then
       read status
     else
       status=timeout
     fi

     # if we timed out, kill the process
     if test $status = timeout ; then
       kill $pid
       exit 99
     else
       # if the program exited normally, kill the waiting shell
       kill $waiter
       exit $status
     fi
  )
}

Verwenden Sie like run_with_timeout 3 sleep 10000, das ausgeführt wird, sleep 10000aber nach 3 Sekunden beendet wird.

Dies ist wie bei anderen Antworten, bei denen ein Hintergrund-Timeout-Prozess verwendet wird, um den untergeordneten Prozess nach einer Verzögerung abzubrechen. Ich denke, dies entspricht fast der erweiterten Antwort von Dan ( https://stackoverflow.com/a/5161274/1351983 ), außer dass die Timeout-Shell nicht beendet wird, wenn sie bereits beendet ist.

Nach dem Ende dieses Programms werden noch einige "Schlaf" -Prozesse ausgeführt, die jedoch harmlos sein sollten.

Dies ist möglicherweise eine bessere Lösung als meine andere Antwort, da die nicht tragbare Shell-Funktion nicht verwendet wird read -t und nicht verwendet wird pgrep.


Was ist der Unterschied zwischen (exec sh -c "$*") &und sh -c "$*" &? Warum sollte erstere anstelle der letzteren verwendet werden?
Justin C

0

Hier ist die dritte Antwort, die ich hier eingereicht habe. Dieser behandelt Signalunterbrechungen und bereinigt Hintergrundprozesse, wenn sie SIGINTempfangen werden. Es verwendet den in der oberen Antwort verwendeten Trick $BASHPIDund , um die PID eines Prozesses zu erhalten (in diesem Fall in einem Aufruf). Es verwendet ein FIFO, um mit einer Subshell zu kommunizieren, die für das Töten und Aufräumen verantwortlich ist. (Dies ist wie die Pipe in meiner zweiten Antwort , aber eine Named Pipe bedeutet, dass der Signalhandler auch in sie schreiben kann.)exec$$sh

run_with_timeout ()
{
  t=$1 ; shift

  trap cleanup 2

  F=$$.fifo ; rm -f $F ; mkfifo $F

  # first, run main process in background
  "$@" & pid=$!

  # sleeper process to time out
  ( sh -c "echo \$\$ >$F ; exec sleep $t" ; echo timeout >$F ) &
  read sleeper <$F

  # control shell. read from fifo.
  # final input is "finished".  after that
  # we clean up.  we can get a timeout or a
  # signal first.
  ( exec 0<$F
    while : ; do
      read input
      case $input in
        finished)
          test $sleeper != 0 && kill $sleeper
          rm -f $F
          exit 0
          ;;
        timeout)
          test $pid != 0 && kill $pid
          sleeper=0
          ;;
        signal)
          test $pid != 0 && kill $pid
          ;;
      esac
    done
  ) &

  # wait for process to end
  wait $pid
  status=$?
  echo finished >$F
  return $status
}

cleanup ()
{
  echo signal >$$.fifo
}

Ich habe versucht, die Rennbedingungen so weit wie möglich zu vermeiden. Eine Fehlerquelle, die ich nicht beseitigen konnte, ist jedoch, wenn der Prozess fast zur gleichen Zeit wie das Zeitlimit endet. Zum Beispiel run_with_timeout 2 sleep 2oder run_with_timeout 0 sleep 0. Letzteres gibt für mich einen Fehler:

timeout.sh: line 250: kill: (23248) - No such process

da es versucht, einen Prozess zu beenden, der bereits von selbst beendet wurde.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.