Jak zabić procesy / zadania w tle, gdy mój skrypt powłoki kończy działanie?

193

Szukam sposobu na uporządkowanie bałaganu, gdy mój skrypt najwyższego poziomu kończy działanie.

Zwłaszcza, jeśli chcę użyć set -e, chciałbym, aby proces w tle umarł po zakończeniu skryptu.

Elmarco
źródło

Odpowiedzi:

186

Aby posprzątać trochę bałaganu, trapmożna użyć. Może dostarczyć listę rzeczy wykonanych, gdy nadejdzie określony sygnał:

trap "echo hello" SIGINT

ale może być również użyty do wykonania czegoś, jeśli powłoka wyjdzie:

trap "killall background" EXIT

Jest wbudowany, więc help trappoda informacje (działa z bash). Jeśli chcesz tylko zabijać zadania w tle, możesz to zrobić

trap 'kill $(jobs -p)' EXIT

Uważaj, aby użyć pojedynczego ', aby zapobiec $()natychmiastowemu podstawieniu powłoki .

Johannes Schaub - litb
źródło
to jak zabijasz wszystkie dzieci ? (lub czy brakuje mi czegoś oczywistego)
elmarco
18
killall zabija wasze dzieci, ale nie was
or
4
kill $(jobs -p)nie działa w desce rozdzielczej, ponieważ wykonuje podstawianie poleceń w podpowłoce (zobacz Zastępowanie poleceń w myślniku)
user1431317
8
to killall backgroundma być zastępczy? backgroundnie ma na stronie podręcznika ...
Evan Benn
170

Działa to dla mnie (poprawione dzięki komentatorom):

trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
  • kill -- -$$wysyła SIGTERM do całej grupy procesów, zabijając w ten sposób również potomków.

  • Określenie sygnału EXITjest przydatne podczas używania set -e(więcej szczegółów tutaj ).

tokland
źródło
1
Powinien ogólnie działać dobrze, ale procesy potomne mogą zmieniać grupy procesów. Z drugiej strony nie wymaga kontroli zadań, a inne procesy mogą powodować pominięcie niektórych procesów wnuka.
michaeljt
5
Uwaga: „kill 0” zabije również nadrzędny skrypt bash. Możesz użyć polecenia „kill - - $ BASHPID”, aby zabić tylko dzieci bieżącego skryptu. Jeśli nie masz $ BASHPID w swojej wersji bash, możesz wyeksportować BASHPID = $ (sh -c 'echo $ PPID')
ACykliczny
2
Dziękujemy za miłe i jasne rozwiązanie! Niestety segfaultuje Bash 4.3, co pozwala na rekurencję pułapki. Natknąłem się na to na 4.3.30(1)-releaseOSX i jest to potwierdzone również na Ubuntu . Jest jednak obvoius wokaround :)
skozin
4
Nie do końca rozumiem -$$. Zwraca wartość do „- <PID>„ np -1234. Na wbudowanej stronie zabijania // wiodący myślnik określa sygnał do wysłania. Jednak - prawdopodobnie blokuje to, ale w przeciwnym razie wiodący myślnik nie jest udokumentowany. Jakaś pomoc?
Evan Benn
4
@EvanBenn: Sprawdź man 2 kill, co wyjaśnia, że ​​gdy PID jest ujemny, sygnał jest wysyłany do wszystkich procesów w grupie procesów o podanym identyfikatorze ( en.wikipedia.org/wiki/Process_group ). To mylące, że nie jest to wymienione w man 1 killlub man bash, i może być uznane za błąd w dokumentacji.
user001
111

Aktualizacja: https://stackoverflow.com/a/53714583/302079 poprawia to, dodając status wyjścia i funkcję czyszczenia.

trap "exit" INT TERM
trap "kill 0" EXIT

Po co konwertować INTi TERMwyjść? Ponieważ oba powinny wyzwalać kill 0nie wchodząc w nieskończoną pętlę.

Dlaczego spust kill 0naEXIT ? Ponieważ normalne wyjścia ze skryptów również powinny się uruchamiać kill 0.

Dlaczego kill 0? Ponieważ zagnieżdżone podpowłoki również muszą zostać zabite. Spowoduje to usunięcie całego drzewa procesów .

Korkman
źródło
3
Jedyne rozwiązanie dla mojej sprawy na Debianie.
MindlessRanger,
3
Ani odpowiedź Johannesa Schauba, ani odpowiedź udzielona przez tokland nie zdołały zabić procesów w tle, które uruchomił mój skrypt powłoki (na Debianie). To rozwiązanie działało. Nie wiem, dlaczego ta odpowiedź nie jest bardziej pozytywna. Czy mógłbyś bardziej rozwinąć temat tego, co dokładnie kill 0oznacza / robi?
josch
7
To jest niesamowite, ale także zabija moją powłokę rodzicielską :-(
vidstige
5
To rozwiązanie jest dosłownie przesadą. kill 0 (wewnątrz mojego skryptu) zrujnował całą moją sesję X! Być może w niektórych przypadkach zabicie 0 może być przydatne, ale nie zmienia to faktu, że nie jest to ogólne rozwiązanie i należy go unikać, jeśli to możliwe, chyba że istnieje bardzo dobry powód, aby go użyć. Byłoby miło dodać ostrzeżenie, że może zabić powłokę nadrzędną lub nawet całą sesję X, a nie tylko zadania skryptu w tle!
Lissanro Rayen
3
Chociaż w niektórych okolicznościach może to być interesujące rozwiązanie, jak wskazał @vidstige, zabije to całą grupę procesów, która obejmuje proces uruchamiania (tj. W większości przypadków powłokę nadrzędną). Zdecydowanie nie jest to coś, czego chcesz, gdy uruchamiasz skrypt za pomocą IDE.
matpen
21

pułapka „kill $ (jobs -p)” EXIT

Dokonałbym tylko drobnych zmian w odpowiedzi Johannesa i używał zadania -pr, aby ograniczyć zabijanie do uruchomionych procesów i dodać kilka dodatkowych sygnałów do listy:

trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
raytraced
źródło
Dlaczego nie zabić również zatrzymanych zleceń? W Bash EXIT pułapka będzie uruchamiana również w przypadku SIGINT i SIGTERM, więc pułapka będzie wywoływana dwukrotnie w przypadku takiego sygnału.
jarno
14

trap 'kill 0' SIGINT SIGTERM EXITRozwiązanie opisane w użytkownika @ tokland odpowiedź jest naprawdę ładne, ale ostatni Bash awarii z winy segmentacja podczas korzystania z niego. Jest tak, ponieważ Bash, począwszy od wersji 4.3, pozwala na rekurencję pułapki, która w tym przypadku staje się nieskończona:

  1. proces powłoki otrzymuje SIGINTlub SIGTERMlub EXIT;
  2. sygnał zostaje uwięziony podczas wykonywania kill 0, co wysyła SIGTERMdo wszystkich procesów w grupie, w tym do samej powłoki;
  3. przejdź do 1 :)

Można to obejść, ręcznie wyrejestrowując pułapkę:

trap 'trap - SIGTERM && kill 0' SIGINT SIGTERM EXIT

Bardziej wymyślny sposób, który pozwala wydrukować otrzymany sygnał i pozwala uniknąć komunikatów „Zakończone”:

#!/usr/bin/env bash

trap_with_arg() { # from https://stackoverflow.com/a/2183063/804678
  local func="$1"; shift
  for sig in "$@"; do
    trap "$func $sig" "$sig"
  done
}

stop() {
  trap - SIGINT EXIT
  printf '\n%s\n' "recieved $1, killing children"
  kill -s SIGINT 0
}

trap_with_arg 'stop' EXIT SIGINT SIGTERM SIGHUP

{ i=0; while (( ++i )); do sleep 0.5 && echo "a: $i"; done } &
{ i=0; while (( ++i )); do sleep 0.6 && echo "b: $i"; done } &

while true; do read; done

UPD : dodano minimalny przykład; ulepszona stopfunkcja aviod-pułapkowania niepotrzebnych sygnałów i ukrywania komunikatów „Terminated:” na wyjściu. Dzięki Trevor Boyd Smith za sugestie!

skozin
źródło
W stop()podać pierwszy argument jako numer sygnału ale wtedy hardcode jakie sygnały są wyrejestrowane. zamiast na stałe wyrejestrowywać sygnały, możesz użyć pierwszego argumentu do wyrejestrowania stop()funkcji (może to potencjalnie zatrzymać inne sygnały rekurencyjne (inne niż 3 zakodowane na stałe)).
Trevor Boyd Smith
@TrevorBoydSmith, to chyba nie działałoby zgodnie z oczekiwaniami. Na przykład pocisk może zostać zabity SIGINT, ale kill 0wysyła SIGTERM, co ponownie zostanie uwięzione. Nie spowoduje to jednak nieskończonej rekurencji, ponieważ SIGTERMpodczas drugiego stoppołączenia zostanie zablokowany .
skozin
Prawdopodobnie trap - $1 && kill -s $1 0powinien działać lepiej. Przetestuję i zaktualizuję tę odpowiedź. Dziękuję za fajny pomysł! :)
skozin
Nie, trap - $1 && kill -s $1 0też nie zadziała, bo nie możemy zabijać EXIT. Ale to naprawdę wystarczy zrobić pułapkę TERM, ponieważ killdomyślnie wysyła ten sygnał.
skozin
Przetestowałem rekurencję EXIT, trapprocedura obsługi sygnału jest zawsze wykonywana tylko raz.
Trevor Boyd Smith
9

Dla pewności lepiej jest zdefiniować funkcję czyszczenia i wywołać ją z pułapki:

cleanup() {
        local pids=$(jobs -pr)
        [ -n "$pids" ] && kill $pids
}
trap "cleanup" INT QUIT TERM EXIT [...]

lub całkowicie unikając funkcji:

trap '[ -n "$(jobs -pr)" ] && kill $(jobs -pr)' INT QUIT TERM EXIT [...]

Czemu? Ponieważ po prostu używając trap 'kill $(jobs -pr)' [...]jednego zakłada się, że będą uruchomione zadania w tle, gdy zasygnalizowany zostanie stan pułapki. Gdy nie ma żadnych zadań, zobaczysz następujący (lub podobny) komunikat:

kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]

ponieważ jobs -prjest pusty - skończyłem w tej „pułapce” (gra słów zamierzona).

tdaitx
źródło
Ten przypadek testowy [ -n "$(jobs -pr)" ]nie działa na moim bashu. Używam GNU bash, wersja 4.2.46 (2) -release (x86_64-redhat-linux-gnu). Pojawia się komunikat „zabij: użycie”.
Douwe van der Leest
Podejrzewam, że ma to związek z faktem, że jobs -prnie zwraca PID dzieci procesów w tle. Nie rozrywa całego drzewa procesu, jedynie przycina korzenie.
Douwe van der Leest
2

Ładna wersja, która działa pod Linuksem, BSD i MacOS X. Najpierw próbuje wysłać SIGTERM, a jeśli to się nie powiedzie, zabija proces po 10 sekundach.

KillJobs() {
    for job in $(jobs -p); do
            kill -s SIGTERM $job > /dev/null 2>&1 || (sleep 10 && kill -9 $job > /dev/null 2>&1 &)

    done
}

TrapQuit() {
    # Whatever you need to clean here
    KillJobs
}

trap TrapQuit EXIT

Pamiętaj, że zadania nie obejmują procesów grand dzieci.

Orsiris de Jong
źródło
2
function cleanup_func {
    sleep 0.5
    echo cleanup
}

trap "exit \$exit_code" INT TERM
trap "exit_code=\$?; cleanup_func; kill 0" EXIT

# exit 1
# exit 0

Jak https://stackoverflow.com/a/22644006/10082476 , ale z dodanym kodem wyjścia

Delaware
źródło
Skąd się exit_codebierze INT TERMpułapka?
jarno
1

Inną opcją jest ustawienie skryptu jako lidera grupy procesów i wychwycenie killpg w grupie procesów przy wyjściu.

orip
źródło
Jak ustawić proces jako lidera grupy procesów? Co to jest „killpg”?
jarno
0

Więc skrypt ładuje skrypt. Uruchom killallkomendę (lub inną dostępną w systemie operacyjnym), która zostanie wykonana natychmiast po zakończeniu skryptu.

Oli
źródło
0

zadania -p nie działa we wszystkich powłokach, jeśli są wywoływane w podpowłoce, chyba że jego dane wyjściowe zostaną przekierowane do pliku, ale nie do potoku. (Zakładam, że pierwotnie był przeznaczony wyłącznie do użytku interaktywnego).

Co z następującymi kwestiami:

trap 'while kill %% 2>/dev/null; do jobs > /dev/null; done' INT TERM EXIT [...]

Wezwanie do „zadań” jest potrzebne w powłoce Debiana, która nie aktualizuje bieżącego zadania („%%”), jeśli go brakuje.

michaeljt
źródło
Hmm ciekawe podejście, ale wydaje się, że nie działa. Rozważ scipt trap 'echo in trap; set -x; trap - TERM EXIT; while kill %% 2>/dev/null; do jobs > /dev/null; done; set +x' INT TERM EXIT; sleep 100 & while true; do printf .; sleep 1; doneJeśli uruchomisz go w Bash (5.0.3) i spróbujesz zakończyć, wydaje się, że istnieje nieskończona pętla. Jeśli jednak zakończysz to ponownie, zadziała. Nawet przez Dash (0.5.10.2-6) musisz go dwukrotnie zakończyć.
jarno
0

Dokonałem adaptacji odpowiedzi @ tokland w połączeniu z wiedzą z http://veithen.github.io/2014/11/16/sigterm-propagation.html, gdy zauważyłem, że trapnie uruchamia się, jeśli uruchomię proces pierwszego planu (bez tła &):

#!/bin/bash

# killable-shell.sh: Kills itself and all children (the whole process group) when killed.
# Adapted from http://stackoverflow.com/a/2173421 and http://veithen.github.io/2014/11/16/sigterm-propagation.html
# Note: Does not work (and cannot work) when the shell itself is killed with SIGKILL, for then the trap is not triggered.
trap "trap - SIGTERM && echo 'Caught SIGTERM, sending SIGTERM to process group' && kill -- -$$" SIGINT SIGTERM EXIT

echo $@
"$@" &
PID=$!
wait $PID
trap - SIGINT SIGTERM EXIT
wait $PID

Przykład jego działania:

$ bash killable-shell.sh sleep 100
sleep 100
^Z
[1]  + 31568 suspended  bash killable-shell.sh sleep 100

$ ps aux | grep "sleep"
niklas   31568  0.0  0.0  19640  1440 pts/18   T    01:30   0:00 bash killable-shell.sh sleep 100
niklas   31569  0.0  0.0  14404   616 pts/18   T    01:30   0:00 sleep 100
niklas   31605  0.0  0.0  18956   936 pts/18   S+   01:30   0:00 grep --color=auto sleep

$ bg
[1]  + 31568 continued  bash killable-shell.sh sleep 100

$ kill 31568
Caught SIGTERM, sending SIGTERM to process group
[1]  + 31568 terminated  bash killable-shell.sh sleep 100

$ ps aux | grep "sleep"
niklas   31717  0.0  0.0  18956   936 pts/18   S+   01:31   0:00 grep --color=auto sleep
nh2
źródło
0

Dla urozmaicenia opublikuję odmianę https://stackoverflow.com/a/2173421/102484 , ponieważ to rozwiązanie prowadzi do komunikatu „Zakończono” w moim środowisku:

trap 'test -z "$intrap" && export intrap=1 && kill -- -$$' SIGINT SIGTERM EXIT
noonex
źródło