Introduction

Sometimes, it can be necessary to execute commands in background inside a bash scripts. Following examples will allow you to understand how this works and avoid many traps.

Reminder

In interactive mode (console), you only need to put a & at the end of a command to execute it in the background:

$ sleep 1 &
[1] 12432
$

Inside a script, we will use the same principle. Take care of /dev/stdout and /dev/stderr channels as they are inherited to children process. The output will be less readable if they are not correctly redirected. /dev/stdin is set to /dev/null. Your command executed in background can not obtain any data from this channel.

Target

The two programs target is to demonstrate the possibility to execute commands in the background. Few sleep commands will be run in the background and then the program will wait for them to finish before exiting, so it can control their correct execution.

Example 1 - nasty mode

The script for this example is available here:

The PID of every child process is appended to the string $pid thanks to the special variable $!. This will be used to verify that the process has exited.

The program enters an infinite loop

while true; do 

If the string $pid is not empty, meaning that they are still child process running, then they are all tested with the command kill -0:

if [ -n "$pids" ] ; then
    # check each pid one by one
    for pid in $pids; do
        echo "Checking the $pid"
        # try to send a signal to the child, if child has exited, 
        #remove its pid from pids string
        kill -0 "$pid" 2>/dev/null || pids=$(echo $pids | sed "s/\b$pid\s*//")
    done
else

If the kill command return an error, as the process is not alive, then the PID is removed from the string $pid:

pids=$(echo $pids | sed "s/\b$pid\s*//")

On the other side, if the string $pid is empty, it means that all child process have exited:

        echo "All your process completed"
        break
    fi
done

The program quits the infinite loop with the break.

Pro
  • simple and efficient
Cons
  • no processing of child return code
  • excessive CPU usage in the infinite loop.

Example 2 - scanner

The script for this example is available here:

This script uses the same string $pid as example 1. It still contains all child PID. The program creates a bash function waitall() which takes all child PID as parameter. We found the same infinite loop:

while :; do

Every PID are tested with the kill -0:

for pid in "$@"; do
    shift
    # test if still present
    if kill -0 "$pid" 2>/dev/null; then
        # still present, remove the pid from pid list
        debug "$pid is still alive."
        set -- "$@" "$pid"

The program enters in the if if the child is still alive, otherwise, we use the bash command wait to get the child return code:

    elif wait "$pid"; then
        debug "$pid exited with zero exit status."
    else
        debug "$pid exited with non-zero exit status."
        ((++errors))
    fi

If the child process exited with a 0, the program enters the elif. If it exited with something else, then we count the number of child that exited in error with the variable $errors.

We force the program to wait for 1 second before next iteration:

sleep ${WAITALL_DELAY:-1}

This allow to "ease" the CPU. It is possible to change this time with the variable $WAITALL_DELAY.

If no child process are alive, we exit the infinite loop:

(("$#" > 0)) || break

Finally, the function exits with return code 0 if no child process was in error:

((errors == 0))
Pro
  • Simple to use
Cons
  • Infinite loop without timeout handling
  • need to do a sleep 1 for every iteration

Next to come

In the next article, we will see how to avoid sleep 1 by using bash interuption (trap). We will also see that it is possible to handle timeout for each child process.