Bash Process Management and Job Control with Background Backups

Background backup process transferring data between servers and secure storage while a terminal remains available

What You’ll Learn

After completing this lesson, you will know how to start long-running Bash backup tasks without blocking your terminal, inspect their status, bring them back to the foreground, and stop or wait for them safely.

  • Run a command in the background with &.
  • Use $!, jobs, and ps to identify and monitor processes.
  • Pause, resume, and stop jobs with Ctrl+Z, bg, fg, and kill.
  • Use wait to collect a backup process’s exit status.

The Concept

Bash normally runs one foreground command at a time. While that command is running, the shell waits for it to finish before displaying another prompt. This is appropriate for quick commands, but a backup, archive, or database export may take several minutes or hours.

Appending & starts a command as a background job and immediately returns control of the terminal:

tar -czf nightly-backup.tar.gz project/ &

Bash assigns the job a job number such as [1] and the operating system assigns it a process ID, or PID. The special variable $! contains the PID of the most recently started background process.

These identifiers serve different purposes:

  • jobs lists jobs managed by the current interactive shell.
  • fg and bg use job specifications such as %1.
  • ps, kill, and wait can use a PID.

For a reliable script, save the PID immediately after starting a process. A later command may change the value of $!. Use wait "$pid" when the script must not continue until the backup finishes.

Basic Example

The following demonstration creates a small source directory, starts a delayed archive in the background, checks its status, and waits for completion. The delay makes the job easy to observe; a real tar backup may run for much longer.

#!/usr/bin/env bash

source_dir="/tmp/dcg-backup-source"
archive_path="/tmp/dcg-backup.tar.gz"

mkdir -p "$source_dir"
printf 'Application configuration backup\n' > "$source_dir/config.txt"

backup_directory() {
    local source="$1"
    local destination="$2"

    printf 'Backup started: %s\n' "$destination"
    sleep 3
    tar -czf "$destination" "$source"
    printf 'Backup finished: %s\n' "$destination"
}

backup_directory "$source_dir" "$archive_path" &
backup_pid=$!

printf 'Started background backup with PID %s\n' "$backup_pid"
jobs -l
ps -p "$backup_pid" -o pid=,stat=,cmd=

wait "$backup_pid"
backup_status=$?

if (( backup_status == 0 )); then
    printf 'Backup completed successfully.\n'
else
    printf 'Backup failed with status %s.\n' "$backup_status"
fi

Expected Output

The PID, job number, and process state vary between runs. The backup may finish before jobs or ps displays it, especially on a fast system. A typical run looks similar to this:

Started background backup with PID 28417
[1]+  28417 Running                 backup_directory "$source_dir" "$archive_path" &
  28417 S    bash
Backup started: /tmp/dcg-backup.tar.gz
Backup finished: /tmp/dcg-backup.tar.gz
Backup completed successfully.

How the Code Works

State diagram showing a backup beginning in the foreground, moving to background execution with ampersand, and being monitored with jobs or ps. From monitoring, the job can return to the foreground with fg, be stopped with kill, or continue until it exits and its status is collected with wait. A stopped foreground job can resume in the background with bg or return to the foreground with fg.
Bash background jobs can be monitored, resumed, stopped, and safely completed by collecting their exit status with wait.

The function receives a source directory and an archive path as arguments. Quoting both variables prevents spaces or wildcard characters in their values from being interpreted incorrectly.

The & after the function call is important. It backgrounds the complete function invocation, not just one command inside the function. The next line stores $! in backup_pid before another background process can replace that value.

jobs -l reports jobs known to the current shell. Its job number, %1 in this example, is useful for interactive commands such as fg %1. The ps command asks the operating system for details about the saved PID, including its state and command.

wait "$backup_pid" blocks until that specific process exits. Its exit status becomes the status of wait, so the script saves it in backup_status and tests it. This is safer than merely assuming that starting the process means the backup succeeded.

In an interactive shell, you can suspend a foreground command with Ctrl+Z. Bash marks it as stopped rather than terminated. Use bg %1 to resume it in the background or fg %1 to resume it in the foreground. To request termination, use kill "$backup_pid". A normal kill sends SIGTERM, giving a well-behaved process an opportunity to clean up.

Another Example

A scheduled backup may need to archive two independent locations. Starting both tasks first can reduce total elapsed time, while waiting for each saved PID lets the script report failures individually.

#!/usr/bin/env bash

backup_root="/tmp/dcg-multi-backup"
mkdir -p "$backup_root/site-a" "$backup_root/site-b"

printf 'Site A database export\n' > "$backup_root/site-a/database.sql"
printf 'Site B uploaded media\n' > "$backup_root/site-b/media.txt"

make_archive() {
    local label="$1"
    local source="$2"
    local destination="$3"
    local log_file="$4"

    {
        printf '%s backup started\n' "$label"
        sleep 2
        tar -czf "$destination" "$source"
        printf '%s backup finished\n' "$label"
    } > "$log_file" 2>&1
}

make_archive "site-a" "$backup_root/site-a" "$backup_root/site-a.tar.gz" "$backup_root/site-a.log" &
site_a_pid=$!

make_archive "site-b" "$backup_root/site-b" "$backup_root/site-b.tar.gz" "$backup_root/site-b.log" &
site_b_pid=$!

printf 'Started site-a with PID %s\n' "$site_a_pid"
printf 'Started site-b with PID %s\n' "$site_b_pid"
ps -p "$site_a_pid,$site_b_pid" -o pid=,stat=,cmd=

wait "$site_a_pid"
site_a_status=$?

wait "$site_b_pid"
site_b_status=$?

printf '\nBackup results:\n'
printf 'site-a: status %s, log %s\n' "$site_a_status" "$backup_root/site-a.log"
printf 'site-b: status %s, log %s\n' "$site_b_status" "$backup_root/site-b.log"

Each function call runs independently in the background and has its own PID and log file. The script waits for both jobs before reporting results. Redirecting each task’s standard output and standard error to a separate log prevents interleaved messages from making the terminal output difficult to read.

Common Mistakes

  • Forgetting the ampersand: Without &, the shell waits for the backup and no background job is created.
  • Not saving $! immediately: Start the process and assign its PID on adjacent lines. Starting another background command first changes which PID $! refers to.
  • Confusing job numbers and PIDs: %1 is a Bash job specification, while 28417 is an operating-system PID. They are not interchangeable in every command.
  • Closing the terminal unexpectedly: Jobs attached to an interactive shell may receive a hangup signal when the shell exits. For dependable unattended backups, use a scheduler or a service designed for long-running tasks rather than relying only on an interactive shell.
  • Ignoring the exit status: A process can start successfully and still fail later because of permissions, missing files, or insufficient disk space. Use wait and inspect the resulting status.
  • Using kill -9 immediately: SIGKILL cannot be handled, so cleanup code cannot run. Try the default kill first and escalate only when a process does not stop.

Try It Yourself

Start two different short-running background tasks using sleep. Save each PID, inspect them with jobs -l or ps, and use wait for both processes. Record each exit status and confirm that the shell prompt remains available while they run.

Challenge

Write a Bash script that launches two backup tasks in parallel:

  • Archive /etc/hosts as /tmp/hosts-backup.tar.gz.
  • Archive /etc/services as /tmp/services-backup.tar.gz.
  • Save each task’s PID immediately after starting it.
  • Write each task’s messages and errors to its own log file in /tmp.
  • Wait for both tasks and print whether each one succeeded based on its exit status.

Use a function so the two tasks share the same backup logic, but do not make the second task wait for the first to finish.

Solution

#!/usr/bin/env bash

backup_file() {
    local label="$1"
    local source_file="$2"
    local archive_file="$3"
    local log_file="$4"

    {
        printf '%s backup started\n' "$label"
        tar -czf "$archive_file" "$source_file"
        printf '%s backup finished\n' "$label"
    } > "$log_file" 2>&1
}

backup_file "hosts" "/etc/hosts" "/tmp/hosts-backup.tar.gz" "/tmp/hosts-backup.log" &
hosts_pid=$!

backup_file "services" "/etc/services" "/tmp/services-backup.tar.gz" "/tmp/services-backup.log" &
services_pid=$!

printf 'Started hosts backup with PID %s\n' "$hosts_pid"
printf 'Started services backup with PID %s\n' "$services_pid"

wait "$hosts_pid"
hosts_status=$?

wait "$services_pid"
services_status=$?

if (( hosts_status == 0 )); then
    printf 'hosts backup succeeded\n'
else
    printf 'hosts backup failed with status %s\n' "$hosts_status"
fi

if (( services_status == 0 )); then
    printf 'services backup succeeded\n'
else
    printf 'services backup failed with status %s\n' "$services_status"
fi

The two function calls are started before either wait command, so both archives can run concurrently. Each PID is saved immediately, and each task has separate output and error logs. The script checks the status returned by each wait, allowing one backup to succeed while the other is reported as failed.

Key Takeaways

  • Append & to run a Bash command in the background.
  • Save $! immediately when you need to monitor or wait for a background process.
  • Use jobs for shell-managed jobs and ps for broader process inspection.
  • Use fg, bg, and kill to control interactive tasks.
  • Use wait to safely detect whether a long-running backup succeeded.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top