1. Overview

In this tutorial, we’ll cover various methods for killing child processes after a certain timeout in Bash. This functionality is desirable for many use-cases, such as restarting misbehaving processes that crash after some time.

2. Child Processes in Bash

As we’re already aware, Bash has some built-in commands like echo and printf that don’t require extra processes. However, all external processes launched in Bash, such as curl, are child processes of the shell.

Child processes inherit all environment variables from the parent shell:

$ export PARENT_VARIABLE="Hello"
$ bash -c 'echo Parent variable: $PARENT_VARIABLE'
Parent variable: Hello

Here, we launched Bash as a child process and accessed the parent shell’s environment variables.

Additionally, we can spawn multiple child processes using the & operator in Bash. This can be useful for running daemons or for parallelizing tasks. We can access the child process’s PID from the $! environment variable:

$ sleep 5 &
$ echo $! # Check PID
18695

However, if we just spawn child processes in a Bash script, the script might exit before the child process has finished. We can use the wait command to wait for a child process to exit:

$ sleep 5 &
$ wait; echo Slept
Slept
[1]+  Done                       sleep 5

3. Killing a Child Process After a Timeout

We might want to kill a child process after a given timeout for a variety of reasons, such as restarting misbehaving programs. Let’s cover the various commands we can use for this purpose.

3.1. Using the timeout Command

For killing a child process after a given timeout, we can use the timeout command. It runs the command passed to it and kills it with the SIGTERM signal after the given timeout. In case we want to send a different signal like SIGINT to the process, we can use the –signal flag.

Let’s write a script that prints a message after each second as an example:

$ cat ./script 
counter=0

echo "Starting sleep"

while sleep 1; do
    : $((counter+=1)) # Increment counter
    echo "Slept $counter time(s)"
done
$ timeout 5 ./script 
Starting sleep
Slept 1 time(s)
Slept 2 time(s)
Slept 3 time(s)
Slept 4 time(s)
Terminated

We can see that the process is terminated after printing 5 lines, as we passed a timeout value of 5 seconds.

3.2. Using Pure Bash Features

The timeout command is an external command that isn’t built into Bash. We can create a pure Bash implementation of timeout using the procfs filesystem:

# First argument: PID
# Second argument: Timeout
my_timeout() {
    # Get process start time (Field 22) to check for PID recycling
    start_time="$(cut -d ' ' -f 22 /proc/$1/stat)"

    sleep "$2"

    # Make sure that the PID was not reused by another process
    # that started at a later time
    if [ "$(cut -d ' ' -f 22 /proc/$1/stat)" = "$start_time" ]; then
        # Kill process with SIGTERM
        kill -15 "$1"
    fi
}

First, we use the cut command to get the process’s start time from the 22nd field in the /proc/PID/stat. Then, we sleep for the specified timeout and kill the process with the SIGTERM signal. Additionally, we ensure that the start time of the process has not changed before killing it. This prevents us from killing a new process that reused the original process’s PID. This might happen if the original process unexpectedly dies before the specified timeout and a new process reuses its PID.

Let’s test it with a timeout of 2 seconds:

$ ./script & my_timeout $!2
Starting sleep
Slept 1 time(s)
$

4. Conclusion

In this article, we learned about spawning child processes in Bash and killing them after a given timeout with the help of the timeout command. Additionally, we created a pure Bash implementation of timeout to learn more about its workings.