How do I kill background processes / jobs when my shell script exits?

Shell

Shell Problem Overview


I am looking for a way to clean up the mess when my top-level script exits.

Especially if I want to use set -e, I wish the background process would die when the script exits.

Shell Solutions


Solution 1 - Shell

To clean up some mess, trap can be used. It can provide a list of stuff executed when a specific signal arrives:

trap "echo hello" SIGINT

but can also be used to execute something if the shell exits:

trap "killall background" EXIT

It's a builtin, so help trap will give you information (works with bash). If you only want to kill background jobs, you can do

trap 'kill $(jobs -p)' EXIT

Watch out to use single ', to prevent the shell from substituting the $() immediately.

Solution 2 - Shell

This works for me (improved thanks to the commenters):

trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
  • kill -- -$$ sends a SIGTERM to the whole process group, thus killing also descendants.

  • Specifying signal EXIT is useful when using set -e (more details here).

Solution 3 - Shell

Update: https://stackoverflow.com/a/53714583/302079 improves this by adding exit status and a cleanup function.

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

Why convert INT and TERM to exit? Because both should trigger the kill 0 without entering an infinite loop.

Why trigger kill 0 on EXIT? Because normal script exits should trigger kill 0, too.

Why kill 0? Because nested subshells need to be killed as well. This will take down the whole process tree.

Solution 4 - Shell

> trap 'kill $(jobs -p)' EXIT

I would make only minor changes to Johannes' answer and use jobs -pr to limit the kill to running processes and add a few more signals to the list:

trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT

Solution 5 - Shell

The trap 'kill 0' SIGINT SIGTERM EXIT solution described in @tokland's answer is really nice, but latest Bash crashes with a segmentation fault when using it. That's because Bash, starting from v. 4.3, allows trap recursion, which becomes infinite in this case:

  1. shell process receives SIGINT or SIGTERM or EXIT;
  2. the signal gets trapped, executing kill 0, which sends SIGTERM to all processes in the group, including the shell itself;
  3. go to 1 :)

This can be worked around by manually de-registering the trap:

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

The more fancy way that allows printing the received signal and avoids "Terminated:" messages:

#!/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' "received $1, killing child processes"
  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: added a minimal example; improved stop function to avoid de-trapping unnecessary signals and to hide "Terminated:" messages from the output. Thanks Trevor Boyd Smith for the suggestions!

Solution 6 - Shell

To be on the safe side I find it better to define a cleanup function and call it from trap:

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

or avoiding the function altogether:

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

Why? Because by simply using trap 'kill $(jobs -pr)' [...] one assumes that there will be background jobs running when the trap condition is signalled. When there are no jobs one will see the following (or similar) message:

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

because jobs -pr is empty - I ended in that 'trap' (pun intended).

Solution 7 - Shell

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

Like https://stackoverflow.com/a/22644006/10082476, but with added exit-code

Solution 8 - Shell

A nice version that works under Linux, BSD and MacOS X. First tries to send SIGTERM, and if it doesn't succeed, kills the process after 10 seconds.

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

Please note that jobs does not include grand children processes.

Solution 9 - Shell

I made an adaption of @tokland's answer combined with the knowledge from http://veithen.github.io/2014/11/16/sigterm-propagation.html when I noticed that trap doesn't trigger if I'm running a foreground process (not backgrounded with &):

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

Example of it working:

$ 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

Solution 10 - Shell

So script the loading of the script. Run a killall (or whatever is available on your OS) command that executes as soon as the script is finished.

Solution 11 - Shell

Another option is it to have the script set itself as the process group leader, and trap a killpg on your process group on exit.

Solution 12 - Shell

jobs -p does not work in all shells if called in a sub-shell, possibly unless its output is redirected into a file but not a pipe. (I assume it was originally intended for interactive use only.)

What about the following:

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

The call to "jobs" is needed with Debian's dash shell, which fails to update the current job ("%%") if it is missing.

Solution 13 - Shell

Just for diversity I will post variation of https://stackoverflow.com/a/2173421/102484 , because that solution leads to message "Terminated" in my environment:

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionelmarcoView Question on Stackoverflow
Solution 1 - ShellJohannes Schaub - litbView Answer on Stackoverflow
Solution 2 - ShelltoklandView Answer on Stackoverflow
Solution 3 - ShellkorkmanView Answer on Stackoverflow
Solution 4 - ShellraytracedView Answer on Stackoverflow
Solution 5 - ShellskozinView Answer on Stackoverflow
Solution 6 - ShelltdaitxView Answer on Stackoverflow
Solution 7 - ShellDelawareView Answer on Stackoverflow
Solution 8 - ShellOrsiris de JongView Answer on Stackoverflow
Solution 9 - Shellnh2View Answer on Stackoverflow
Solution 10 - ShellOliView Answer on Stackoverflow
Solution 11 - ShelloripView Answer on Stackoverflow
Solution 12 - ShellmichaeljtView Answer on Stackoverflow
Solution 13 - ShellnoonexView Answer on Stackoverflow