Bash – Prevent a shell fork from living longer than its initiator

background-processbashconcurrencyforkjobs

If I have a Bash script like:

function repeat {
    while :; do
        echo repeating; sleep 1
    done
}
repeat &
echo running once

running once is printed once but repeat's fork lives forever, printing endlessly.

How should I prevent repeat from continuing to run after the script which created it has exited?

I thought maybe explicitly instantiating a new bash -c interpreter would force it to exit as its parent has disappeared, but I guess orphaned processes are adopted by init or PID 1.

Testing this using another file:

# repeat.bash
while :; do echo repeating; sleep 1; done

# fork.bash
bash -c "./repeat.bash & echo an exiting command"

Running ./fork.bash still causes repeat.bash to continue to run in the background forever.

The simple and lazy solution is to add the line to fork.bash:

pkill repeat.bash

But you had better not have another important process with that name, or it will also be obliterated.

  1. I wonder, if there is a better or accepted way to handle background jobs in forked shells that should exit when the script (or process) that created them has exited?

  2. If there is no better way than blindly pkilling all processes with the same name, how should a repeating job that runs alongside something like a webserver be handled to exit? I want to avoid a cron job because the script is in a git repository, and the code should be self-contained without changing system files in /etc/.

Best Answer

This kills the background process before the script exits:

trap '[ "$pid" ] && kill "$pid"' EXIT

function repeat {
    while :; do
        echo repeating; sleep 1
    done
}
repeat &
pid=$!
echo running once

How it works

  • trap '[ "$pid" ] && kill "$pid"' EXIT

    This creates a trap. Whenever the script is about to exit, the commands in single-quotes will be run. That command checks to see if the shell variable pid has been assigned a non-empty value. If it has, then the process associated with pid is killed.

  • pid=$!

    This saves the process id of the preceding background command (repeat &) in the shell variable pid.

Improvement

As Patrick points out in the comments, there is a chance that the script could be killed after the background process starts but before the pid variable is set. We can handle that case with this code:

my_exit() {
    [ "$racing" ] && pid=$!
    [ "$pid" ] && kill "$pid"
}
trap my_exit EXIT

function repeat {
    while :; do
        echo repeating; sleep 1
    done
}

racing=Y
repeat &
pid=$!
racing=

echo running once
Related Question