Bagaimana cara saya mematikan proses latar belakang / pekerjaan saat skrip shell saya keluar?

193

Saya mencari cara untuk membersihkan kekacauan ketika skrip tingkat atas saya keluar.

Terutama jika saya ingin menggunakan set -e, saya berharap proses latar belakang akan mati ketika skrip keluar.

elmarco
sumber

Jawaban:

186

Untuk membersihkan beberapa kekacauan, trapbisa digunakan. Itu dapat memberikan daftar hal yang dieksekusi ketika sinyal tertentu tiba:

trap "echo hello" SIGINT

tetapi juga bisa digunakan untuk mengeksekusi sesuatu jika shell keluar:

trap "killall background" EXIT

Itu adalah builtin, jadi help trapakan memberi Anda informasi (bekerja dengan bash). Jika Anda hanya ingin mematikan pekerjaan latar belakang, Anda bisa melakukannya

trap 'kill $(jobs -p)' EXIT

Berhati-hatilah untuk menggunakan tunggal ', agar cangkang tidak $()segera diganti .

Johannes Schaub - litb
sumber
lalu bagaimana Anda membunuh semua anak saja? (Atau saya kehilangan sesuatu yang jelas)
elmarco
18
killall membunuh anak-anak Anda, tetapi tidak Anda
orip
4
kill $(jobs -p)tidak berfungsi dalam tanda hubung, karena menjalankan penggantian perintah dalam sebuah subshell (lihat Substitusi Perintah di man dash)
user1431317
8
adalah killall backgroundseharusnya menjadi pengganti? backgroundtidak ada di halaman manual ...
Evan Benn
170

Ini berfungsi untuk saya (ditingkatkan terima kasih kepada para komentator):

trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
  • kill -- -$$mengirimkan SIGTERM ke seluruh kelompok proses, sehingga membunuh juga keturunan.

  • Menentukan sinyal EXITberguna saat menggunakan set -e(lebih detail di sini ).

Tokland
sumber
1
Seharusnya bekerja dengan baik secara keseluruhan, tetapi proses anak dapat mengubah kelompok proses. Di sisi lain itu tidak memerlukan kontrol pekerjaan, dan mungkin juga mendapatkan beberapa proses cucu yang terlewatkan oleh solusi lain.
michaeljt
5
Catatan, "kill 0" juga akan membunuh skrip bash induk. Anda mungkin ingin menggunakan "kill - - $ BASHPID" untuk hanya membunuh anak-anak dari skrip saat ini. Jika Anda tidak memiliki $ BASHPID dalam versi bash Anda, Anda dapat mengekspor BASHPID = $ (sh -c 'echo $ PPID')
ACyclic
2
Terima kasih atas solusi yang bagus dan jelas! Sayangnya, itu memisahkan Bash 4.3, yang memungkinkan rekursi perangkap. Saya mengalami ini di 4.3.30(1)-releaseOSX, dan itu juga dikonfirmasi di Ubuntu . Ada wokaround obvoius , :)
skozin
4
Saya tidak begitu mengerti -$$. Ini mengevaluasi ke '- <PID> `misalnya -1234. Di halaman kill kill // builtin manpage tanda hubung utama menentukan sinyal yang akan dikirim. Namun - mungkin memblokir itu, tetapi kemudian dasbor terdepan tidak terdokumentasi sebaliknya. Ada bantuan?
Evan Benn
4
@EvanBenn: Periksa man 2 kill, yang menjelaskan bahwa ketika PID negatif, sinyal dikirim ke semua proses dalam grup proses dengan ID yang disediakan ( en.wikipedia.org/wiki/Process_group ). Ini membingungkan bahwa ini tidak disebutkan dalam man 1 killatau man bash, dan dapat dianggap sebagai bug dalam dokumentasi.
user001
111

Pembaruan: https://stackoverflow.com/a/53714583/302079 meningkatkan ini dengan menambahkan status keluar dan fungsi pembersihan.

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

Mengapa mengonversi INTdan TERMkeluar? Karena keduanya harus memicu kill 0tanpa memasuki loop infinite.

Mengapa memicu kill 0pada EXIT? Karena keluar skrip normal harus dipicukill 0 .

Mengapa kill 0 ? Karena subshells bersarang perlu dibunuh juga. Ini akan mencatat seluruh pohon proses .

korkman
sumber
3
Satu-satunya solusi untuk kasus saya di Debian.
MindlessRanger
3
Baik jawaban oleh Johannes Schaub maupun jawaban yang diberikan oleh tokland tidak berhasil membunuh proses latar belakang skrip shell saya dimulai (pada Debian). Solusi ini berhasil. Saya tidak tahu mengapa jawaban ini tidak lebih dipilih. Bisakah Anda memperluas lebih lanjut tentang apa sebenarnya kill 0artinya / tidak?
josch
7
Ini luar biasa, tetapi juga membunuh shell orangtua saya :-(
vidstige
5
Solusi ini secara harfiah berlebihan. kill 0 (di dalam skrip saya) menghancurkan seluruh sesi X saya! Mungkin dalam beberapa kasus membunuh 0 dapat berguna, tetapi ini tidak mengubah fakta bahwa itu bukan solusi umum dan harus dihindari jika mungkin kecuali ada alasan yang sangat baik untuk menggunakannya. Akan lebih baik untuk menambahkan peringatan bahwa itu dapat membunuh shell induk atau bahkan seluruh sesi X, bukan hanya pekerjaan latar belakang skrip!
Lissanro Rayen
3
Walaupun ini mungkin solusi yang menarik dalam beberapa keadaan, seperti yang ditunjukkan oleh @vidstige ini akan membunuh seluruh kelompok proses yang mencakup proses peluncuran (yaitu shell induk dalam kebanyakan kasus). Jelas bukan sesuatu yang Anda inginkan ketika Anda menjalankan skrip melalui IDE.
matpen
21

trap 'kill $ (jobs -p)' EXIT

Saya hanya akan membuat sedikit perubahan pada jawaban Johannes dan menggunakan jobs -pr untuk membatasi kill pada proses yang sedang berjalan dan menambahkan beberapa sinyal lagi ke daftar:

trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
raytraced
sumber
Mengapa tidak membunuh pekerjaan yang dihentikan juga? Dalam Bash EXIT, perangkap akan dijalankan jika SIGINT dan SIGTERM juga, sehingga perangkap akan dipanggil dua kali jika sinyal tersebut.
jarno
14

The trap 'kill 0' SIGINT SIGTERM EXITsolusi yang dijelaskan dalam jawaban @ tokland ini benar-benar baik, tapi terbaru Bash crash dengan kesalahan segmantation ketika menggunakannya. Itu karena Bash, mulai dari ay 4.3, memungkinkan rekursi perangkap, yang menjadi tak terbatas dalam kasus ini:

  1. proses shell menerima SIGINTatau SIGTERMatau EXIT;
  2. sinyal terjebak, mengeksekusi kill 0, yang mengirim SIGTERMke semua proses dalam grup, termasuk shell itu sendiri;
  3. pergi ke 1 :)

Ini dapat diatasi dengan secara manual membatalkan pendaftaran perangkap:

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

Cara yang lebih mewah, yang memungkinkan untuk mencetak sinyal yang diterima dan menghindari pesan "Terminated:":

#!/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 : menambahkan contoh minimal; stopfungsi ditingkatkan untuk aviod menghilangkan sinyal yang tidak perlu dan untuk menyembunyikan "Dihentikan:" pesan dari output. Trevor Boyd Smith terima kasih untuk sarannya!

skozin
sumber
di stop()Anda memberikan argumen pertama sebagai nomor sinyal, tetapi kemudian Anda hardcode sinyal apa yang sedang tidak terdaftar. alih-alih hardcode, deregistrasi sinyal Anda bisa menggunakan argumen pertama untuk deregister dalam stop()fungsi (melakukan hal itu berpotensi menghentikan sinyal rekursif lainnya (selain 3 hardcode)).
Trevor Boyd Smith
@ TrevorBoydSmith, ini tidak akan berfungsi seperti yang diharapkan, saya kira. Misalnya, shell mungkin terbunuh dengan SIGINT, tetapi kill 0mengirimkan SIGTERM, yang akan terjebak sekali lagi. Ini tidak akan menghasilkan rekursi yang tak terbatas, karena SIGTERMakan terperangkap selama stoppanggilan kedua .
skozin
Mungkin, trap - $1 && kill -s $1 0harus bekerja lebih baik. Saya akan menguji dan memperbarui jawaban ini. Terima kasih atas ide bagusnya! :)
skozin
Tidak, tidak trap - $1 && kill -s $1 0akan berfungsi juga, karena kita tidak bisa membunuh EXIT. Tetapi cukup memadai melakukan de-trap TERM, karena killmengirimkan sinyal ini secara default.
skozin
Saya menguji rekursi dengan EXIT, trappengendali sinyal selalu hanya dieksekusi sekali.
Trevor Boyd Smith
9

Untuk berada di sisi aman saya merasa lebih baik untuk mendefinisikan fungsi pembersihan dan memanggilnya dari perangkap:

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

atau menghindari fungsi sama sekali:

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

Mengapa? Karena dengan hanya menggunakan trap 'kill $(jobs -pr)' [...]satu mengasumsikan bahwa akan ada pekerjaan latar belakang berjalan ketika kondisi perangkap diisyaratkan. Ketika tidak ada pekerjaan, seseorang akan melihat pesan berikut (atau serupa):

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

karena jobs -prkosong - saya mengakhiri 'jebakan' itu (pun intended).

tdaitx
sumber
Kasing uji ini [ -n "$(jobs -pr)" ]tidak berfungsi pada bash saya. Saya menggunakan GNU bash, versi 4.2.46 (2) -release (x86_64-redhat-linux-gnu). Pesan "kill: use" terus bermunculan.
Douwe van der Leest
Saya menduga itu ada hubungannya dengan fakta yang jobs -prtidak mengembalikan PID anak-anak dari proses latar belakang. Itu tidak merobohkan seluruh pohon proses, hanya memotong akar.
Douwe van der Leest
2

Versi bagus yang bekerja di Linux, BSD dan MacOS X. Pertama mencoba mengirim SIGTERM, dan jika tidak berhasil, bunuh proses setelah 10 detik.

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

Harap dicatat bahwa pekerjaan tidak termasuk proses anak cucu.

Orsiris de Jong
sumber
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

Seperti https://stackoverflow.com/a/22644006/10082476 , tetapi dengan kode keluar yang ditambahkan

Delaware
sumber
Mana exit_codedatang dari dalam INT TERMperangkap?
jarno
1

Opsi lain adalah membuat skrip mengatur dirinya sebagai pemimpin grup proses, dan menjebak killpg pada grup proses Anda saat keluar.

orip
sumber
Bagaimana Anda mengatur proses sebagai pemimpin grup proses? Apa itu "killpg"?
jarno
0

Jadi naskah memuat skrip. Jalankan perintah killall(atau apa pun yang tersedia di OS Anda) yang dijalankan segera setelah skrip selesai.

Oli
sumber
0

jobs -p tidak bekerja di semua shell jika dipanggil dalam sub-shell, mungkin kecuali outputnya dialihkan ke file tetapi bukan pipa. (Saya menganggap itu awalnya ditujukan hanya untuk penggunaan interaktif.)

Bagaimana dengan yang berikut ini:

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

Panggilan ke "pekerjaan" diperlukan dengan shell dasbor Debian, yang gagal memperbarui pekerjaan saat ini ("%%") jika tidak ada.

michaeljt
sumber
Hmm pendekatan yang menarik, tetapi sepertinya tidak berhasil. Pertimbangkan 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; doneJika Anda menjalankannya di Bash (5.0.3) dan mencoba untuk mengakhiri, tampaknya ada loop tak terbatas. Namun, jika Anda menghentikannya lagi, itu berfungsi. Bahkan dengan Dash (0.5.10.2-6) Anda harus menghentikannya dua kali.
jarno
0

Saya membuat adaptasi jawaban @ tokland yang dikombinasikan dengan pengetahuan dari http://veithen.github.io/2014/11/16/sigterm-propagation.html ketika saya melihat bahwa trapitu tidak memicu jika saya menjalankan proses foreground (tidak dilatar belakangi dengan &):

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

Contohnya berfungsi:

$ 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
sumber
0

Hanya untuk keragaman, saya akan memposting variasi https://stackoverflow.com/a/2173421/102484 , karena solusi itu mengarah ke pesan "Dihentikan" di lingkungan saya:

trap 'test -z "$intrap" && export intrap=1 && kill -- -$$' SIGINT SIGTERM EXIT
noonex
sumber