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

Viewed 136172

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.

14 Answers

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

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.

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

I finally have found a solution that appears to work in all cases to kill all descents recursively regardless of whether they are jobs, or sub-processes. The other solutions here all seemed to fail with things such as:

while ! ffmpeg ....
do
  sleep 1
done

In my situation, ffmpeg would keep running after the parent script exited.

I found a solution here to recursively getting the PIDs of all child processes recursively and used that in the trap handler thus:

cleanup() {
    # kill all processes whose parent is this process
    kill $(pidtree $$ | tac)
}

pidtree() (
    [ -n "$ZSH_VERSION"  ] && setopt shwordsplit
    declare -A CHILDS
    while read P PP;do
        CHILDS[$PP]+=" $P"
    done < <(ps -e -o pid= -o ppid=)
    walk() {
        echo $1
        for i in ${CHILDS[$1]};do
            walk $i
        done
    }

    for i in "$@";do
        walk $i
    done
)

trap cleanup EXIT

The above placed at the start of a bash script succeeds in killing all child processes. Note that pidtree is called with $$ which is the PID of the bash script that is exiting and the list of PIDs (one per line) is reversed using tac to try and ensure that prarent processes are killed only after their children to avoid possible race conditions in loops such as the example I gave.

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.

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.

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
Related