Signal: Explanation & Insights
The smallest possible message in Unix — just a number, hand-delivered by the kernel.
What It Is
A signal is a tiny notification the kernel delivers to a running process on behalf of another process, the user, or the kernel itself. And by tiny I mean radically tiny: a signal is just a number. No payload, no body, no return value, no string, no struct. SIGTERM is the integer 15. SIGKILL is 9. SIGUSR1 is 10. The receiving process is told "signal N arrived" and is expected to know what N means.
That minimalism is the whole trick. It's why signals are the universal IPC mechanism of Unix — they ship in every kernel, they cost almost nothing, every process that has ever run was born ready to receive them, and any process can signal any other (subject to permissions) without setting up a socket, a pipe, or shared memory first. It's also why signals are so limited: you can't pass data with a signal, only the fact that something happened. Telling a daemon "reload your config" works perfectly. Telling it "reload, and here's the new config path" does not — signals don't carry strings.
If you've reached this page from kill, pkill, or killall, those are the messengers. This page is about the message itself: what a signal really is, what a process can do with one, and the surprising mechanics underneath.
Why It Matters
Signals are how the outside world talks to a running process. Pressing Ctrl-C in a terminal is a signal (SIGINT). Pressing Ctrl-Z is a signal (SIGTSTP). systemctl stop nginx is a signal (SIGTERM, then SIGKILL after a timeout). A segfault is a signal (SIGSEGV, delivered by the kernel to your own process when it touches bad memory). A child process exiting is a signal to its parent (SIGCHLD). Writing to a closed pipe is a signal (SIGPIPE, which is why yes | head doesn't run forever). Half the surprising behavior of Unix is signals, quietly doing their job.
Understanding signals turns "why did my program die?" from mystery into mechanics. The exit code 137 everyone sees in Docker and Kubernetes? That's 128 + 9 — your container was killed by signal 9, the OOM killer reaping it for using too much memory. Exit 143? 128 + 15 — graceful SIGTERM from systemd. Once you read exit codes in terms of signals, the whole error model clicks.
The Three Things a Process Can Do With a Signal
When a signal arrives, the process has exactly three options for most signals:
- Catch it. Install a signal handler — a function that runs when this signal arrives. The process decides what to do. Web servers catch SIGHUP and reload their config. Long-running batch jobs catch SIGUSR1 and dump a progress report.
bashcatches SIGINT to cancel the current command line without exiting the shell. - Ignore it. Tell the kernel "drop this signal on the floor if it ever comes." The signal still gets sent — your permission check still runs — but nothing happens on the receiving side.
nohupworks by ignoring SIGHUP so a process survives its terminal closing. - Default it. Don't install a handler at all. The kernel does whatever the default disposition for that signal says — usually terminate, sometimes terminate + core dump, sometimes stop, sometimes ignore. Most programs never touch most signals, so most signals run their default.
And then there are the two uncatchable signals — the reason kill -9 is famous:
- SIGKILL (9) — terminate, cannot be caught, blocked, or ignored. The kernel refuses to install a handler for it. No matter how broken the process, SIGKILL yanks it out of existence with no chance to clean up.
- SIGSTOP (19) — pause execution, also uncatchable. The kernel freezes the process; only SIGCONT wakes it back up. This is what makes SIGSTOP such a beautiful debugging tool: freeze any process, attach
straceor peek at/proc, then SIGCONT it.
This is the answer to the newbie question "why does kill -9 always work?" — because the kernel guarantees it. The process is given no opportunity to disobey.
Pro Tip
Run
kill -Lto see every signal name and number your current kernel knows. Almost no one knows this flag exists; it prints a neat 5-column grid (kill -lis the older single-column listing). It's the fastest way to translate a mystery exit-code-minus-128 into a name.
The Signals Worth Knowing
About 30 standard signals exist; on day-to-day Linux you'll really only meet a dozen. Defaults in parens; catchable unless noted. Here's the quick reference, then the prose digs into the ones that bite:
| Signal | Number | Default action | Catchable? | What it's for |
|---|---|---|---|---|
| SIGHUP | 1 | Term | Yes | Terminal hang-up; daemons treat it as "reload config" |
| SIGINT | 2 | Term | Yes | Interrupt from keyboard (Ctrl-C) |
| SIGQUIT | 3 | Core | Yes | Quit from keyboard (Ctrl-\), dumps a core file |
| SIGABRT | 6 | Core | Yes | Abort, raised by abort() / failed assertions |
| SIGKILL | 9 | Term | No | Unconditional kill; cannot be caught, blocked, or ignored |
| SIGUSR1 | 10 | Term | Yes | Application-defined (e.g. nginx reopens log files) |
| SIGSEGV | 11 | Core | Yes | Invalid memory reference (segfault) |
| SIGUSR2 | 12 | Term | Yes | Application-defined (e.g. nginx binary upgrade) |
| SIGPIPE | 13 | Term | Yes | Write to a pipe with no reader |
| SIGTERM | 15 | Term | Yes | Polite "please exit"; the default for kill |
| SIGCHLD | 17 | Ign | Yes | A child process stopped or exited; parent should reap it |
| SIGCONT | 18 | Cont | Yes | Resume a stopped process |
| SIGSTOP | 19 | Stop | No | Freeze the process; cannot be caught, blocked, or ignored |
| SIGTSTP | 20 | Stop | Yes | Stop from keyboard (Ctrl-Z); the catchable cousin of SIGSTOP |
Default action: Term = terminate, Core = terminate + core dump, Stop = pause, Cont = resume, Ign = ignored by default. Numbers are for x86/AMD64/ARM — see "Signal Numbers Are Not Universal" below.
- SIGHUP — number 1, default terminate. "Hang up." Originally sent when a serial-line modem dropped carrier. Today every long-running daemon on the planet catches it and treats it as "reload your config." That's the famous nginx idiom
kill -HUP $(cat /var/run/nginx.pid). - SIGINT — number 2, default terminate. What the terminal sends when you press Ctrl-C.
- SIGQUIT — number 3, default terminate + core dump. What Ctrl-\ sends. Like SIGINT but leaves a core file for
gdb. - SIGILL / SIGFPE / SIGBUS / SIGSEGV — numbers 4/8/7/11, default terminate + core. The crash signals: illegal instruction, divide by zero, bus error, segfault. The kernel sends these to your own process when it does something illegal at the CPU level.
- SIGKILL — number 9, default terminate, uncatchable. The sledgehammer.
- SIGUSR1 / SIGUSR2 — numbers 10/12, default terminate. Reserved for the application to define however it likes. nginx uses USR1 to re-open log files (the
logrotatetrick) and USR2 for binary upgrades. - SIGPIPE — number 13, default terminate. Sent when a process writes to a pipe whose reader has closed. The reason
yes | headexits silently and your shell pipelines sometimes die for no obvious reason. - SIGTERM — number 15, default terminate. The polite "please exit." The default for
kill, the default forsystemctl stop, the right first move every time. - SIGCHLD — number 17, default ignore. Sent to a parent when a child process exits. The parent is supposed to catch this and call
wait()to reap the child; if it forgets, the child becomes a zombie (STAT Zinps). - SIGCONT — number 18, default continue. Resume a stopped process.
- SIGSTOP — number 19, default stop, uncatchable. Freeze the process mid-execution.
- SIGTSTP — number 20, default stop. The catchable cousin of SIGSTOP. What Ctrl-Z sends; how
bgandfgwork. - Signal 0 — no signal, but the permission check still runs.
kill -0 PIDis the canonical "does this process still exist, and may I signal it?" idiom — pure existence test, zero side effect.
Beyond these, numbers 34-64 are the real-time signals (SIGRTMIN through SIGRTMAX) — queued, ordered, and able to carry a small integer value via sigqueue(). Used by a few specialized libraries (POSIX timers, glibc threading). Almost never relevant to normal admin work.
Signal Numbers Are Not Universal
Here's a gotcha that quietly breaks portable scripts: signal numbers depend on the CPU architecture. On x86, AMD64, and ARM, SIGUSR1 is 10. On MIPS it's 16. On Alpha and SPARC it's 30. SIGSTOP is 19 on x86 but 23 on MIPS and 17 on Alpha. Only the names are portable.
kill -USR1 $PID # portable — works everywhere
kill -SIGUSR1 $PID # also portable, fully qualified
kill -10 $PID # WRONG on MIPS, Alpha, SPARC, PA-RISC
Tutorials that say "send signal 10 to reload" are subtly broken. Always use the name in scripts you might ever ship beyond your laptop.
How the Delivery Actually Works
A signal isn't an interrupt and it isn't a function call — it's a flag the kernel sets on the target process. The full chain when you type kill -HUP 2323:
- The
killcommand makes akill(2)syscall into the kernel. - The kernel checks permissions — your real or effective UID must match the target's, or you must be root. Fails with
EPERMif not. - The kernel sets a pending-signal bit in the target's task struct. That's it — the signal is queued, not yet delivered.
- The next time the target process returns from a syscall or is scheduled in by the CPU, the kernel checks the pending bits and delivers the signal — either by running the registered handler or by performing the default disposition.
That step-4 detail is the deepest insight on this page: signals are delivered at syscall boundaries, not asynchronously into running code. Which is why a process stuck in D state (uninterruptible sleep — almost always a frozen disk or hung NFS mount) cannot receive any signal, not even SIGKILL, until the syscall it's inside returns. And if the underlying disk is dead, the syscall never returns, and kill -9 does nothing. This is the "load average 100, CPU 0%, ps shows the process but kill -9 won't budge it" mystery that bites every sysadmin eventually. It's not a bug — it's how signals fundamentally work.
Warning
Inside a signal handler you can only call async-signal-safe functions — roughly: a short whitelist in
man 7 signal-safetythat excludes nearly all of libc. Noprintf, nomalloc, nofopen, no most-of-what-you-know. Call something unsafe and you can deadlock, corrupt heap state, or trigger reentrancy bugs that surface months later. Well-written daemons handle this by making the handler do almost nothing — just set avolatile sig_atomic_tflag — and react to that flag from the main loop on the next iteration. If you ever write a signal handler that does real work, you are probably writing a bug.
How I Inspect Signals
A small toolkit for seeing signals in motion:
kill -L # list every signal name + number on this kernel
kill -l 11 # translate number 11 to a name → SIGSEGV
kill -l SEGV # translate name to number → 11
cat /proc/$PID/status # SigBlk, SigIgn, SigCgt bitmasks (what the process catches/ignores/blocks)
strace -e signal=all -p $PID # watch signals being delivered live
trap -l # in bash: list signals trap can catch
The /proc/<pid>/status view is underrated — three hex bitmasks tell you exactly which signals this process has blocked, which it ignores, and which it catches. If SIGTERM isn't in SigCgt and isn't in SigIgn, the default disposition will run, and the process will exit when you send it.
Inside shell scripts, trap is how you catch signals: trap 'rm -f $tmpfile' EXIT INT TERM cleans up a temp file whether the script exits normally, gets SIGINT via Ctrl-C, or gets SIGTERM from systemd.
Gotchas
kill -9doesn't always work. AD-state process is unreachable until its syscall returns. Fix the underlying I/O problem instead — often that's a stuck NFS mount or a dead disk.- PID 1 is signal-immune by default. The kernel silently drops signals to
init/ systemd unless they've installed a handler — killing PID 1 would kernel-panic the box, so the kernel refuses. - Numbers shift across architectures. Use names in scripts, always. See above.
- Signals don't queue (except real-time ones). If two SIGUSR1s arrive while the handler runs once, the second is merged with the first — the process sees one signal, not two. Don't count signals; treat them as edge-triggered flags.
- You can only signal processes you own. Same UID, or you're root. Otherwise the syscall returns
EPERM. - Exit code
128 + Nmeans killed by signal N.137=SIGKILL.143=SIGTERM.139=SIGSEGV. This is the convention used by bash, Docker, and Kubernetes.
History & Philosophy
Signals shipped in the First Edition of Unix in 1971, older than pipes, older than C, almost as old as Unix itself. Dennis Ritchie and Ken Thompson invented them as the simplest possible way to interrupt a running program on a PDP-11 — a single integer, hand-delivered by the kernel, that the program could choose to handle or ignore. The mechanism was generalized over the years (BSD added reliable signals; POSIX standardized them; real-time signals came in POSIX.1b in 1993) but the core idea has never changed: a signal is a numbered shoulder-tap.
The deeper philosophy is that signals are Unix's answer to "how do you talk to a running program from outside it, without any prior arrangement?" The answer turns out to be: as little as possible. Just a number. No protocol, no handshake, no schema. Any process can signal any other (with permission); every process is born knowing what the defaults mean. That's an extraordinary level of cross-program agreement — fifty years of every Unix program on Earth speaking the same nine-bit vocabulary — achieved with one syscall and a small table of integers. It is, like much of Unix, both an accident of 1970s pragmatism and quietly perfect.
Modern alternatives exist for richer IPC (D-Bus, systemd units, sockets, message queues), and you should use them when you need to pass data. But for "tell this process that something happened" — nothing beats a signal.
See Also
- kill — the command that sends signals by PID
- pkill — send signals by process name
- pgrep — find PIDs by name (so you can signal them)
- killall — name-based signaling, exact match
- ps — see what's running and what's in zombie state
- top / htop — interactive process view, with built-in signal keys
- strace — watch signal delivery live with
-e signal=all - trap — catch signals from inside a shell script
- systemctl — the modern wrapper that sends the right signals with the right timeouts
- nohup — run a process that ignores SIGHUP, surviving terminal close
- process / pid / kernel — the concepts a signal connects
Saw a service exit with code 137 and have no idea why?
CleverUptime watches every service on every server, decodes the exit signal into plain English ("killed by SIGKILL — likely OOM"), and surfaces the recent
dmesgand memory context next to it — so you stop guessing whether the kernel, systemd, or your own code pulled the trigger.Want to see your own server's health right now? One command, no signup, no install.