⚙️ Bash Process & Job Control

Comprehensive Reference Guide — Background jobs, signals, traps, process management, and parallel execution

📋 Table of Contents

Overview

Bash provides comprehensive process and job control for running commands in the background, managing multiple jobs, handling signals, and ensuring clean script termination. These capabilities are essential for writing robust scripts that handle long-running tasks, parallel execution, and graceful shutdown.

Key Capabilities

Background Jobs: Run processes asynchronously with & and manage with jobs, fg, bg

Process IDs: Track processes with $$, $!, $BASHPID

Signals & Traps: Catch and handle signals for cleanup and control

Wait & Synchronization: Coordinate multiple background processes

Process Variables

VariableDescriptionExample
$$ Current shell's PID echo "Script PID: $$"
$! PID of last background job cmd &; pid=$!
$BASHPID PID of current Bash process (subshell-aware) echo $BASHPID
$PPID Parent process PID echo "Parent: $PPID"
$? Exit status of last command cmd; echo $?
${PIPESTATUS[@]} Exit statuses of pipeline commands cmd1 | cmd2; echo ${PIPESTATUS[0]}
⚠️ $$ vs $BASHPID
$$ is the PID of the main shell and doesn't change in subshells. $BASHPID gives the actual PID of the current Bash process, including subshells.

Common Signals

SignalNumberDescriptionDefault Action
SIGHUP 1 Hangup (terminal closed) Terminate
SIGINT 2 Interrupt (Ctrl+C) Terminate
SIGQUIT 3 Quit (Ctrl+\) Core dump
SIGKILL 9 Kill (cannot be caught) Terminate
SIGTERM 15 Terminate (default kill) Terminate
SIGSTOP 19 Stop (cannot be caught) Stop process
SIGTSTP 20 Terminal stop (Ctrl+Z) Stop process
SIGCONT 18 Continue if stopped Continue
SIGCHLD 17 Child process terminated Ignore
SIGUSR1 10 User-defined signal 1 Terminate
SIGUSR2 12 User-defined signal 2 Terminate

Trap Pseudo-Signals

SignalDescription
EXIT Script exit (any reason)
ERR Command returns non-zero (with set -e)
DEBUG Before each command
RETURN Function or sourced script returns

Detailed Examples

1
Background Jobs and Process IDs

Running commands in the background and tracking their PIDs.

#!/bin/bash
# Background jobs and process IDs

echo "=== Process ID Variables ==="
echo "Script PID ($$):      $$"
echo "Parent PID (PPID):    $PPID"
echo "Bash PID (BASHPID):   $BASHPID"

echo ""
echo "=== $$ vs \$BASHPID in Subshells ==="
echo "Main shell: $$ / $BASHPID"
(echo "Subshell:   $$ / $BASHPID")

echo ""
echo "=== Run Background Job ==="

# Start a background job
sleep 5 &
bg_pid=$!
echo "Started background sleep, PID: $bg_pid"

# Check if running
if kill -0 $bg_pid 2>/dev/null; then
    echo "Process $bg_pid is running"
fi

# Kill it
kill $bg_pid
echo "Killed process $bg_pid"

echo ""
echo "=== Multiple Background Jobs ==="

# Start multiple jobs
pids=()

for i in {1..3}; do
    sleep $((i * 2)) &
    pids+=($!)
    echo "Started job $i with PID $!"
done

echo ""
echo "All PIDs: ${pids[*]}"

# List jobs
echo ""
echo "Jobs:"
jobs -l

# Clean up
for pid in "${pids[@]}"; do
    kill $pid 2>/dev/null
done

echo ""
echo "=== Check Exit Status ==="

# Run command and check
ls /nonexistent 2>/dev/null
echo "ls exit status: $?"

(exit 42)
echo "Subshell exit status: $?"

echo ""
echo "=== Pipeline Exit Statuses ==="

# PIPESTATUS array
false | true | false
echo "Pipeline statuses: ${PIPESTATUS[*]}"
echo "  First:  ${PIPESTATUS[0]}"
echo "  Second: ${PIPESTATUS[1]}"
echo "  Third:  ${PIPESTATUS[2]}"
Output: === Process ID Variables === Script PID ($$): 12345 Parent PID (PPID): 12340 Bash PID (BASHPID): 12345 === $$ vs $BASHPID in Subshells === Main shell: 12345 / 12345 Subshell: 12345 / 12350 === Run Background Job === Started background sleep, PID: 12351 Process 12351 is running Killed process 12351 === Pipeline Exit Statuses === Pipeline statuses: 1 0 1 First: 1 Second: 0 Third: 1
2
Wait for Background Processes

Using wait to synchronize with background jobs.

#!/bin/bash
# Wait for background processes

echo "=== Basic Wait ==="

# Start background job
sleep 2 &
pid=$!
echo "Started sleep (PID: $pid), waiting..."

wait $pid
status=$?
echo "Process $pid finished with status: $status"

echo ""
echo "=== Wait for Multiple Jobs ==="

# Start multiple jobs
pids=()

echo "Starting 3 jobs..."
for i in 1 2 3; do
    (sleep $i; echo "Job $i done" >&2; exit $i) &
    pids+=($!)
done

echo "PIDs: ${pids[*]}"
echo "Waiting for all to complete..."

# Wait for all
wait
echo "All jobs complete"

echo ""
echo "=== Wait for Specific Jobs ==="

# Start jobs with different durations
(sleep 3; exit 0) &
pid1=$!

(sleep 1; exit 0) &
pid2=$!

(sleep 2; exit 0) &
pid3=$!

echo "Started PIDs: $pid1, $pid2, $pid3"

# Wait for each and get status
wait $pid2
echo "PID $pid2 (1s) finished: $?"

wait $pid3
echo "PID $pid3 (2s) finished: $?"

wait $pid1
echo "PID $pid1 (3s) finished: $?"

echo ""
echo "=== Wait with Timeout Pattern ==="

wait_with_timeout() {
    local pid=$1
    local timeout=$2
    local elapsed=0
    
    while kill -0 $pid 2>/dev/null; do
        if ((elapsed >= timeout)); then
            echo "Timeout after ${timeout}s, killing $pid"
            kill $pid
            return 1
        fi
        sleep 1
        ((elapsed++))
    done
    
    wait $pid
    return $?
}

# Test timeout
sleep 10 &
pid=$!
echo "Started long job (PID: $pid)"

wait_with_timeout $pid 3
echo "Result: $?"

echo ""
echo "=== Collect All Exit Statuses ==="

declare -A results

for i in {1..3}; do
    (sleep $((RANDOM % 3)); exit $((RANDOM % 2))) &
    results[$!]="pending"
done

echo "Started jobs: ${!results[*]}"

for pid in "${!results[@]}"; do
    wait $pid
    results[$pid]=$?
done

echo "Results:"
for pid in "${!results[@]}"; do
    echo "  PID $pid: exit ${results[$pid]}"
done
Output: === Basic Wait === Started sleep (PID: 12360), waiting... Process 12360 finished with status: 0 === Wait for Multiple Jobs === Starting 3 jobs... PIDs: 12361 12362 12363 Waiting for all to complete... Job 1 done Job 2 done Job 3 done All jobs complete === Wait with Timeout Pattern === Started long job (PID: 12367) Timeout after 3s, killing 12367 Result: 1
3
Signal Handling with trap

Catching signals and performing cleanup on exit.

#!/bin/bash
# Signal handling with trap

echo "=== Basic EXIT Trap ==="

cleanup() {
    echo "Cleanup: removing temp files..."
    rm -f /tmp/myscript_*.tmp
}

# Set trap for EXIT (runs on any exit)
trap cleanup EXIT

# Create temp file
tmpfile="/tmp/myscript_$$.tmp"
echo "data" > "$tmpfile"
echo "Created: $tmpfile"

echo "Script continues..."
# cleanup runs automatically when script exits

echo ""
echo "=== Trap Multiple Signals ==="

# Handler function
handle_signal() {
    local signal=$1
    echo ""
    echo "Caught signal: $signal"
    exit 1
}

# Trap multiple signals
trap 'handle_signal SIGINT' SIGINT
trap 'handle_signal SIGTERM' SIGTERM
trap 'handle_signal SIGHUP' SIGHUP

echo "Traps set for SIGINT, SIGTERM, SIGHUP"
echo "PID: $$ (send signals to test)"

# Short wait to demonstrate
sleep 1

echo ""
echo "=== Trap with Cleanup and Exit ==="

tempdir=""

setup() {
    tempdir=$(mktemp -d)
    echo "Created temp dir: $tempdir"
}

cleanup_and_exit() {
    local exit_code=${1:-0}
    echo "Cleaning up..."
    
    if [[ -n "$tempdir" && -d "$tempdir" ]]; then
        rm -rf "$tempdir"
        echo "Removed: $tempdir"
    fi
    
    exit $exit_code
}

# Trap EXIT to ensure cleanup
trap cleanup_and_exit EXIT

# Trap signals to exit cleanly
trap 'echo "Interrupted!"; exit 130' INT
trap 'echo "Terminated!"; exit 143' TERM

setup
sleep 1
echo "Work done"

echo ""
echo "=== Ignore Signals ==="

# Ignore SIGINT temporarily
trap '' SIGINT
echo "SIGINT ignored (Ctrl+C won't work)"
sleep 1

# Restore default
trap - SIGINT
echo "SIGINT restored"

echo ""
echo "=== Show Current Traps ==="
trap -p
Output: === Basic EXIT Trap === Created: /tmp/myscript_12345.tmp Script continues... Cleanup: removing temp files... === Trap Multiple Signals === Traps set for SIGINT, SIGTERM, SIGHUP PID: 12345 (send signals to test) === Trap with Cleanup and Exit === Created temp dir: /tmp/tmp.Ab3Cd4Ef Work done Cleaning up... Removed: /tmp/tmp.Ab3Cd4Ef === Show Current Traps === trap -- 'cleanup_and_exit' EXIT trap -- 'echo "Interrupted!"; exit 130' SIGINT trap -- 'echo "Terminated!"; exit 143' SIGTERM
💡 Always Use EXIT Trap
The EXIT trap runs regardless of how the script exits (success, error, signal). Use it for guaranteed cleanup of temp files, lock files, and child processes.
4
Sending Signals to Processes

Using kill to send signals and control processes.

#!/bin/bash
# Sending signals to processes

echo "=== Basic Kill ==="

# Start a process
sleep 100 &
pid=$!
echo "Started sleep, PID: $pid"

# Check if running
if kill -0 $pid 2>/dev/null; then
    echo "Process is running"
fi

# Kill it (SIGTERM by default)
kill $pid
sleep 0.1

if ! kill -0 $pid 2>/dev/null; then
    echo "Process terminated"
fi

echo ""
echo "=== Different Signals ==="

# Start process
sleep 100 &
pid=$!

# Send specific signals
echo "Sending SIGTERM (15) to $pid"
kill -TERM $pid
# or: kill -15 $pid
# or: kill -s SIGTERM $pid

# Start another
sleep 100 &
pid=$!

# SIGKILL - cannot be caught
echo "Sending SIGKILL (9) to $pid"
kill -9 $pid

echo ""
echo "=== Check Process Existence ==="

check_process() {
    local pid=$1
    
    # kill -0 doesn't send signal, just checks
    if kill -0 $pid 2>/dev/null; then
        echo "PID $pid: running"
        return 0
    else
        echo "PID $pid: not running"
        return 1
    fi
}

sleep 100 &
pid=$!
check_process $pid

kill $pid
sleep 0.1
check_process $pid

echo ""
echo "=== Graceful Shutdown Pattern ==="

graceful_kill() {
    local pid=$1
    local timeout=${2:-5}
    
    # Check if process exists
    if ! kill -0 $pid 2>/dev/null; then
        echo "Process $pid not running"
        return 0
    fi
    
    # Send SIGTERM (graceful)
    echo "Sending SIGTERM to $pid..."
    kill -TERM $pid
    
    # Wait for termination
    local count=0
    while kill -0 $pid 2>/dev/null; do
        if ((count >= timeout)); then
            echo "Timeout, sending SIGKILL..."
            kill -9 $pid
            return 1
        fi
        sleep 1
        ((count++))
    done
    
    echo "Process terminated gracefully"
    return 0
}

# Test it
(trap 'echo "Caught TERM"; exit 0' TERM; sleep 100) &
pid=$!
graceful_kill $pid 3

echo ""
echo "=== Kill Process Group ==="

# Start a process that spawns children
(
    sleep 100 &
    sleep 100 &
    sleep 100
) &
parent=$!

sleep 0.5

echo "Parent PID: $parent"
echo "Killing process group..."

# Negative PID kills the process group
kill -- -$parent 2>/dev/null

echo ""
echo "=== pkill and pgrep ==="

# Start named process
(exec -a "myprocess" sleep 100) &
pid=$!

echo "Started 'myprocess' (PID: $pid)"

# Find by name
found=$(pgrep -f "myprocess")
echo "pgrep found: $found"

# Kill by name
pkill -f "myprocess"
echo "pkill executed"
Output: === Basic Kill === Started sleep, PID: 12380 Process is running Process terminated === Graceful Shutdown Pattern === Sending SIGTERM to 12384... Caught TERM Process terminated gracefully === pkill and pgrep === Started 'myprocess' (PID: 12389) pgrep found: 12389 pkill executed
5
Subshells and Process Substitution

Understanding subshells, command grouping, and process substitution.

#!/bin/bash
# Subshells and process substitution

echo "=== Subshell Basics ==="

var="original"

# Subshell - changes don't affect parent
(
    var="modified"
    echo "In subshell: var=$var"
    echo "Subshell PID: $BASHPID"
)

echo "After subshell: var=$var"
echo "Parent PID: $BASHPID"

echo ""
echo "=== Command Grouping ==="

# {} runs in current shell
{
    var="changed"
    echo "In braces: var=$var"
}
echo "After braces: var=$var"

# () runs in subshell
var="original"
(
    var="changed"
)
echo "After parens: var=$var"

echo ""
echo "=== Pipelines Create Subshells ==="

# Each command in pipeline runs in subshell
count=0
echo -e "a\nb\nc" | while read line; do
    ((count++))
done
echo "Count after pipe: $count"  # 0! Lost in subshell

# Solution: use process substitution
count=0
while read line; do
    ((count++))
done < <(echo -e "a\nb\nc")
echo "Count with process sub: $count"  # 3

echo ""
echo "=== Process Substitution ==="

# <() creates a file descriptor from command output
echo "Using <():"

# Compare two command outputs
diff <(echo -e "a\nb\nc") <(echo -e "a\nB\nc")

echo ""

# Feed multiple inputs
paste <(seq 1 3) <(seq 4 6)

echo ""
echo "=== Output Process Substitution ==="

# >() sends output to command
echo "Using >():"

# Tee to multiple commands
echo "Hello World" | tee >(wc -c > /tmp/count.txt) >(tr 'a-z' 'A-Z')
sleep 0.1
echo "Character count: $(cat /tmp/count.txt)"
rm /tmp/count.txt

echo ""
echo "=== Subshell Exit Status ==="

# Subshell exit status
(exit 5)
echo "Subshell exit: $?"

# Pipeline exit status
false | true
echo "Pipeline exit (last): $?"
echo "PIPESTATUS: ${PIPESTATUS[*]}"

echo ""
echo "=== Practical: Parallel Processing ==="

# Process files in parallel with subshells
process_file() {
    local file=$1
    sleep 1  # Simulate work
    echo "Processed: $file"
}

files=("a.txt" "b.txt" "c.txt" "d.txt")
pids=()

for file in "${files[@]}"; do
    (process_file "$file") &
    pids+=($!)
done

echo "Processing ${#files[@]} files in parallel..."
wait
echo "All files processed"
Output: === Subshell Basics === In subshell: var=modified Subshell PID: 12391 After subshell: var=original Parent PID: 12390 === Pipelines Create Subshells === Count after pipe: 0 Count with process sub: 3 === Process Substitution === Using <(): 2c2 < b --- > B 1 4 2 5 3 6 === Practical: Parallel Processing === Processing 4 files in parallel... Processed: a.txt Processed: b.txt Processed: c.txt Processed: d.txt All files processed
⚠️ Pipeline Variable Scope
Variables modified in a pipeline are lost because each command runs in a subshell. Use process substitution < <(cmd) or lastpipe option to work around this.
6
Parallel Execution Patterns

Running tasks in parallel with controlled concurrency.

#!/bin/bash
# Parallel execution patterns

echo "=== Simple Parallel ==="

# Run all in parallel, wait for all
start=$(date +%s)

sleep 1 &
sleep 1 &
sleep 1 &
wait

end=$(date +%s)
echo "3 jobs (1s each) took $((end - start))s (parallel)"

echo ""
echo "=== Parallel with PID Tracking ==="

declare -A jobs

run_job() {
    local name=$1
    local duration=$2
    sleep $duration
    echo "Job '$name' complete"
}

# Start jobs
for job in "database:2" "api:1" "cache:3"; do
    IFS=':' read -r name dur <<< "$job"
    run_job "$name" "$dur" &
    jobs[$!]="$name"
    echo "Started $name (PID: $!)"
done

# Wait and report
for pid in "${!jobs[@]}"; do
    wait $pid
    status=$?
    echo "${jobs[$pid]} finished with status $status"
done

echo ""
echo "=== Limited Concurrency ==="

MAX_JOBS=2
job_count=0

limited_parallel() {
    local cmd=$1
    
    # Wait if at max jobs
    while ((job_count >= MAX_JOBS)); do
        wait -n  # Wait for any job
        ((job_count--))
    done
    
    # Start new job
    eval "$cmd" &
    ((job_count++))
}

echo "Running 5 jobs with max $MAX_JOBS concurrent..."

for i in {1..5}; do
    limited_parallel "sleep 1; echo 'Job $i done'"
done

wait
echo "All done"

echo ""
echo "=== xargs Parallel ==="

# Using xargs for parallel execution
echo -e "1\n2\n3\n4\n5" | xargs -P 3 -I {} bash -c 'sleep 1; echo "xargs job {}"'

echo ""
echo "=== GNU Parallel Style ==="

# Simulate parallel with bash
parallel_run() {
    local max_procs=$1
    shift
    local -a commands=("$@")
    
    local running=0
    local i=0
    
    while ((i < ${#commands[@]})); do
        if ((running < max_procs)); then
            eval "${commands[$i]}" &
            ((running++))
            ((i++))
        else
            wait -n
            ((running--))
        fi
    done
    
    wait
}

commands=(
    "echo 'A'; sleep 1"
    "echo 'B'; sleep 1"
    "echo 'C'; sleep 1"
    "echo 'D'; sleep 1"
)

echo "Parallel run:"
parallel_run 2 "${commands[@]}"
echo "Done"
Output: === Simple Parallel === 3 jobs (1s each) took 1s (parallel) === Parallel with PID Tracking === Started database (PID: 12400) Started api (PID: 12401) Started cache (PID: 12402) Job 'api' complete api finished with status 0 Job 'database' complete database finished with status 0 Job 'cache' complete cache finished with status 0 === Limited Concurrency === Running 5 jobs with max 2 concurrent... Job 1 done Job 2 done Job 3 done Job 4 done Job 5 done All done
7
Lock Files and Singleton Scripts

Preventing multiple instances of a script using lock files.

#!/bin/bash
# Lock files and singleton scripts

echo "=== Basic Lock File ==="

LOCKFILE="/tmp/myscript.lock"

acquire_lock() {
    if [[ -e "$LOCKFILE" ]]; then
        local pid=$(cat "$LOCKFILE")
        
        # Check if process is still running
        if kill -0 "$pid" 2>/dev/null; then
            echo "Script already running (PID: $pid)"
            return 1
        else
            echo "Stale lock file, removing"
            rm -f "$LOCKFILE"
        fi
    fi
    
    echo $$ > "$LOCKFILE"
    return 0
}

release_lock() {
    rm -f "$LOCKFILE"
}

# Set up trap to release lock on exit
trap release_lock EXIT

if ! acquire_lock; then
    exit 1
fi

echo "Lock acquired, running..."
sleep 2
echo "Done"

echo ""
echo "=== flock for Atomic Locking ==="

LOCKFILE="/tmp/flock_test.lock"

(
    # flock creates lock on file descriptor
    exec 200>"$LOCKFILE"
    
    # Try to acquire lock (non-blocking)
    if ! flock -n 200; then
        echo "Could not acquire lock"
        exit 1
    fi
    
    echo "Got lock, working..."
    sleep 2
    echo "Done"
    
    # Lock released when fd closes
) &

# Try to get lock while first is running
sleep 0.5
(
    exec 200>"$LOCKFILE"
    if ! flock -n 200; then
        echo "Second instance blocked (good!)"
        exit 1
    fi
    echo "Second got lock (unexpected)"
) &

wait
rm -f "$LOCKFILE"

echo ""
echo "=== Complete Singleton Pattern ==="

singleton() {
    local lockfile="/tmp/${0##*/}.lock"
    local fd=200
    
    # Open lock file
    eval "exec $fd>\"$lockfile\""
    
    # Try to acquire lock
    if ! flock -n $fd; then
        local pid=$(cat "$lockfile")
        echo "Already running (PID: $pid)" >&2
        return 1
    fi
    
    # Write PID
    echo $$ > "$lockfile"
    
    # Set cleanup trap
    trap "rm -f '$lockfile'" EXIT
    
    return 0
}

# Usage
if ! singleton; then
    exit 1
fi

echo "Singleton running..."

echo ""
echo "=== Timeout Lock ==="

acquire_lock_timeout() {
    local lockfile=$1
    local timeout=$2
    local fd=200
    
    eval "exec $fd>\"$lockfile\""
    
    if flock -w "$timeout" $fd; then
        echo $$ > "$lockfile"
        return 0
    else
        echo "Could not acquire lock within ${timeout}s"
        return 1
    fi
}

# Test
LOCKFILE="/tmp/timeout_test.lock"

# First process holds lock
(
    exec 200>"$LOCKFILE"
    flock 200
    echo "First process has lock"
    sleep 3
) &

sleep 0.5

# Second tries with timeout
if acquire_lock_timeout "$LOCKFILE" 2; then
    echo "Got lock"
else
    echo "Timed out"
fi

wait
rm -f "$LOCKFILE"
Output: === Basic Lock File === Lock acquired, running... Done === flock for Atomic Locking === Got lock, working... Second instance blocked (good!) Done === Complete Singleton Pattern === Singleton running... === Timeout Lock === First process has lock Could not acquire lock within 2s Timed out
💡 Use flock for Production
flock provides atomic locking and handles race conditions properly. Simple PID-file approaches can have race conditions.
8
Managing Child Processes

Spawning, monitoring, and cleaning up child processes.

#!/bin/bash
# Managing child processes

echo "=== Track All Children ==="

declare -a children

spawn() {
    "$@" &
    children+=($!)
    echo "Spawned PID $!: $*"
}

cleanup_children() {
    echo "Cleaning up ${#children[@]} children..."
    for pid in "${children[@]}"; do
        if kill -0 "$pid" 2>/dev/null; then
            kill "$pid"
            echo "Killed $pid"
        fi
    done
}

trap cleanup_children EXIT

spawn sleep 10
spawn sleep 20
spawn sleep 30

echo "Children: ${children[*]}"
sleep 1
# cleanup runs on exit

echo ""
echo "=== Monitor Child Health ==="

monitor_children() {
    local -n pids=$1
    local -a running=()
    
    for pid in "${pids[@]}"; do
        if kill -0 "$pid" 2>/dev/null; then
            running+=($pid)
        else
            wait $pid
            echo "Child $pid exited with status $?"
        fi
    done
    
    pids=("${running[@]}")
    return ${#running[@]}
}

# Start some children
children=()
for i in {1..3}; do
    (sleep $i; exit $((i % 2))) &
    children+=($!)
done

echo "Started: ${children[*]}"

while ((${#children[@]} > 0)); do
    sleep 0.5
    monitor_children children
done

echo "All children done"

echo ""
echo "=== Restart Failed Children ==="

declare -A workers

start_worker() {
    local name=$1
    (
        trap 'exit 0' TERM
        while true; do
            sleep 1
        done
    ) &
    workers[$name]=$!
    echo "Started worker $name (PID: $!)"
}

supervise() {
    for name in "${!workers[@]}"; do
        local pid=${workers[$name]}
        
        if ! kill -0 "$pid" 2>/dev/null; then
            wait $pid
            local status=$?
            echo "Worker $name (PID $pid) died with status $status"
            
            # Restart
            start_worker "$name"
        fi
    done
}

# Start workers
for name in worker1 worker2; do
    start_worker "$name"
done

# Simulate a crash
sleep 1
echo "Killing worker1..."
kill ${workers[worker1]}

sleep 0.5
supervise

sleep 1
echo "Stopping all workers..."
for pid in "${workers[@]}"; do
    kill "$pid" 2>/dev/null
done

echo ""
echo "=== Forward Signals to Children ==="

children=()

forward_signal() {
    echo "Forwarding signal to children..."
    for pid in "${children[@]}"; do
        kill -TERM "$pid" 2>/dev/null
    done
}

trap forward_signal TERM INT

# Start children
for i in {1..3}; do
    (trap 'echo "Child $$ got signal"; exit' TERM; sleep 100) &
    children+=($!)
done

echo "Parent $$, children: ${children[*]}"
sleep 1
forward_signal
wait
Output: === Track All Children === Spawned PID 12410: sleep 10 Spawned PID 12411: sleep 20 Spawned PID 12412: sleep 30 Children: 12410 12411 12412 Cleaning up 3 children... Killed 12410 Killed 12411 Killed 12412 === Restart Failed Children === Started worker worker1 (PID: 12416) Started worker worker2 (PID: 12417) Killing worker1... Worker worker1 (PID 12416) died with status 143 Started worker worker1 (PID: 12418) Stopping all workers...
9
Timeout and Resource Limits

Implementing timeouts and resource constraints for commands.

#!/bin/bash
# Timeout and resource limits

echo "=== timeout Command ==="

# Basic timeout
echo "Running with 2s timeout..."
if timeout 2 sleep 5; then
    echo "Completed"
else
    echo "Timed out (exit: $?)"
fi

# Exit code 124 means timeout
echo ""
echo "Exit code 124 means timeout:"
timeout 1 sleep 10
echo "Exit: $?"

echo ""
echo "=== Custom Timeout Function ==="

run_with_timeout() {
    local timeout=$1
    shift
    
    # Run command in background
    "$@" &
    local pid=$!
    
    # Start watchdog
    (
        sleep "$timeout"
        if kill -0 $pid 2>/dev/null; then
            kill -TERM $pid
        fi
    ) &
    local watchdog=$!
    
    # Wait for command
    wait $pid
    local status=$?
    
    # Kill watchdog if still running
    kill $watchdog 2>/dev/null
    wait $watchdog 2>/dev/null
    
    return $status
}

echo "Custom timeout test:"
run_with_timeout 2 sleep 5
echo "Status: $?"

echo ""
echo "=== ulimit Resource Limits ==="

echo "Current limits:"
echo "  Max processes: $(ulimit -u)"
echo "  Open files:    $(ulimit -n)"
echo "  Stack size:    $(ulimit -s) KB"
echo "  CPU time:      $(ulimit -t) seconds"

# Set limit in subshell
echo ""
echo "Limited subshell:"
(
    ulimit -t 1  # 1 second CPU time
    echo "CPU limit: $(ulimit -t)s"
)

echo ""
echo "=== Retry with Timeout ==="

retry_with_timeout() {
    local max_attempts=$1
    local timeout=$2
    shift 2
    
    local attempt=1
    
    while ((attempt <= max_attempts)); do
        echo "Attempt $attempt of $max_attempts..."
        
        if timeout "$timeout" "$@"; then
            echo "Success on attempt $attempt"
            return 0
        fi
        
        echo "Attempt $attempt failed"
        ((attempt++))
        sleep 1
    done
    
    echo "All $max_attempts attempts failed"
    return 1
}

retry_with_timeout 3 1 bash -c '((RANDOM % 2 == 0)) && echo "OK" || sleep 5'

echo ""
echo "=== Deadline Pattern ==="

run_until_deadline() {
    local deadline=$1
    shift
    
    local now=$(date +%s)
    local timeout=$((deadline - now))
    
    if ((timeout <= 0)); then
        echo "Deadline already passed"
        return 1
    fi
    
    echo "Running with ${timeout}s until deadline..."
    timeout "$timeout" "$@"
}

# Set deadline 3 seconds from now
deadline=$(($(date +%s) + 3))
run_until_deadline $deadline sleep 10
Output: === timeout Command === Running with 2s timeout... Timed out (exit: 124) Exit code 124 means timeout: Exit: 124 === Custom Timeout Function === Custom timeout test: Status: 143 === ulimit Resource Limits === Current limits: Max processes: 63304 Open files: 1024 Stack size: 8192 KB CPU time: unlimited === Retry with Timeout === Attempt 1 of 3... Attempt 1 failed Attempt 2 of 3... OK Success on attempt 2
10
Complete Process Management Library

A reusable library for process and job control.

#!/bin/bash
# Complete process management library

#######################################
# Global State
#######################################

declare -a __MANAGED_PIDS=()
declare -A __PID_NAMES=()

#######################################
# Process Information
#######################################

pid_exists() {
    kill -0 "$1" 2>/dev/null
}

get_pid_status() {
    local pid=$1
    
    if ! pid_exists "$pid"; then
        echo "not running"
    elif [[ -f /proc/$pid/status ]]; then
        grep State /proc/$pid/status | awk '{print $2}'
    else
        echo "running"
    fi
}

#######################################
# Job Management
#######################################

start_job() {
    local name=$1
    shift
    
    "$@" &
    local pid=$!
    
    __MANAGED_PIDS+=($pid)
    __PID_NAMES[$pid]="$name"
    
    echo "$pid"
}

stop_job() {
    local pid=$1
    local timeout=${2:-5}
    
    if ! pid_exists "$pid"; then
        return 0
    fi
    
    # Graceful shutdown
    kill -TERM "$pid"
    
    local count=0
    while pid_exists "$pid" && ((count < timeout)); do
        sleep 1
        ((count++))
    done
    
    # Force kill if needed
    if pid_exists "$pid"; then
        kill -9 "$pid"
        return 1
    fi
    
    return 0
}

stop_all_jobs() {
    for pid in "${__MANAGED_PIDS[@]}"; do
        stop_job "$pid"
    done
    __MANAGED_PIDS=()
}

wait_all_jobs() {
    local failed=0
    
    for pid in "${__MANAGED_PIDS[@]}"; do
        if ! wait "$pid"; then
            ((failed++))
        fi
    done
    
    return $failed
}

#######################################
# Signal Handling
#######################################

setup_signal_handlers() {
    trap 'echo "Interrupted"; stop_all_jobs; exit 130' INT
    trap 'echo "Terminated"; stop_all_jobs; exit 143' TERM
    trap 'stop_all_jobs' EXIT
}

#######################################
# Execution Helpers
#######################################

run_with_timeout() {
    local timeout=$1
    shift
    timeout "$timeout" "$@"
}

run_parallel() {
    local max_jobs=$1
    shift
    local -a commands=("$@")
    
    local running=0
    local idx=0
    
    while ((idx < ${#commands[@]} || running > 0)); do
        while ((running < max_jobs && idx < ${#commands[@]})); do
            eval "${commands[$idx]}" &
            ((running++))
            ((idx++))
        done
        
        if ((running > 0)); then
            wait -n
            ((running--))
        fi
    done
}

retry() {
    local attempts=$1
    local delay=$2
    shift 2
    
    local i
    for ((i=1; i<=attempts; i++)); do
        if "$@"; then
            return 0
        fi
        
        if ((i < attempts)); then
            sleep "$delay"
        fi
    done
    
    return 1
}

#######################################
# Demo
#######################################

echo "========================================"
echo "   Process Management Library Demo"
echo "========================================"

echo ""
echo "=== Setup ==="
setup_signal_handlers
echo "Signal handlers installed"

echo ""
echo "=== Start Jobs ==="
pid1=$(start_job "worker1" sleep 10)
pid2=$(start_job "worker2" sleep 10)
pid3=$(start_job "worker3" sleep 10)

echo "Started:"
for pid in "${__MANAGED_PIDS[@]}"; do
    echo "  PID $pid: ${__PID_NAMES[$pid]}"
done

echo ""
echo "=== Check Status ==="
for pid in "${__MANAGED_PIDS[@]}"; do
    status=$(get_pid_status $pid)
    echo "  PID $pid: $status"
done

echo ""
echo "=== Stop One Job ==="
stop_job $pid1
echo "Stopped PID $pid1"

echo ""
echo "=== Parallel Execution ==="
commands=(
    "sleep 1; echo 'Task A done'"
    "sleep 1; echo 'Task B done'"
    "sleep 1; echo 'Task C done'"
)
run_parallel 2 "${commands[@]}"

echo ""
echo "=== Cleanup ==="
stop_all_jobs
echo "All jobs stopped"

echo ""
echo "========================================"
Output: ======================================== Process Management Library Demo ======================================== === Setup === Signal handlers installed === Start Jobs === Started: PID 12440: worker1 PID 12441: worker2 PID 12442: worker3 === Check Status === PID 12440: running PID 12441: running PID 12442: running === Stop One Job === Stopped PID 12440 === Parallel Execution === Task A done Task B done Task C done === Cleanup === All jobs stopped ========================================

Quick Reference

Essential Commands

# Background job
command &          # Run in background
$!                 # PID of last background job
wait $pid          # Wait for specific job
wait               # Wait for all jobs
jobs               # List jobs

# Signals
kill $pid          # Send SIGTERM
kill -9 $pid       # Send SIGKILL
kill -0 $pid       # Check if running
trap 'cmd' SIGNAL  # Set signal handler

# Process info
$$                 # Current PID
$BASHPID           # Current Bash PID (subshell-aware)
$PPID              # Parent PID
$?                 # Last exit status

Common Patterns

TaskCode
Cleanup on exittrap 'cleanup' EXIT
Handle Ctrl+Ctrap 'exit 130' INT
Check process runningkill -0 $pid 2>/dev/null
Wait with timeouttimeout 30 command
Singleton scriptflock -n lockfile
Parallel executioncmd &; pids+=($!); wait
✓ Best Practices
✗ Common Mistakes
← Back to BashSpecial Index ↑ Back to EXPANDED