Skip to content

Signals and Process Control: kill, SIGTERM, and SIGKILL

Lowering or raising a process's priority reshapes how much CPU time it gets, but it never stops the process — a niced-down process keeps running indefinitely. Administrators regularly need something categorically different: force a hung process to exit, tell a running service to reload its configuration without a full restart, or pause a long-running job and resume it later. All three are done through the same underlying kernel mechanism — signals — even though the everyday commands for them (kill, killall, Ctrl+C) look unrelated on the surface.

This article covers what a signal actually is at the kernel level, the handful of signals that matter for daily administration, why SIGKILL is a last resort rather than a first instinct, and how a process can choose to catch and handle a signal instead of simply dying from it.

What a Signal Actually Is

A signal is a limited, asynchronous notification the kernel delivers to a process, identified by a small integer and a symbolic name (SIGTERM is signal 15, SIGKILL is signal 9). Signals interrupt whatever the process is currently doing and force it to handle the notification, according to one of three possible dispositions:

  • Default action — most signals have a kernel-defined default if the process does nothing special: terminate, terminate with a core dump, stop, continue, or (for a few) be ignored entirely.
  • Ignored — a process can explicitly tell the kernel to disregard a signal.
  • Caught (handled) — a process can register its own function to run when the signal arrives, letting it clean up resources, log the event, or reload configuration before deciding how to proceed.

Critically, not every signal can be caught or ignored. SIGKILL (9) and SIGSTOP (19) are handled directly by the kernel and cannot be intercepted, blocked, or given a custom handler by the target process under any circumstances — this is a deliberate design guarantee that there is always a way to stop or terminate a process no matter how badly it misbehaves.

The Signals That Matter Day to Day

Signal Number Default action Typical use
SIGHUP 1 terminate historically "terminal hung up"; commonly repurposed by daemons to mean "reload configuration"
SIGINT 2 terminate sent by Ctrl+C in an interactive terminal
SIGQUIT 3 terminate + core dump sent by Ctrl+\; requests a core dump for debugging
SIGKILL 9 terminate unconditional, uncatchable termination
SIGTERM 15 terminate the polite, catchable request to shut down — the default signal kill sends
SIGSTOP 19 stop unconditional, uncatchable pause
SIGCONT 18 continue resumes a stopped process
SIGTSTP 20 stop sent by Ctrl+Z; catchable, unlike SIGSTOP
SIGCHLD 17 ignored sent to a parent when a child terminates or stops
SIGUSR1/SIGUSR2 10/12 terminate reserved for the application's own custom meaning

Exact signal numbers can differ slightly by CPU architecture, but the ones listed above are consistent across the common x86_64 and aarch64 platforms used on Ubuntu Server and RHEL-family systems.

Sending a Signal: kill, killall, pkill

Despite the name, kill sends any signal, not just termination signals — the name reflects that termination was historically its most common use.

kill -TERM 22110
kill -SIGTERM 22110
kill -15 22110

All three lines are equivalent; kill accepts the signal by symbolic name (with or without the SIG prefix) or by number. Without any signal specified, kill 22110 defaults to SIGTERM.

killall nginx
pkill -f 'backup-script.sh'

killall matches by exact process name; pkill matches by name or, with -f, against the entire command line, which is more flexible but also more likely to match something unintended.

Warning

Always confirm the target before sending a signal by pattern. Run pgrep -af '<pattern>' (or pgrep -a for killall's exact-name matching) first and read every line of matched output — a pattern that is broader than intended can terminate an unrelated process that merely shares a substring in its name or arguments.

Checking Whether a Process Exists Without Signaling It

kill -0 22110
echo $?

Signal 0 is special: the kernel performs the existence and permission check without actually delivering any signal. An exit status of 0 means the process exists and you have permission to signal it; a nonzero status (with an error message such as "No such process") means it does not, or you lack permission. This is the standard, side-effect-free way to check "is this PID still alive" in a script.

SIGTERM vs. SIGKILL: Why Order Matters

SIGTERM asks a process to shut down; because it can be caught, a well-behaved process uses it as a cue to close open files, flush buffered writes, finish an in-flight database transaction, and release locks cleanly before exiting. SIGKILL gives the process no such chance — the kernel terminates it immediately at the next scheduling opportunity, with no cleanup code run at all.

kill -TERM 22110
sleep 5
kill -0 22110 && kill -KILL 22110

This is the standard escalation pattern: request a graceful shutdown, wait a reasonable interval for the process to actually exit, and only send SIGKILL if it is still alive after that grace period. systemctl stop follows exactly this pattern internally, configurable per unit via TimeoutStopSec.

Danger

Reaching for SIGKILL (kill -9) as a first instinct routinely causes real damage: a database process killed mid-write can leave a corrupted data file requiring a slow recovery on next startup; an application holding a distributed lock never releases it because its cleanup handler never runs; a temporary file the process would have deleted on graceful exit is left behind. Always try SIGTERM first and give the process a genuine chance to exit on its own before escalating.

Reloading Configuration Without Restarting: SIGHUP

Many long-running daemons — nginx, sshd, rsyslog — repurpose SIGHUP (whose original, terminal-related default action would simply terminate the process) as a request to reload their configuration file in place, without dropping existing connections or losing accumulated state:

sudo nginx -t
sudo kill -HUP "$(cat /run/nginx.pid)"

nginx -t validates the configuration syntax before signaling — sending SIGHUP with a broken configuration file can leave the process in an inconsistent state or cause the reload to silently fail, depending on the application. Checking first is the safer habit regardless of which daemon is involved.

systemctl reload <service> is usually a thin, more discoverable wrapper around exactly this same SIGHUP mechanism (or an application-specific equivalent configured in the unit file's ExecReload=), and should be preferred over sending the signal manually whenever the service is managed by systemd — it keeps systemd's own view of the unit's state consistent.

Pausing and Resuming: SIGSTOP and SIGCONT

kill -STOP 22110
ps -o pid,stat,cmd -p 22110
kill -CONT 22110
    PID STAT CMD
  22110 T    /usr/local/bin/long-running-job

STAT of T confirms the process is genuinely stopped — not running, not sleeping, simply frozen in place by the kernel, consuming no CPU time but retaining all its memory and open file descriptors. SIGCONT resumes it exactly where it left off. This pair is occasionally useful for temporarily pausing a resource-heavy background job during a period of contention, without losing its progress the way terminating and restarting it would.

How a Process Catches a Signal

An application registers a signal handler through the sigaction() (or the older signal()) system call, telling the kernel "when signal X arrives, run this function of mine instead of the default action." This is exactly the mechanism behind graceful shutdown logic in a well-written service: a SIGTERM handler that sets a flag telling the main loop to finish its current unit of work and exit cleanly, rather than the process simply vanishing mid-operation.

strace -e trace=rt_sigaction -p 22110

Running strace briefly against a live process and then sending it a signal from another terminal can show, in real time, whether the process actually registered a custom handler for that signal or is relying on the kernel's default action — useful when diagnosing why a service does or does not seem to respond to SIGHUP the way its documentation claims.

Interview angle

A common interview question: "why can't you catch SIGKILL, and why does that matter operationally?" The kernel-level answer is that SIGKILL and SIGSTOP bypass the normal signal-delivery and disposition machinery entirely, guaranteeing an administrator always has a way to terminate or pause any process regardless of bugs in its signal handling. The operational implication is the reverse of what people sometimes assume: SIGKILL's uncatchability is exactly why it should be a last resort, not a first choice — the process gets no opportunity to release locks, flush data, or clean up temporary state, which is the direct cause of the corruption and leaked-resource problems mentioned above.

Zombies: When a Child's Exit Status Is Never Collected

When a process terminates, it does not disappear from the process table immediately — it becomes a zombie (STAT of Z), retaining only its exit status, until its parent calls wait() (or waitpid()) to collect that status. A parent that never calls wait() — usually a bug, sometimes a genuinely simple script that forks children and never checks on them — leaves zombies accumulating.

ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z/'

A small, stable number of transient zombies is normal — every child briefly passes through this state between exiting and being reaped. A zombie count that keeps growing over time, especially all sharing the same parent PID, points to a parent process with a genuine reaping bug.

Note

A zombie process cannot be killed — it is already dead; only its process table entry remains, waiting to be cleaned up. Sending it any signal, including SIGKILL, has no effect. The only fix is for the parent to call wait(), which means either fixing the parent's code or, if the parent itself is unresponsive, terminating the parent (which reparents the zombie to PID 1 or a subreaper, which does correctly reap it).

A Realistic Scenario: A Service Not Responding to a Normal Stop

Problem. systemctl stop catalog.service returns, but systemctl status still shows the process running after the configured timeout, and the deployment pipeline is now stuck waiting on it.

Check the process's actual state first:

systemctl status catalog.service
ps -o pid,ppid,stat,cmd -p "$(systemctl show -p MainPID --value catalog.service)"
    PID   PPID STAT CMD
  18422      1 D    /srv/catalog/current/bin/catalog

STAT of D (uninterruptible sleep, not simply ignoring the signal) is the key finding — the process is blocked inside the kernel on an I/O operation and, by design, cannot be interrupted by any signal, including SIGTERM and even SIGKILL, until that specific kernel operation completes or times out.

Confirm what it is blocked on:

cat /proc/18422/wchan; echo
sudo cat /proc/18422/stack 2>/dev/null | head -5

Conclusion. If the process is genuinely stuck in D state on a stalled disk or network filesystem operation, no signal will terminate it until the underlying I/O resolves or times out at the kernel level — this is a fundamentally different situation from an application simply ignoring SIGTERM, and sending repeated SIGKILL attempts will not help. The real fix targets the stalled I/O source (an unresponsive NFS server, a failing disk) rather than the process itself; once the I/O either completes or the kernel gives up on it, the process becomes killable normally. If instead the process shows STAT of S or R and simply never exits after SIGTERM, that points to the application's own signal handler being broken or absent — worth checking against its documentation for the correct reload/shutdown signal — and only then is escalating to SIGKILL the appropriate, if blunt, next step.

Common Mistakes

Reaching for kill -9 as a First Step

Skips any cleanup code the process would have run on a graceful SIGTERM, risking corrupted data files, leaked locks, and orphaned temporary state. Always try SIGTERM and wait a reasonable interval first.

Assuming SIGHUP Always Means "Reload"

SIGHUP's default action, absent an application-specific handler, is termination. Whether a given daemon reinterprets it as "reload configuration" depends entirely on that daemon's own code — check its documentation rather than assuming the convention universally applies.

Trying to Kill a Zombie

A zombie is already dead; no signal has any effect on it. The fix targets the parent process that has not called wait(), not the zombie entry itself.

Using a Broad pkill Pattern Without Checking First

pkill -f matches against the full command line, and can silently catch unrelated processes containing the same substring. Run pgrep -af with the same pattern first and read every matched line.

Expecting SIGKILL to Instantly Terminate a Process in D State

A process blocked in uninterruptible sleep on a kernel I/O operation cannot be terminated by any signal, including SIGKILL, until that specific operation resolves. Diagnose the underlying stalled I/O rather than repeating the kill attempt.

Exercises

  1. Start a background sleep 300 &, send it SIGTERM, and confirm with ps that it exited. Repeat with SIGSTOP followed by ps -o stat, then SIGCONT, and describe the difference in what happened to the process at each step.
  2. Explain, citing the signal numbers involved, why kill -9 <pid> and kill -SIGSTOP <pid> cannot be intercepted by the target process, while kill -15 <pid> and kill -SIGTSTP <pid> (from Ctrl+Z) can be.
  3. Find (or reason through) a service on your system that supports configuration reload via SIGHUP, and write the exact command sequence you would use to validate its configuration before signaling it, following the pattern shown for nginx.
  4. A process shows STAT of Z and has been present for over an hour with a growing count of siblings sharing the same PPID. Write the diagnostic command sequence you would use to identify the parent process responsible, and explain what actually needs to be fixed.

Verification criterion: any exercise answer proposing SIGKILL as the first action taken against a running process is incorrect unless you can justify, in writing, why SIGTERM was already confirmed ineffective for that specific case.

References

  • Linux man-pages: signal(7): https://man7.org/linux/man-pages/man7/signal.7.html
  • Linux man-pages: kill(1), kill(2): https://man7.org/linux/man-pages/man1/kill.1.html
  • Linux man-pages: pkill(1), killall(1): https://man7.org/linux/man-pages/man1/pkill.1.html
  • Linux man-pages: sigaction(2): https://man7.org/linux/man-pages/man2/sigaction.2.html
  • Linux man-pages: wait(2), waitpid(2): https://man7.org/linux/man-pages/man2/waitpid.2.html
  • Linux man-pages: proc(5)/proc/[pid]/stat, /proc/[pid]/wchan: https://man7.org/linux/man-pages/man5/proc.5.html
  • freedesktop.org: systemd.service — ExecReload=, TimeoutStopSec=: https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html