CoursesLinux essentialsSignals: kill, TERM vs KILL

Signals: kill, TERM vs KILL

Ask nicely before you force it.

Beginner10 min · lesson 20 of 25

A signal is a tap on the shoulder. You walk over to someone who is deep in their work, tap them, and depending on how you tap they read it differently: "wrap up when you get a chance," "stop right now," or "freeze where you are." On a Linux machine, the tapping is done by the kernel (the core part of the operating system that talks directly to the hardware). You tell the kernel who to tap and which of these small, numbered messages to deliver. The program on the receiving end usually decides what to do about it. Usually, but not always, and that exception is the whole point of this lesson.

The command you use is called kill, which oversells what it does. Most signals are not fatal. kill really means "send a signal to a process," and you aim it using that program's PID (process ID, the unique number the kernel hands to every running program). The two signals you will reach for almost every day are SIGTERM and SIGKILL. SIG is short for signal, so read those as signal-terminate and signal-kill.

Two Messages You'll Send Constantly

SIGTERM (signal 15) is the closing-time announcement. It is the bartender flicking the lights and saying "we're closing, please finish your drink and head out." The process hears it and gets to shut down on its own terms: finish the request it is handling, flush data sitting in memory out to disk, release any locks it holds, then exit. SIGKILL (signal 9) is cutting the power to the whole building. The process gets no announcement and no chance to react. The kernel removes it on the spot. That difference, a graceful exit versus an instant stop, is why the order you send them in matters so much.

You can watch the difference. Here is a tiny script that catches SIGTERM and does a little cleanup before it leaves.

cleanup-demo.sh
#!/usr/bin/env bash
cleanup() {
echo "caught SIGTERM: flushing work, releasing lock"
rm -f /tmp/worker.lock
exit 0
}
trap cleanup TERM # run cleanup() when SIGTERM arrives
echo "worker started (PID $$), holding lock"
touch /tmp/worker.lock
while true; do sleep 1; done # pretend to do real work

Run it in the background, then send the polite signal (the default one) with a plain kill and no number at all.

~/secopslog — bash
$ ./cleanup-demo.sh & kill 4821 ls /tmp/worker.lock
[1] 4821 worker started (PID 4821), holding lock caught SIGTERM: flushing work, releasing lock [1]+ Done ./cleanup-demo.sh ls: cannot access '/tmp/worker.lock': No such file or directory

The process heard the request, ran its cleanup, deleted its lock file, and exited cleanly (the shell reports Done). Now do the same thing but reach for the hammer.

~/secopslog — bash
$ ./cleanup-demo.sh & kill -9 4830 ls /tmp/worker.lock
[1] 4830 worker started (PID 4830), holding lock [1]+ Killed ./cleanup-demo.sh /tmp/worker.lock

No "caught SIGTERM" line this time, and the lock file is still sitting there. SIGKILL never gave the cleanup code a chance to run, so the process left its mess behind. Multiply that lock file by a real database's half-written files and you can see the shape of the problem. This also has a security edge: a well-behaved program cleans up on SIGTERM, but hostile code can catch that same signal and use the moment to delete its files and cover its tracks. A graceful stop is a request to code you may not trust.

Never SIGKILL a database to save time
Force-killing a stateful service (a database, a message queue) with SIGKILL is a real way to corrupt data, because the process never gets to flush its in-memory buffers or close its files cleanly. Always try a graceful stop first (SIGTERM, or the service's own stop command) and give it a few seconds. Escalate to -9 only for a genuinely stuck process, and know you may be trading a short hang for a long recovery job.

What 'Catching' a Signal Means

Most signals are like a doorbell. The program has wired up a handler (a small piece of its own code that runs when the bell rings) and can answer however it likes: tidy up and leave, ignore the bell completely, or do something else entirely. That freedom is deliberate and useful. It is also why a plain kill is a request, not a command. A buggy program can wedge and never answer the door. A malicious one can catch SIGTERM on purpose, either to refuse to die or to wipe evidence the instant you ask it to stop.

Two signals have no doorbell. SIGKILL (9) and SIGSTOP (19) are handled entirely by the kernel and never reach the program's own code. There is nothing to catch, nothing to ignore, nothing to trap. That is exactly why SIGKILL always wins, and why, in a hurry, it feels so tempting. The catch is that "always wins" also means "no cleanup, ever."

When a process dies from a signal, that fact travels back to whatever started it. Your shell reports it as an exit status of 128 plus the signal number, and container runtimes like Docker do the same. Press Ctrl-C, which sends SIGINT (signal 2, signal-interrupt), and you can read it straight off.

~/secopslog — bash
$ sleep 60 ^C echo $?
130

130 is 128 plus 2. A process cut down by SIGKILL (9) exits 137, and one that quits on SIGTERM (15) exits 143. So when you spot status=137 in a log, that is the machine telling you something reached for the big hammer, whether that was you, systemd, or an out-of-memory killer stepping in.

Naming Names Instead of Numbers

Typing PIDs by hand gets old, and it is dangerously easy to fat-finger one and kill the wrong program. You can work by name instead. pgrep finds processes, pkill signals them by name, and killall does the same by exact program name. You can also ask the shell to translate between a signal's name and its number.

~/secopslog — bash
$ pgrep -a nginx sudo kill -HUP 812 pkill -TERM -u deploy -f 'node.*worker' kill -l TERM kill -l 9
812 nginx: master process /usr/sbin/nginx -g daemon on; master_process on; 1240 nginx: worker process 1241 nginx: worker process 15 KILL

pkill -u deploy limits the blast radius to processes owned by the deploy user, and -f matches against the whole command line, not only the program name. Those two flags are the difference between stopping one runaway worker and taking down every node process on the box. SIGHUP (signal 1, signal-hangup) is a leftover from the dial-up era, when it meant "the phone line dropped." Daemons (programs that run quietly in the background) repurposed it to mean "reread your configuration." Send SIGHUP to nginx or sshd and it reloads its config file in place without dropping the connections it is already serving. It is the difference between swapping a recipe card and closing the whole kitchen to reprint the menu.

Let Systemd Do the Escalation

You rarely want to run this ladder by hand for a real service. systemd (the manager that starts, stops, and supervises services on most modern Linux systems) already does the graceful-then-forceful dance for you. When you stop a service it sends SIGTERM, waits, and only if the process is still breathing after a timeout does it send SIGKILL. You can read the exact settings for any unit.

~/secopslog — bash
$ systemctl show myapp.service -p KillSignal -p FinalKillSignal -p TimeoutStopUSec
KillSignal=15 FinalKillSignal=9 TimeoutStopUSec=1min 30s

Read that as: ask with SIGTERM, wait a minute and a half, then force with SIGKILL. When that timeout actually fires, the journal (systemd's log) spells it out.

~/secopslog — bash
$ journalctl -u myapp -n 4 --no-pager
Jul 17 09:14:59 web01 systemd[1]: Stopping myapp.service... Jul 17 09:16:29 web01 systemd[1]: myapp.service: State 'stop-sigterm' timed out. Killing. Jul 17 09:16:29 web01 systemd[1]: myapp.service: Killing process 5127 (myapp) with signal SIGKILL. Jul 17 09:16:29 web01 systemd[1]: myapp.service: Main process exited, code=killed, status=9/KILL

Look at the timestamps: 09:14:59 to 09:16:29 is exactly ninety seconds. The service ignored the polite request, its time ran out, and systemd escalated to SIGKILL on its own. If your app needs longer to drain connections, raise TimeoutStopSec in the unit; if a wedged app is stalling your deploys, lower it. Docker runs the same play with a shorter fuse: docker stop sends SIGTERM and waits ten seconds before SIGKILL.

Freeze First, Kill Later

Here is where a DevSecOps habit earns its keep. You find a process you do not trust, say a mystery python script phoning home from /tmp. Your instinct is to kill it. Hold off. A plain kill sends SIGTERM, which hostile code can catch and use to shred its own files on the way out. Instead, freeze it. SIGSTOP (signal 19) is the uncatchable pause. The process stops mid-instruction and cannot run a single line of its own code to react.

~/secopslog — bash
$ sudo kill -STOP 9142 ps -o pid,stat,cmd -p 9142
PID STAT CMD 9142 T python3 /tmp/.hidden/beacon.py

The T in the STAT column means stopped. The process is frozen in place, its memory intact, unable to touch the disk or the network. Now you can capture that memory for analysis, note its open files and network connections, and only then end it. SIGCONT (signal 18) would resume it if you needed to watch it run; SIGKILL ends it for good once you have collected what you need.

Two more things a defender leans on. First, you can only signal a process you own, unless you are root. Try to touch someone else's and the kernel says no.

~/secopslog — bash
$ kill 700
bash: kill: (700) - Operation not permitted

That one line, Operation not permitted, is a permission boundary doing its job: an ordinary user cannot kill root's audit daemon or another user's session. The flip side is the danger. An attacker who reaches root can silence your logging and monitoring (auditd, the Linux audit daemon, or your EDR (endpoint detection and response) agent) with a single signal, which is why those processes are worth watching for unexpected death. Second, there is a signal that sends nothing: signal 0. It delivers no message but still runs the kernel's permission-and-existence check, so scripts (and quiet attackers) use it to ask "is this PID alive, and may I touch it?" without disturbing anything.

~/secopslog — bash
$ kill -0 9142; echo $? kill -0 99999; echo $?
0 bash: kill: (99999) - No such process 1
SIGKILL can't cut through disk I/O
There is one state even SIGKILL cannot touch. A process stuck in uninterruptible sleep (shown as D in the ps STAT column, usually blocked on a broken disk or a hung network mount) ignores every signal, including 9, until the kernel finishes or gives up on the I/O it is waiting for. If kill -9 seems to do nothing and ps shows a D, you are not sending the wrong signal; the process literally cannot be woken to receive it. The fix is upstream: recover the storage or the mount, and in the worst case, reboot.
A process is running and you need it to stop
How do you make it stop?
Normal case
SIGTERM (15)
Ask it to shut down. It flushes data, releases locks, and exits clean. This is the default kill.
Hung / ignoring you
SIGKILL (9)
Kernel force-stops it. No cleanup, no choice. Last resort; may leave a lock or corrupt file behind.
Suspicious
SIGSTOP (19)
Freeze it uncatchably. Capture memory and connections for forensics before it can react, then kill.
Quick check
01Why can a program ignore SIGTERM but never ignore SIGKILL?
Correct — catchable versus uncatchable is the entire distinction.
Incorrect — signal numbers are only identifiers, not priority rankings.
Incorrect — SIGKILL is the opposite of polite, and the program gets no say.
Incorrect — any user can send SIGTERM to a process they own.
02A container's logs show its main process exited with status 137. Reading that number, what almost certainly happened?
Incorrect — a clean exit is status 0, and 137 encodes death by a signal.
Incorrect — a SIGTERM exit is 143 (128+15); 137 is a different signal.
Incorrect — SIGHUP does not terminate a well-behaved daemon, and an exit of 137 means the process died.
Correct — a signal death exits as 128 plus the signal number, so 137 means SIGKILL, whether from you, a systemd timeout, or the out-of-memory killer.
03A process is stuck, and kill -9 on it seems to do nothing at all. ps shows its STAT column as D. What is going on, and what fixes it?
Correct — a D-state process is waiting on the kernel to finish I/O and ignores every signal until that resolves, so you fix the I/O, not the signal.
Incorrect — SIGKILL is 9 (SIGTERM is 15), and the D state, not a wrong number, is why it will not die.
Incorrect — a zombie shows Z, not D, and is a finished process awaiting cleanup, a different situation.
Incorrect — a permission refusal prints 'Operation not permitted' rather than failing silently, and it would not show as state D.

Try this

Work through “Freeze First, Kill Later” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: never SIGKILL a database to save time. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related