Killing Processes (kill, killall, signals)

Every running program on Linux is a process, and the kernel gives you a precise way to ask a process to stop, reload, or die: signals. The kill command doesn’t actually “kill” anything by itself — it sends a signal to a process, and what happens next depends on which signal you sent and how that process responds. Understanding signals turns “just run kill -9” panic into confident, surgical process control: stopping a runaway script, restarting a daemon, or shutting a service down cleanly so it can save its state first.

Overview: How Signals and Process Termination Work

A process is a running instance of a program, tracked by the kernel with a unique PID (process ID) and a data structure that records its memory, open files, and pending work. You can’t erase a process from memory directly — you ask the kernel to deliver it a signal, a small integer that acts like a software interrupt. The kernel marks the signal as “pending” against that process, and the next time the process is scheduled to run, the kernel checks for pending signals before handing control back to the program’s own code.

What happens next depends on the signal and the process. Every signal has a default action defined by the kernel: terminate, terminate with a core dump, stop (suspend), continue (resume), or ignore. A process can override this for most signals by installing a signal handler — code that runs instead of the default action. In a Bash script, that’s exactly what the trap builtin does: it registers a handler so the script can close files, delete lock files, or log a message before exiting instead of dying mid-operation. That’s why SIGTERM is described as “asking” a process to terminate: a well-behaved program treats it as a polite request, runs its own cleanup, and exits on its own.

Two signals are special: SIGKILL (9) and SIGSTOP (19 on most Linux systems). Neither can be caught, blocked, or ignored — the kernel enforces their default action directly, with no chance for the target process to intervene. That is what makes kill -9 a “sure thing”: it doesn’t ask the process to exit, it forces the kernel to tear it down immediately, with no opportunity to close database connections, flush buffers, or remove temporary files.

To send a signal you need permission: you must own the target process (same user ID) or be root. Otherwise the kernel refuses with “Operation not permitted.” Once a process actually terminates, it doesn’t vanish instantly — it becomes a zombie, an entry that exists only to hold its exit status until its parent calls wait() to collect it. kill cannot remove a zombie; only its parent reaping it can. You can also signal an entire process group (all processes started together in one pipeline or job) using a negative PID, which is how the shell implements job-control signals like those sent by Ctrl+C and Ctrl+Z.

Syntax

Three commands cover almost everything: kill targets a PID directly, killall targets processes by exact command name, and pkill targets processes by a pattern matched against the name or full command line.

kill [-signal | -s signal_name] PID...
kill -l
killall [-signal] process_name...
pkill [-signal] [-f] pattern
  • -signal / -s signal — which signal to send, by number (-9) or name (-TERM, -s TERM). Defaults to SIGTERM (15) if omitted.
  • -l — list every signal name and number kill understands.
  • PID — the numeric process ID to signal; you can pass several at once. A negative PID (e.g. -1234) targets an entire process group.
  • killall process_name — signals every process whose command name matches exactly (e.g. node, not node server.js).
  • pkill pattern — signals every process whose name matches a regular expression; add -f to match against the full command line, including arguments.
  • pgrep pattern — same matching as pkill, but only lists matching PIDs; use it to check what you’re about to kill before you actually kill it.
Number Name Default action Typical use
1 SIGHUP Terminate Terminal closed; many daemons reinterpret this as “reload your config”
2 SIGINT Terminate Sent by Ctrl+C in a terminal
3 SIGQUIT Terminate + core dump Sent by Ctrl+\
9 SIGKILL Terminate (forced) Cannot be caught, blocked, or ignored — a last resort
15 SIGTERM Terminate The default signal for kill; a polite shutdown request
18 SIGCONT Continue Resumes a stopped process
19 SIGSTOP Stop Pauses a process; cannot be caught, blocked, or ignored
20 SIGTSTP Stop Sent by Ctrl+Z; catchable, unlike SIGSTOP

Examples

Example 1: Find and gracefully stop a stuck process

A Node.js server has stopped responding. First, find its PID:

ps aux | grep node

Output:

alice       4821  0.3  1.2 909124 98234 pts/1    Sl   10:02   0:03 node server.js
alice       5310  0.0  0.0   6408   736 pts/1    S+   10:05   0:00 grep --color=auto node

The second line is just grep matching its own command line — ignore it. The real target is PID 4821. Send the default signal, SIGTERM, asking it to shut down:

kill 4821

There’s no output on success — kill only prints something if it fails. Confirm the process actually exited:

ps -p 4821

Output:

    PID TTY          TIME CMD

Only the header line prints, with no matching row — the process is gone. If server.js had installed its own SIGTERM handler, this is the point where it would have flushed logs or closed database connections before exiting.

Example 2: SIGTERM can be caught; SIGKILL cannot

This script installs a handler for SIGTERM so it can clean up a lock file before exiting:

#!/usr/bin/env bash
# cleanup-demo.sh
trap 'echo "Caught SIGTERM - cleaning up..."; rm -f /tmp/cleanup-demo.lock; exit 0' TERM

touch /tmp/cleanup-demo.lock
echo "Running with PID $$"

while true; do
    sleep 1
done

Make it executable and run it in the background:

chmod +x cleanup-demo.sh
./cleanup-demo.sh &

Output:

Running with PID 8842
[1] 8842

Now send it SIGTERM using the PID Bash just printed:

kill 8842

Output, printed a moment later since the trap runs before the process exits:

Caught SIGTERM - cleaning up...
[1]+  Done                    ./cleanup-demo.sh

The handler ran, deleted the lock file, and exited cleanly. Now start it again and force-kill it instead:

./cleanup-demo.sh &
kill -9 $!

Output:

Running with PID 9010
[1]+  Killed                  ./cleanup-demo.sh

Notice there is no “Caught SIGTERM” line, and /tmp/cleanup-demo.lock is left behind — SIGKILL bypasses the trap entirely, because the kernel enforces it directly without giving the process any chance to run its own code.

Example 3: Stopping processes by name with killall and pkill

Suppose several Python scripts are running and you want to stop just one kind. Check first, before touching anything:

pgrep -af python

Output:

7715 python3 manage.py runserver 0.0.0.0:8000
7902 python3 scripts/backup.py
8010 python3 -m http.server 8080

A plain killall python3 would hit all three, including the backup job you don’t want to interrupt. Match the full command line instead:

pkill -f "manage.py runserver"

Nothing is printed on success. Re-running pgrep -af python now shows only the backup script and the HTTP server still running — exactly one process was targeted. When you really do want every process sharing an exact command name, killall is more direct:

killall -TERM firefox

This sends SIGTERM to every running firefox process at once. Both killall and pkill default to SIGTERM just like kill, so only add -9/-KILL if a process ignores the polite request.

How It Works, Step by Step

  1. You run kill -TERM 4821. Bash’s kill builtin calls the kill() system call with PID 4821 and signal number 15.
  2. The kernel looks up the target process and checks permissions: is the caller root, or does it own the process (same UID)? If not, the call fails with EPERM and kill reports “Operation not permitted.”
  3. If permitted, the kernel marks SIGTERM as pending against that process and, if the process is sleeping in an interruptible wait, wakes it up so it can handle the signal.
  4. Next time the process is scheduled, before returning to its own code, the kernel checks pending signals. If the process installed a handler (via sigaction(), or trap in a Bash script), that handler runs; otherwise the kernel’s default action — terminate — runs instead.
  5. If the process terminates, the kernel frees its memory and file descriptors but keeps a small record (PID, exit status) until the parent calls wait()/waitpid(). Until then it’s a zombie, visible in ps as <defunct>.
  6. For SIGKILL or SIGSTOP, the kernel skips the “ask nicely” step entirely and forces the default action — which is why they still work against unresponsive or misbehaving processes.

Common Mistakes

Mistake 1: Reaching for kill -9 first

Force-killing skips any cleanup the process would have done — open files, database transactions, and temp files are left in whatever state they were in.

kill -9 4821

Give the process a chance to shut down gracefully first, and only escalate if it’s still running a few seconds later:

kill 4821
sleep 5
kill -0 4821 2>/dev/null && kill -9 4821

Mistake 2: Confusing a job number with a PID

kill 1 looks like it might mean “job 1” but Bash treats bare numbers as PIDs — PID 1 is init/systemd, and signaling it can crash or reboot the machine.

kill 1

Job numbers need a percent sign so Bash knows to translate them to the right PID:

jobs
kill %1

Mistake 3: Letting killall match more than intended

A short, generic process name can match processes you never meant to touch.

killall python

Check what would match first, then target the specific command line:

pgrep -af python
pkill -f "manage.py runserver"

Best Practices

  • Try SIGTERM (the default) first and give the process a few seconds to exit on its own before escalating to SIGKILL — you lose any in-process cleanup once you force-kill.
  • Use pgrep -af pattern to see exactly which processes and command lines will match before running pkill or killall with the same pattern.
  • Prefer pkill -f over killall when several unrelated programs could share a short command name (e.g. python, node).
  • In scripts, use trap '...' TERM INT EXIT to guarantee cleanup code runs whether the script finishes normally, is interrupted, or is asked to terminate.
  • Use kill -0 PID to test whether a process is still running, and that you have permission to signal it, without actually sending a signal.
  • Never signal PID 1 (init/systemd) — it can crash or reboot the entire system.
  • For services managed by systemd, use sudo systemctl stop service_name instead of hunting down its PID — systemd already tracks the whole process tree and cleans up correctly.

Practice Exercises

  1. Run sleep 300 & in your terminal, then use pgrep -a sleep to find its PID and kill to stop it gracefully. Confirm it’s gone with pgrep sleep (it should print nothing).
  2. Write a script cleanup-demo.sh that traps SIGTERM, removes a temp file it created, and exits. Run it in the background, then send it SIGTERM and check that the cleanup message printed before it exited.
  3. Start two background jobs with sleep 500 &, run twice. Use jobs to list them, then stop each one by job number with kill %1 and kill %2 instead of by PID.

Summary

  • kill sends a signal to a process by PID; it doesn’t necessarily terminate it — the process decides how to respond, except to SIGKILL/SIGSTOP.
  • SIGTERM (15) is the default signal and a polite shutdown request; SIGKILL (9) is a forced, uncatchable termination and should be a last resort.
  • killall targets processes by exact name; pkill/pgrep match by pattern, optionally against the full command line with -f.
  • Always check what you’re about to signal with pgrep -af before running a broad killall or pkill.
  • A terminated process becomes a zombie until its parent reaps its exit status with wait().
  • Use trap in Bash scripts to handle signals gracefully and clean up before exiting.