Stage 2 · Tools
Process & System Management
Signals & Traps
SIGTERM, SIGKILL, SIGHUP — handle signals gracefully in scripts with trap.
Signal Fundamentals
Signals are software interrupts sent to a process. They are the Unix mechanism for inter-process communication. When a process receives a signal, it can handle it, ignore it, or terminate.
# Send signal to process by PID
kill -SIGTERM PID
kill -15 PID # Same as SIGTERM
# Send signal by name
kill -USR1 $(cat /var/run/app.pid)
# List all signals
kill -l
# Send to process group
kill -TERM -PIDkill does not actually kill — it sends a signal. The process decides what to do with it. By default, kill sends SIGTERM (15). The process can catch SIGTERM and clean up before exiting.
Common Signals
| Signal | Number | Default Action | Can Catch? | Use Case |
|---|---|---|---|---|
| SIGHUP | 1 | Terminate | Yes | Terminal hangup, config reload |
| SIGINT | 2 | Terminate | Yes | Ctrl+C |
| SIGQUIT | 3 | Core dump | Yes | Ctrl+\ |
| SIGTERM | 15 | Terminate | Yes | Graceful shutdown (default) |
| SIGKILL | 9 | Terminate | No | Force kill (last resort) |
| SIGUSR1 | 10 | Terminate | Yes | User-defined (e.g., log reopen) |
| SIGUSR2 | 12 | Terminate | Yes | User-defined (e.g., upgrade) |
SIGTERM can be caught and handled — the process can clean up open files, close network connections, and save state. SIGKILL cannot be caught — the kernel terminates the process immediately with no cleanup.
The trap Command
trap lets you specify a command or function to run when a signal is received. It is the primary mechanism for graceful shutdown, cleanup, and signal handling in shell scripts.
# Trap SIGTERM and SIGINT
trap 'echo "Received signal, cleaning up..."; cleanup' SIGTERM SIGINT
# Trap EXIT (runs on any exit)
trap 'echo "Script finished"' EXIT
# Ignore a signal
trap '' SIGINT # Ctrl+C does nothing
# Reset a trap to default
trap - SIGTERM # Back to default behavior
# Multiple signals in one trap
trap 'cleanup' SIGTERM SIGINT SIGHUPThe trap command takes a command string and a list of signals. When any of those signals is received, the command runs. The EXIT trap runs when the script exits for any reason.
Cleanup Patterns
The most common use of trap is cleanup — removing temporary files, closing connections, or stopping background processes when the script exits.
TMPDIR=""
cleanup() {
if [[ -n "$TMPDIR" && -d "$TMPDIR" ]]; then
rm -rf "$TMPDIR"
echo "Cleaned up $TMPDIR"
fi
}
trap cleanup EXIT
TMPDIR=$(mktemp -d)
echo "Working in $TMPDIR"
# ... do work ...
# Cleanup runs automatically when script exitsThe EXIT trap guarantees cleanup happens — whether the script finishes normally, exits on error, or is killed by a signal. This pattern prevents temp file accumulation.
PIDS=()
cleanup() {
echo "Stopping background processes..."
for pid in "${PIDS[@]}"; do
kill "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
done
echo "All processes stopped"
}
trap cleanup EXIT INT TERM
# Start background workers
for i in 1 2 3; do
./worker.sh &
PIDS+=($!)
done
# Wait for all workers
waitTrack background process PIDs in an array. The cleanup trap kills them all when the script exits. wait ensures they finish before the trap completes.
Signal Ignoring
Sometimes you want to prevent a signal from interrupting your script. trap '' SIGNAL ignores the signal — the process continues as if nothing happened.
# Ignore Ctrl+C during critical section
trap '' INT
echo "Critical operation in progress..."
# ... critical code ...
trap - INT # Re-enable Ctrl+C
# Ignore all signals
trap '' SIGTERM SIGINT SIGHUP
# ... atomic operation ...
# Restore default behavior
trap - SIGTERM SIGINT SIGHUPIgnoring signals is useful for atomic operations that must not be interrupted. Always restore signal handling after the critical section — you do not want to permanently ignore signals.
Production Signal Handling
Production scripts need robust signal handling for graceful shutdown, log rotation, and process coordination.
#!/usr/bin/env bash
set -euo pipefail
RUNNING=true
handle_signal() {
local signal="$1"
echo "Received $signal, shutting down gracefully..."
RUNNING=false
# Stop accepting new work
# Finish current work
# Clean up resources
}
trap 'handle_signal SIGTERM' SIGTERM
trap 'handle_signal SIGINT' SIGINT
trap 'handle_signal SIGHUP' SIGHUP
# Main loop
while $RUNNING; do
process_queue_item
sleep 1
done
echo "Shutdown complete"The RUNNING flag controls the main loop. When a signal is received, RUNNING becomes false and the loop exits after finishing the current item. This provides graceful shutdown.
#!/usr/bin/env bash
LOG_FILE="/var/log/app.log"
reopen_logs() {
# Reopen log files (for logrotate)
exec 3>&-
exec 3>>"$LOG_FILE"
echo "Logs reopened at $(date)" >&3
}
trap reopen_logs USR1
# Keep script running
while true; do
echo "$(date): heartbeat" >&3
sleep 60
doneUSR1 is commonly used for log rotation. When logrotate sends USR1, the script reopens its log file descriptors. This lets logrotate compress the old file while the script continues writing to a new one.
The EXIT trap runs on normal exit, error exit, and signal-triggered exit. Use it as your primary cleanup mechanism. Add signal-specific traps only when you need custom behavior for specific signals.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.