Linux Error Guide: 'Terminated by signal 2' — Handle SIGINT and Ctrl-C Cleanly
Understand 'Terminated by signal 2' on Linux: why Ctrl-C sends SIGINT, why the exit code is 130, and how to trap the signal so scripts and services shut down cleanly.
- #linux
- #troubleshooting
- #errors
- #signals
Stuck on this Linux Admins error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
A process ended because it received signal 2 — SIGINT, the interrupt signal. Most commonly this is a human pressing Ctrl-C in the terminal, but it can also come from a parent process, a supervisor, or a kill -2 sent by a script. The kernel reports the termination in messages like these:
$ ./long-running-job.sh
^C
Terminated by signal 2
$ echo $?
130
The exit code 130 is the convention for a process killed by SIGINT: 128 + signal_number where the signal number is 2. Seeing Terminated by signal 2 or an exit status of 130 almost always means the process was interrupted rather than crashing on its own.
Symptoms
- A foreground command stops immediately after you press
Ctrl-C. echo $?returns130right after the process exits.- Shell scripts abort partway through a loop with
Terminated by signal 2in the output. - A CI job or wrapper script logs
exited with code 130and marks the step as failed. dmesg,journalctl, or a supervisor log shows the child receivedSIGINTand shut down.- Cleanup that should run at exit (temp files, locks, child processes) is skipped because the trap was never installed.
Common Root Causes
- Interactive Ctrl-C — the operator interrupted a foreground job; the terminal driver sends SIGINT to the entire foreground process group.
- Parent forwarding the signal — a wrapper script or shell received SIGINT and the child in the same process group also got it.
- Explicit
kill -2 <pid>/kill -INT— a deploy or watchdog script sent SIGINT deliberately to request a graceful stop. - Supervisor stop policy — a process manager (systemd with
KillSignal=SIGINT, tini, dumb-init, or a language runtime) sends SIGINT before SIGTERM. - No signal handler / trap — the program treats SIGINT as the default action (terminate) and exits without cleanup.
- Nested subshells — a script backgrounds work but does not forward or trap SIGINT, so children die abruptly.
Diagnostic Workflow
Confirm the exit code and decode it into a signal number:
./your-command
echo "exit code: $?" # 130 means 128 + 2 (SIGINT)
kill -l 2 # prints: INT (signal 2 = SIGINT)
List the signals a running process is currently ignoring, catching, or blocking (read the bitmask fields):
grep -E 'SigIgn|SigCgt|SigBlk' /proc/$(pgrep -f long-running-job)/status
See who or what may be sending the signal by watching the process tree and the terminal’s foreground group:
ps -o pid,ppid,pgid,stat,cmd -p "$(pgrep -f long-running-job | head -1)"
If a systemd unit is involved, check the configured kill signal and the last exit reason:
systemctl show your.service -p KillSignal -p KillMode
journalctl -u your.service --since '10 min ago' | grep -iE 'signal|SIGINT|stopped'
Reproduce interactively and observe the interrupt in real time:
sleep 300 & # background a dummy job
kill -INT %1 # send SIGINT to it
wait; echo $? # 130 confirms SIGINT termination
Example Root Cause Analysis
A backup script leaves stale lock files whenever an operator cancels it. The team assumed the script was crashing. Running it and pressing Ctrl-C showed:
$ ./nightly-backup.sh
Creating snapshot...
^C
$ echo $?
130
$ ls /var/run/backup.lock
/var/run/backup.lock # lock left behind
The exit code 130 proved the script was interrupted by SIGINT, not failing internally. Inspecting the script revealed no trap for cleanup — the default SIGINT action terminated the shell instantly, before the rm -f "$LOCKFILE" line at the end could run. Adding a trap fixed both the stale lock and orphaned child processes:
#!/usr/bin/env bash
set -euo pipefail
LOCKFILE=/var/run/backup.lock
cleanup() {
rm -f "$LOCKFILE"
# forward the interrupt so callers still see exit 130
trap - INT
kill -INT "$$"
}
trap cleanup INT TERM EXIT
After the change, cancelling with Ctrl-C removed the lock, killed children, and still exited with 130 so CI and wrappers correctly saw an interrupted run rather than a false success.
Prevention Best Practices
- Install a
trap 'cleanup' INT TERM EXITin any script that creates temp files, locks, or child processes. - Preserve the correct exit code: after cleanup, re-raise SIGINT (
trap - INT; kill -INT "$$") so the exit status stays130. - In long-running services, register real SIGINT handlers (Python
signal.signal, Gosignal.Notify, Nodeprocess.on('SIGINT')) to drain work before exiting. - Forward signals to children in wrapper scripts, or use
execso the child replaces the shell and receives signals directly. - For containers, use an init like
tiniordumb-init(ordocker run --init) so PID 1 reaps children and forwards SIGINT properly. - Treat exit code
130in CI as “interrupted,” distinct from application failures, so retries and alerts behave sensibly.
Quick Command Reference
echo $? # 130 = terminated by SIGINT
kill -l 2 # map signal number 2 -> INT
kill -INT <pid> # send SIGINT to a process
trap 'cleanup' INT TERM EXIT # run cleanup on interrupt/exit
grep Sig /proc/<pid>/status # inspect caught/ignored signals
systemctl show <unit> -p KillSignal # supervisor's stop signal
stty -a | grep intr # show which key sends SIGINT (^C)
Conclusion
Terminated by signal 2 and exit code 130 are not crashes — they are a clean report that the process received SIGINT, almost always from Ctrl-C or a deliberate kill -2. Decode the exit code (128 + signal), confirm the source with /proc/<pid>/status and your supervisor’s config, and add a trap or an in-process signal handler so interruptions run your cleanup and preserve the correct exit status. Handled well, SIGINT becomes a graceful-shutdown feature rather than a source of stale locks and orphaned processes.
Fixed it? Get 500 Linux Admins & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.