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 toSIGTERM(15) if omitted.-l— list every signal name and numberkillunderstands.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, notnode server.js).pkill pattern— signals every process whose name matches a regular expression; add-fto match against the full command line, including arguments.pgrep pattern— same matching aspkill, 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
- You run
kill -TERM 4821. Bash’skillbuiltin calls thekill()system call with PID 4821 and signal number 15. - 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
EPERMandkillreports “Operation not permitted.” - 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.
- 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(), ortrapin a Bash script), that handler runs; otherwise the kernel’s default action — terminate — runs instead. - 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 inpsas<defunct>. - 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 toSIGKILL— you lose any in-process cleanup once you force-kill. - Use
pgrep -af patternto see exactly which processes and command lines will match before runningpkillorkillallwith the same pattern. - Prefer
pkill -foverkillallwhen several unrelated programs could share a short command name (e.g.python,node). - In scripts, use
trap '...' TERM INT EXITto guarantee cleanup code runs whether the script finishes normally, is interrupted, or is asked to terminate. - Use
kill -0 PIDto 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_nameinstead of hunting down its PID — systemd already tracks the whole process tree and cleans up correctly.
Practice Exercises
- Run
sleep 300 &in your terminal, then usepgrep -a sleepto find its PID andkillto stop it gracefully. Confirm it’s gone withpgrep sleep(it should print nothing). - Write a script
cleanup-demo.shthat trapsSIGTERM, removes a temp file it created, and exits. Run it in the background, then send itSIGTERMand check that the cleanup message printed before it exited. - Start two background jobs with
sleep 500 &, run twice. Usejobsto list them, then stop each one by job number withkill %1andkill %2instead of by PID.
Summary
killsends a signal to a process by PID; it doesn’t necessarily terminate it — the process decides how to respond, except toSIGKILL/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.killalltargets processes by exact name;pkill/pgrepmatch by pattern, optionally against the full command line with-f.- Always check what you’re about to signal with
pgrep -afbefore running a broadkillallorpkill. - A terminated process becomes a zombie until its parent reaps its exit status with
wait(). - Use
trapin Bash scripts to handle signals gracefully and clean up before exiting.
