open atlas
↑ Back to track
Linux, the operating system LIN · 08 · 02

Signals — deeper

Signals are asynchronous notifications the kernel delivers to a process. SIGTERM is catchable and asks for graceful shutdown; SIGKILL is uncatchable and forces immediate termination. A D-state process ignores even SIGKILL.

LIN Middle ◷ 20 min
Level
FoundationsJuniorMiddleSenior

You already know kill -9 PID from the CLI track. But experienced operators reach for -9 last, not first — because SIGKILL bypasses shutdown hooks, leaves temp files uncleaned, and can corrupt in-flight writes. The right signal depends on what you want the process to do. SIGTERM politely asks it to stop; SIGKILL forces it. SIGHUP tells a daemon to reload its config. SIGSTOP freezes a process without killing it. And when you press Ctrl-C in a terminal, the signal goes to an entire process group — not just one PID. Understanding signals at this level is what separates “I kill things” from “I can control processes precisely.”

Goal

After this lesson you can explain the difference between SIGTERM and SIGKILL, explain why SIGKILL cannot be caught or ignored, know why a D-state process ignores SIGKILL, use SIGHUP to reload a daemon without restarting it, explain process groups and sessions, and understand why Ctrl-C kills a whole pipeline.

1

Signals are asynchronous notifications — the kernel interrupts the process and delivers them.

A signal is a small integer sent by the kernel (or by another process with appropriate permissions) to a target process. The kernel interrupts the target at an instruction boundary and calls the registered signal handler. If no custom handler is installed, the default action runs (which for most signals is: terminate the process).

# List all signals and their default actions:
kill -l
# 1) SIGHUP    2) SIGINT    3) SIGQUIT   4) SIGILL    5) SIGTRAP
# 6) SIGABRT   7) SIGBUS    8) SIGFPE    9) SIGKILL  10) SIGUSR1
# 11) SIGSEGV 12) SIGUSR2  13) SIGPIPE  14) SIGALRM  15) SIGTERM
# 18) SIGCONT  19) SIGSTOP  20) SIGTSTP ...

# Signal by number or name — both are equivalent:
kill -15 1234      # SIGTERM by number
kill -TERM 1234    # SIGTERM by name
kill -SIGTERM 1234 # SIGTERM with SIG prefix

# Default actions (if no handler is installed):
# SIGTERM (15) → terminate
# SIGINT  (2)  → terminate (from Ctrl-C)
# SIGHUP  (1)  → terminate (but daemons often handle it as "reload")
# SIGQUIT (3)  → terminate + core dump
# SIGKILL (9)  → terminate (CANNOT be caught/ignored — kernel-enforced)
# SIGSTOP (19) → stop (CANNOT be caught/ignored — kernel-enforced)
# SIGCONT (18) → continue (resumes a stopped process)
2

SIGTERM vs SIGKILL: the right tool for the right job.

# SIGTERM (15): polite shutdown request
# The process CAN:
# - Catch it and run cleanup code
# - Flush write buffers, close connections, delete temp files
# - Ignore it (badly written processes sometimes do this)
# - Take time before actually exiting
#
# The operator pattern: send SIGTERM, wait, then SIGKILL if needed:
kill -TERM 1234
sleep 10
kill -0 1234 2>/dev/null && kill -KILL 1234
# kill -0 doesn't send a signal — it just checks if the process exists
# If it still exists after 10 seconds, escalate to SIGKILL

# SIGKILL (9): forcible termination — no exceptions
# The kernel forcibly removes the process from the scheduler.
# The process's signal handlers are NEVER invoked.
# No cleanup, no flush, no graceful shutdown.
# Consequences of kill -9:
# - Open database connections left dangling
# - Write-ahead logs not flushed (requires crash recovery on restart)
# - Temp files left on disk
# - systemd restart may be needed since the process did not acknowledge shutdown
kill -KILL 1234    # or: kill -9 1234

# SIGHUP (1): originally "hangup" (terminal disconnected)
# Daemons conventionally handle SIGHUP as "reload configuration"
# This lets you update nginx.conf and reload without dropping connections:
sudo kill -HUP $(cat /var/run/nginx.pid)
# nginx re-reads its config, gracefully drains old workers, starts new ones

# Or with systemctl:
sudo systemctl reload nginx   # sends SIGHUP internally
3

Why D-state processes ignore even SIGKILL.

SIGKILL is the one signal the kernel guarantees cannot be ignored — except when the process is in D (uninterruptible sleep) state. In D state, the process is inside a non-preemptible kernel operation, typically waiting for:

  • Block device I/O (a read from a disk that has stopped responding)
  • An NFS operation on a mounted filesystem that went away
  • A kernel lock (very briefly — usually microseconds, not minutes)
# Identify D-state processes:
ps aux | awk '$8 ~ /D/'

# If a process is stuck in D state for minutes, the problem is the subsystem:
# 1. Check for hung mounts:
mount | grep nfs
df -h  # if df hangs, an NFS mount is unresponsive

# 2. Check kernel logs for I/O errors:
sudo dmesg | tail -30 | grep -E 'error|timeout|hung|reset'
# "task ... blocked for more than 120 seconds" → stuck I/O

# 3. The fix is to resolve the I/O issue (unmount the NFS, replace the disk),
#    NOT to send more signals.
#
# The reason SIGKILL cannot reach D-state processes:
# The kernel delivers signals at safe points — when returning from a syscall
# or kernel preemption point. In uninterruptible sleep, the process is INSIDE
# a critical kernel section. Delivering a signal would leave kernel data structures
# in an inconsistent state. Safety over liveness: the kernel accepts a zombie
# stuck in D over a kernel panic.
4

Process groups and sessions: why Ctrl-C kills a whole pipeline.

Processes are organized into process groups (a set of related processes, usually a pipeline) and sessions (all processes started from a terminal login). The terminal sends signals to the foreground process group — not to a single process.

# Start a two-stage pipeline:
sleep 300 | cat -

# In another terminal, find the process group:
ps -o pid,pgid,sid,comm | grep -E 'sleep|cat'
# PID   PGID   SID    COMM
# 5001  5001   4999   sleep    ← pgid=pid: sleep is process group leader
# 5002  5001   4999   cat      ← same pgid: same group as sleep

# PGID 5001 is the foreground process group of the terminal.
# Ctrl-C sends SIGINT to ALL processes in PGID 5001 — both sleep AND cat.
# That is why the whole pipeline dies, not just the first stage.

# Sessions:
# A session (SID) groups all processes from one terminal login.
# The session leader is the shell. When the terminal closes, SIGHUP is sent
# to the foreground process group. This is why backgrounded processes
# die when you close an SSH session — they receive SIGHUP.
# 'nohup' or 'disown' detaches a process from the session's SIGHUP:
nohup long_running_job &   # ignores SIGHUP from terminal close
Worked example

Graceful shutdown of a service with a timeout fallback.

The pattern every operator should know: try graceful first, force only if needed.

# Target: a long-running Python data pipeline, PID 8901
# It writes to a database and should flush its transaction log on exit.

# Step 1: try SIGTERM — ask it to shut down cleanly
sudo kill -TERM 8901

# Step 2: wait up to 30 seconds for it to exit
for i in $(seq 1 30); do
  kill -0 8901 2>/dev/null || { echo "Process exited cleanly after ${i}s"; exit 0; }
  sleep 1
done

# Step 3: if still running after 30 seconds, escalate
if kill -0 8901 2>/dev/null; then
  echo "Process did not exit cleanly — sending SIGKILL"
  sudo kill -KILL 8901
  echo "Forced. Check for leftover temp files and incomplete DB transactions."
fi

# Verify it is gone:
kill -0 8901 2>/dev/null && echo "still running" || echo "gone"

# Systemd does exactly this: sends SIGTERM, waits TimeoutStopSec (default 90s),
# then sends SIGKILL. You can tune the timeout in the unit file:
# [Service]
# TimeoutStopSec=30   # give 30s before SIGKILL
Why this works

On macOS, the same signals exist and behave identically — POSIX standardizes the core set. The key macOS difference: daemons are managed by launchd, not systemd. launchd sends signals too: when you launchctl unload a service, launchd sends SIGTERM then SIGKILL. The signal mechanism itself (kernel interruption, handler dispatch, uncatchable SIGKILL) is identical because both macOS and Linux follow the POSIX signal model. Even the D-state immunity exists on macOS — processes waiting on I/O are similarly protected against signal interruption inside the kernel.

Common mistake

kill -9 a parent and the children become zombies. If you kill -9 a process that has children, those children are immediately reparented to PID 1 (systemd). But any children that had already exited and were waiting for wait() become zombies — orphaned zombies now under PID 1’s care. systemd reaps them promptly. The real problem is if the parent was managing persistent connections (a proxy, a database) and you kill -9’d it: child workers may now be running without supervision, holding connections open. pkill -TERM -P <ppid> sends SIGTERM to all children of a parent before killing the parent itself — the graceful parent-then-children shutdown sequence.

Check yourself
Quiz

You press Ctrl-C in a terminal where you are running: find / -name '*.log' | gzip > all.gz. Which processes receive a signal, and which signal?

Recap

Signals are asynchronous kernel notifications. SIGTERM (15) is catchable — a well-written process runs cleanup code and exits gracefully. SIGKILL (9) is uncatchable — the kernel forcibly removes the process, no cleanup, no handler. SIGHUP (1) originally meant terminal hangup; daemons conventionally handle it as “reload config” — the standard way to apply a config change without restarting. SIGSTOP (19) freezes a process; SIGCONT (18) resumes it. The D-state exception: a process in uninterruptible sleep ignores even SIGKILL because the kernel will not interrupt critical I/O operations. Process groups are what make Ctrl-C useful: the terminal sends SIGINT to every process in the foreground process group simultaneously, which is why pressing Ctrl-C kills an entire multi-stage pipeline. Sessions group processes from a terminal login; closing the terminal sends SIGHUP to the foreground group. The graceful operator pattern: SIGTERM first, wait, then SIGKILL as the last resort.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources1
expand
  1. 01

Trademarks belong to their respective owners. Editorial reference only.