Jak zabić proces dziecka po określonym czasie w Bash?

178

Mam skrypt bash, który uruchamia proces potomny, który od czasu do czasu ulega awarii (właściwie się zawiesza) i bez wyraźnego powodu (zamknięte źródło, więc niewiele mogę z tym zrobić). W rezultacie chciałbym móc uruchomić ten proces na określony czas i zabić go, jeśli nie powrócił on pomyślnie po określonym czasie.

Czy istnieje prosty i solidny sposób na osiągnięcie tego za pomocą basha?

PS: powiedz mi, czy to pytanie lepiej pasuje do błędu serwera lub superużytkownika.

Greg
źródło
Związane z: stackoverflow.com/q/601543/132382
pilcrow
Bardzo kompletna odpowiedź tutaj: stackoverflow.com/a/58873049/2635443
Orsiris de Jong

Odpowiedzi:

260

(Jak widać w: BASH FAQ, wpis nr 68: „Jak uruchomić polecenie i zatrzymać je (przekroczenie limitu czasu) po N sekundach?” )

Jeśli nie przeszkadza pobieranie coś, wykorzystania timeout( sudo apt-get install timeout) i używać go tak: (większość systemów nie jest już zainstalowany inny sposób wykorzystywać sudo apt-get install coreutils)

timeout 10 ping www.goooooogle.com

Jeśli nie chcesz czegoś pobierać, zrób to, co limit czasu robi wewnętrznie:

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

W przypadku, gdy chcesz zrobić limit czasu dla dłuższego kodu bash, użyj drugiej opcji jako takiej:

( cmdpid=$BASHPID; 
    (sleep 10; kill $cmdpid) \
   & while ! ping -w 1 www.goooooogle.com 
     do 
         echo crap; 
     done )
Ignacio Vazquez-Abrams
źródło
8
Odpowiedź Re Ignacio na wypadek, gdyby ktoś inny zastanawiał się, co zrobiłem: cmdpid=$BASHPIDnie weźmie pid powłoki wywołującej, ale (pierwsza) podpowłoka, która jest uruchamiana przez (). To (sleep... wywołuje drugą podpowłokę w pierwszej podpowłoce, aby czekała 10 sekund w tle i zabijała pierwszą podpowłokę, która po uruchomieniu zabójczego procesu podpowłoki przechodzi do wykonania swojego obciążenia ...
jamadagni
17
timeoutjest częścią coreutils GNU, więc powinien być już zainstalowany we wszystkich systemach GNU.
Sameer
1
@Sameer: ​​Tylko od wersji 8.
Ignacio Vazquez-Abrams,
3
Nie jestem tego w 100% pewien, ale o ile wiem (i wiem, co powiedział mi mój podręcznik) timeoutjest teraz częścią coreutils.
benaryorg
5
To polecenie nie „kończy się wcześnie”. Zawsze będzie zabijać proces po przekroczeniu limitu czasu - ale nie obsłuży sytuacji, w której nie upłynął limit czasu.
hawkeye
28
# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) &

lub aby otrzymać kody wyjścia:

# 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=$?
Dan
źródło
8
Nie powinieneś używać kill -9przed wypróbowaniem sygnałów, które proces może przetworzyć jako pierwszy.
Wstrzymano do odwołania.
To prawda, chciałem jednak szybko naprawić i po prostu założyłem, że chce, aby proces został natychmiast zakończony, ponieważ powiedział, że się zawiesza
Dan
8
To właściwie bardzo złe rozwiązanie. Co jeśli dosmthzakończy się za 2 sekundy, inny proces zajmie stary pid, a ty zabijesz nowy?
Teleporting Goat
Recykling PID działa poprzez osiągnięcie limitu i zawijanie. Jest bardzo mało prawdopodobne, aby inny proces ponownie wykorzystał PID w ciągu pozostałych 8 sekund, chyba że system całkowicie zwariował.
kittydoor
13
sleep 999&
t=$!
sleep 10
kill $t
DigitalRoss
źródło
To powoduje nadmierne czekanie. Co jeśli prawdziwa komenda ( sleep 999tutaj) często kończy się szybciej niż narzucony sen ( sleep 10)? Co jeśli chcę dać mu szansę do 1 minuty, 5 minut? A co jeśli mam w skrypcie kilka takich przypadków :)
it3xl
3

Miałem też to pytanie i znalazłem dwie inne bardzo przydatne rzeczy:

  1. Zmienna SECONDS w bash.
  2. Polecenie „pgrep”.

Więc używam czegoś takiego w linii poleceń (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

Ponieważ jest to pętla, dodałem „uśpienie 0.2”, aby procesor był chłodny. ;-)

(BTW: ping i tak jest złym przykładem, po prostu użyłbyś wbudowanej opcji „-t” (timeout).)

Ulrich
źródło
1

Zakładając, że masz (lub możesz łatwo utworzyć) plik pid do śledzenia pid dziecka, możesz następnie utworzyć skrypt, który sprawdza czas modyfikacji pliku pid i zabija / odradza proces w razie potrzeby. Następnie po prostu umieść skrypt w crontab, aby działał mniej więcej w wymaganym okresie.

Daj mi znać, jeśli potrzebujesz więcej informacji. Jeśli to nie brzmi tak, jakby pasowało do twoich potrzeb, co z początkiem?

kojiro
źródło
1

Jednym ze sposobów jest uruchomienie programu w podpowłoce i komunikowanie się z podpowłoką za pośrednictwem nazwanego potoku za pomocą readpolecenia. W ten sposób możesz sprawdzić status zakończenia uruchomionego procesu i przekazać go z powrotem przez potok.

Oto przykład przekroczenia limitu czasu yespolecenia po 3 sekundach. Pobiera PID procesu używającego pgrep(prawdopodobnie działa tylko w systemie Linux). Istnieje również pewien problem z używaniem potoku, ponieważ proces otwierający potok do odczytu zawiesza się, dopóki nie zostanie on również otwarty do zapisu i odwrotnie. Aby zapobiec readzawieszaniu się polecenia, „zaklinowałem” potok do odczytu z podpowłoką w tle. (Innym sposobem, aby zapobiec zamrożeniu otwierania potoku do odczytu i zapisu, tj. read -t 5 <>finished.pipe- to jednak może również nie działać, z wyjątkiem Linuksa.)

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
Gavin Smith
źródło
0

Oto próba uniknięcia zabicia procesu po jego zakończeniu, co zmniejsza szansę na zabicie innego procesu z tym samym identyfikatorem procesu (chociaż prawdopodobnie niemożliwe jest całkowite uniknięcie tego rodzaju błędu).

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
  )
}

Użyj like run_with_timeout 3 sleep 10000, który działa, sleep 10000ale kończy go po 3 sekundach.

Jest to podobne do innych odpowiedzi, które wykorzystują proces przekroczenia limitu czasu w tle do zabicia procesu potomnego po opóźnieniu. Myślę, że jest to prawie to samo, co rozszerzona odpowiedź Dana ( https://stackoverflow.com/a/5161274/1351983 ), z wyjątkiem tego, że powłoka limitu czasu nie zostanie zabita, jeśli już się skończyła.

Po zakończeniu tego programu nadal będzie działać kilka długotrwałych procesów „uśpienia”, ale powinny one być nieszkodliwe.

Może to być lepsze rozwiązanie niż moja inna odpowiedź, ponieważ nie używa funkcji nieprzenośnej powłoki read -ti nie używa pgrep.

Gavin Smith
źródło
Jaka jest różnica między (exec sh -c "$*") &i sh -c "$*" &? A konkretnie, po co używać tego pierwszego zamiast drugiego?
Justin C
0

Oto trzecia odpowiedź, którą tu przedstawiłem. Ten obsługuje przerwania sygnału i czyści procesy w tle, gdy SIGINTzostanie odebrany. Używa sztuczki $BASHPIDi execużytej w górnej odpowiedzi, aby uzyskać PID procesu (w tym przypadku $$w shwywołaniu). Używa FIFO do komunikowania się z podpowłoką odpowiedzialną za zabijanie i czyszczenie. (To jest jak potok w mojej drugiej odpowiedzi , ale posiadanie nazwanego potoku oznacza, że ​​program obsługi sygnału może również pisać do niego.)

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
}

Starałem się unikać warunków wyścigu tak bardzo, jak tylko mogłem. Jednak jednym źródłem błędu, którego nie mogłem usunąć, jest zakończenie procesu w tym samym czasie, co przekroczenie limitu czasu. Na przykład run_with_timeout 2 sleep 2lub run_with_timeout 0 sleep 0. U mnie ten ostatni daje błąd:

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

ponieważ próbuje zabić proces, który już sam się zakończył.

Gavin Smith
źródło