You already know which process is stuck — a runaway worker, a leftover decoy, a binary that will not exit after a deploy. The next move is sending it a signal: a small kernel message that asks (or forces) the process to change state. That is what kill, pkill, and killall are for. kill targets a precise PID. pkill and killall match by name when you do not want to type every number by hand.

Finding the PID first belongs with ps and pgrep. Watching a hot box live belongs with top, htop, and btop. This article owns the stop side — choose SIGTERM, SIGKILL, or SIGHUP with intent, and prefer the service manager when a supervised unit is involved so you do not leave systemd and sockets in a half-broken state.

Warm-up: a decoy that reacts to signals

Start with two harmless processes: a quiet sleep, and a tiny bash script that prints when it receives TERM or HUP. You will practice sending signals without touching system daemons.

Launch a background sleep and note the PID the shell prints:

sleep 300 &

Confirm it is yours:

ps -p $!

Typical shape:

  PID TTY          TIME CMD
12345 pts/0    00:00:00 sleep

Now create a trap script that logs the signals you care about most:

cat > /tmp/signal-decoy.sh <<'EOF'
#!/usr/bin/env bash
trap 'echo "[decoy] caught SIGTERM — exiting cleanly"; exit 0' TERM
trap 'echo "[decoy] caught SIGHUP — would reload config here"' HUP
echo "[decoy] pid=$$ waiting (TERM exits, HUP logs)"
while true; do sleep 1; done
EOF
chmod +x /tmp/signal-decoy.sh
/tmp/signal-decoy.sh &
DECOY=$!
echo "decoy PID: $DECOY"

Leave both running for the sections below. When you are done practicing, stop only what you started:

kill "$DECOY" 2>/dev/null
kill "$(pgrep -n sleep)" 2>/dev/null

Note: Prefer killing the PID you just launched — not every sleep or bash on a shared host. Name-based tools (pkill, killall) amplify that risk; the sections below show how to dry-run first.

Signals that matter day to day

A signal is not always “die now.” Some are polite requests a process can handle; one of them cannot be caught at all. List the common names on your system:

kill -l

For everyday ops, three names cover almost every intentional stop or reload:

SignalNumberTypical meaning
SIGHUP1“Hang up” — many daemons reload config; orphaned shell jobs may exit
SIGTERM15Default polite stop — flush, close sockets, exit
SIGKILL9Immediate kill — cannot be caught or ignored

Symbolic names and numbers are interchangeable with kill:

kill -TERM 12345
kill -15 12345
kill -KILL 12345
kill -9 12345
kill -HUP 12345

SIGTERM is the default when you omit a signal: kill 12345 means “please exit.” Well-behaved apps register a handler, finish in-flight work, and leave cleanly.

SIGKILL skips handlers entirely. The kernel tears the process down. Use it only after TERM failed — you lose graceful shutdown (open transactions, temp files, listener cleanup).

SIGHUP is overloaded in practice. Interactive shells historically got HUP when a terminal disconnected. Many long-running daemons reinterpret HUP as “re-read config without a full restart.” Prefer the unit’s documented reload path when one exists.

Note: There is no “polite SIGKILL.” If you need cleanup, stay on TERM (or the service manager) longer. Escalate to KILL only when the process is stuck in uninterruptible sleep, ignoring TERM, or already orphaned and unsafe to leave running.

kill by PID (preferred precision)

kill is the precise tool: one PID (or a small explicit list), one signal. After you identify the target with ps or pgrep, send TERM and verify:

kill -TERM 12345
ps -p 12345

If ps still shows the process after a few seconds, escalate:

kill -KILL 12345
ps -p 12345

An empty ps -p result (or exit status 1) means the PID is gone.

Try the same on your trap decoy. TERM should print the clean-exit line and leave:

kill -TERM "$DECOY"

Restart the decoy if you need it again for later sections.

You can also target a process group so a parent and its children share one signal. A negative PID means “this process group”:

kill -TERM -- -12345

The -- stops option parsing so -12345 is not mistaken for a flag. Reach for group signals when you started a pipeline or job tree yourself and know the PGID; guessing group IDs on a production host is a good way to stop more than you meant.

Note: Always re-check with ps -p PID (or pgrep) after TERM and after KILL. “Command succeeded” only means the signal was delivered — not that the process has finished exiting yet.

pkill and killall by name

When you have a distinctive command name and many matching workers, pkill and killall save typing. They also raise the blast radius: a short name like python or node can match unrelated jobs.

pkill mirrors pgrep selection. List matches before signaling whenever the pattern is not unique to your decoy:

pgrep -a sleep
pkill -TERM sleep

Useful pkill filters:

pkill -u "$USER" -TERM sleep
pkill -f 'signal-decoy.sh'

-u limits by user. -f matches the full command line — powerful and dangerous. A broad -f pattern on a shared host can hit another user’s job or a production worker you did not intend.

killall matches by process name (the short comm, not always the full path). On Linux it often ships from psmisc:

killall -l
killall -TERM sleep

killall -l lists signal names. Some platforms also support a “dry run” style flag; when unsure, fall back to pgrep -a name first, then signal the exact PID with kill.

Note: On multi-user or production boxes, prefer kill PID over pkill / killall unless you have just confirmed the match set with pgrep -a (or equivalent) and it contains only what you own. Name tools are conveniences, not a substitute for looking.

Safe stop without breaking service state

Raw signals shine for your decoys, orphaned children, and one-off scripts. Managed services are different. If systemd (or another supervisor) owns the unit, a naked kill on the main PID races the manager:

  • Restart policies may bring the process right back — you “killed” nothing durable.
  • The unit can flip to failed while sockets, mounts, or side cars stay half-open.
  • Dependencies that expected an ordered stop never run their teardown.

Prefer the service manager when the process is a known unit:

systemctl status nginx
systemctl reload nginx
systemctl stop nginx
systemctl restart nginx

Use reload (or the unit’s documented HUP-equivalent) when you only need config reread. Use stop / restart when the process must fully exit and come back under the manager’s control. Afterward, confirm health in the journal:

journalctl -u nginx -n 50 --no-pager

Deep log triage lives in the journalctl guide.

When is a raw kill appropriate next to systemd?

  • A stuck child or helper that the unit did not reap, after you confirmed it is not the main PID systemctl status wants you to manage.
  • An orphan left behind after a crashed supervisor, with no active unit claiming it.
  • A personal process you started in a shell (your sleep, your decoy, your one-off server).

Note: Sending SIGHUP to a random PID because “daemons reload on HUP” is folklore, not a contract. Check the unit docs or man page. When the unit exposes ExecReload=, systemctl reload is the path that keeps service state consistent.

Escalation playbook

When something must stop and you are sure about the target, use a short, repeatable sequence:

  1. Identifyps, pgrep, or systemctl status until the PID (or unit) is unambiguous.
  2. Prefer the manager — if it is a unit, systemctl stop / reload / restart and skip to verification.
  3. TERMkill -TERM PID (or a carefully confirmed pkill).
  4. Wait — a few seconds; watch with ps -p PID or a live monitor.
  5. KILL — only if the PID is still alive and TERM had a fair chance.
  6. Confirm — process gone; for services, check status and recent journal lines.
PID=12345
kill -TERM "$PID"
sleep 3
if ps -p "$PID" >/dev/null; then
  echo "still alive — escalating"
  kill -KILL "$PID"
fi
ps -p "$PID" || echo "gone"

Note: Uninterruptible processes (often stuck on bad disk I/O) may ignore everything until the underlying wait ends. SIGKILL cannot rescue a task sleeping in the kernel on a wedged device — fix storage or reboot paths, do not spam -9 hoping for magic.

Quick reference card

GoalCommand
Default polite stopkill PID / kill -TERM PID
Force stopkill -KILL PID
Reload-style signalkill -HUP PID
List signal nameskill -l
Signal a process groupkill -TERM -- -PGID
Preview name matchespgrep -a name
TERM by name (user-scoped)pkill -u "$USER" -TERM name
Match full cmdlinepkill -f 'pattern' (confirm first)
By short namekillall -TERM name
Managed service stopsystemctl stop unit
Managed service reloadsystemctl reload unit
Verify PID goneps -p PID

Practice drills

Use only decoys you start yourself. Do not practice on sshd, PID 1, or random matches from pgrep on a shared machine.

  1. Start sleep 300 &, send SIGTERM to that PID, and confirm ps -p no longer shows it.
  2. Restart /tmp/signal-decoy.sh, send SIGHUP, and confirm the script printed a reload-style line without exiting.
  3. List would-be targets with pgrep -a for a name you choose, then signal only with kill on the exact PID (skip pkill for this drill).
  4. Start a fresh sleep 300 &, send TERM, wait three seconds, and escalate to KILL only if it is still present (it should already be gone — practice the branch anyway).
  5. Name one real service on your box and write the systemctl stop/reload command you would use instead of kill on its main PID (do not run it on production without intent).

When you are ready to compare, here are solid answers — not the only ones, but clear and portable:

sleep 300 &
kill -TERM $!
ps -p $! || echo gone

/tmp/signal-decoy.sh & DECOY=$!
kill -HUP "$DECOY"
# expect a SIGHUP log line; then:
kill -TERM "$DECOY"

pgrep -a sleep
kill -TERM "$(pgrep -n sleep)"

sleep 300 & PID=$!
kill -TERM "$PID"
sleep 3
ps -p "$PID" >/dev/null && kill -KILL "$PID"
ps -p "$PID" || echo gone

systemctl reload nginx   # or: systemctl stop nginx

Clean up any leftover decoys when finished:

pkill -u "$USER" -TERM -f 'signal-decoy.sh' 2>/dev/null
kill "$(pgrep -n -u "$USER" sleep)" 2>/dev/null
rm -f /tmp/signal-decoy.sh

If you can work through those five comfortably, you already cover most real stop work: pick the right signal, prefer PID precision over broad name matches, escalate TERM → wait → KILL only when needed, and hand managed services back to systemd so unit state stays coherent. Query with ps/pgrep, watch with top/htop/btop when the machine is still moving, and read the aftermath in journalctl when a unit is involved.