Chapter 08Lesson 04~50 minutes

Signals, kill, pkill, and Graceful Shutdown

Use Linux signals precisely, verify targets before sending them, and implement graceful shutdown with escalation only when necessary.

SignalsGraceful shutdownHands-on lab

Learning objectives

By the end of this lesson

  • Explain signal delivery, disposition, masking, and the difference between catchable and uncatchable signals.
  • Use kill, pkill, and pgrep with verified targets.
  • Distinguish graceful termination, reload, stop/continue, and forced termination semantics.
  • Implement a shell process that handles SIGTERM and cleans up safely.
  • Apply a bounded TERM-wait-KILL escalation procedure.

1. Signals are asynchronous lifecycle notifications

A signal is a small notification delivered to a process or thread. It does not carry a large payload; it tells the target that an event occurred. Each signal has a default action, and many signals can be caught by a handler, ignored, or temporarily blocked. Signal names are more readable than numbers and avoid some cross-platform differences.

SIGTERM

Request orderly termination

The conventional first choice. Applications can catch it, stop accepting work, flush state, and exit.

SIGHUP

Hangup or application-defined reload

Many daemons interpret it as configuration reload, but behavior is program-specific.

SIGINT

Interactive interruption

Usually generated by Ctrl+C for the foreground process group.

SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. They are kernel-enforced controls. That makes them powerful, but SIGKILL denies the target any opportunity to clean up application state.

2. Signal delivery has a target and a policy

Graceful shutdown decision path
flowchart TD
  V["Verify PID, start time, owner, and service"] --> T["Send SIGTERM"]
  T --> W["Wait for bounded grace period"]
  W -->|Exited| C["Confirm cleanup and service state"]
  W -->|Still running| D["Collect diagnostics"]
  D --> E{"Approved escalation?"}
  E -->|No| O["Continue investigation"]
  E -->|Yes| K["Send SIGKILL"]
  K --> C

Before signaling, verify that the target is the intended process instance. PIDs are reused. Record PID, parent, owner, start time, executable, arguments, service unit, and cgroup when relevant. A broad name match can affect unrelated processes.

pid=$$

# List available signals and show the numeric mapping on this host.
kill -l

# Verify a target before any action.
ps -o user,pid,ppid,lstart,etime,stat,comm,args -p "$pid"
readlink -f "/proc/$pid/exe"
cat "/proc/$pid/cgroup"

# Check whether a PID currently exists and is signalable by this user.
if kill -0 "$pid" 2>/dev/null; then
  printf 'PID %s exists and is accessible.\n' "$pid"
fi

kill -0 sends no signal; it performs existence and permission checks. It still does not prove that a reused PID identifies the same process you observed earlier.

3. kill, pgrep, and pkill

ToolBest useRisk control
killSignal known PIDs or process groupsVerify each target identity first
pgrepPreview processes matching criteriaPrint PID, owner, and full command before acting
pkillSignal processes matching criteriaUse narrow owner, parent, exact-name, or full-command constraints
systemctlManage a supervised servicePrefer service-manager semantics over raw PID control
# Preview exact-name matches owned by the current user.
pgrep -a -u "$(id -u)" -x sleep || true

# Start two controlled targets.
sleep 300 & p1=$!
sleep 300 & p2=$!

# Verify explicit PIDs, then request termination.
ps -o pid,ppid,user,lstart,comm,args -p "$p1" -p "$p2"
kill -TERM "$p1" "$p2"
wait "$p1" 2>/dev/null || true
wait "$p2" 2>/dev/null || true
Preview before pkill

Run the equivalent pgrep query and inspect every match. Name truncation, wrappers, shared executables, and broad regular expressions can produce unexpected targets.

4. The application defines graceful behavior

The kernel delivers the signal, but the application decides what a caught signal means. A well-designed service handles termination by stopping new work, marking itself unready, completing or canceling bounded in-flight work, flushing buffers, closing listeners and files, releasing locks, writing final status, and exiting before the grace deadline.

01Stop admission

Prevent new requests, jobs, or messages from entering the process.

02Expose draining state

Update readiness or registration so traffic moves elsewhere.

03Finish bounded work

Complete safe operations or checkpoint work that can resume.

04Close resources

Flush logs and state; release sockets, files, leases, and locks.

05Exit with evidence

Return a meaningful status and leave logs that explain the shutdown.

Orchestrators and service managers often send SIGTERM, wait a configured grace period, then use SIGKILL. Align application drain time with platform timeouts.

5. Handle termination in a shell worker

Shell traps run code when selected signals or shell events occur. Keep handlers simple, idempotent, and bounded. The example writes state only inside a dedicated lab directory and terminates its current child.

lab="$HOME/devops-academy/linux/chapter08/lesson04"
mkdir -p "$lab"
worker="$lab/worker.sh"

cat > "$worker" <<'SCRIPT'
#!/usr/bin/env bash
set -u
state_dir=${1:?state directory required}
mkdir -p "$state_dir"
child=''

cleanup() {
  trap - TERM INT
  printf '%s shutdown requested\n' "$(date --iso-8601=seconds)" >> "$state_dir/events.log"
  if [[ -n $child ]] && kill -0 "$child" 2>/dev/null; then
    kill -TERM "$child" 2>/dev/null || true
    wait "$child" 2>/dev/null || true
  fi
  rm -f "$state_dir/worker.pid"
  printf '%s cleanup complete\n' "$(date --iso-8601=seconds)" >> "$state_dir/events.log"
  exit 0
}

trap cleanup TERM INT
printf '%s\n' "$$" > "$state_dir/worker.pid"
printf '%s worker started pid=%s\n' "$(date --iso-8601=seconds)" "$$" >> "$state_dir/events.log"

while :; do
  sleep 10 &
  child=$!
  wait "$child" 2>/dev/null || true
  child=''
done
SCRIPT
chmod 700 "$worker"

"$worker" "$lab/state" &
pid=$!
sleep 1
kill -TERM "$pid"
wait "$pid"
cat "$lab/state/events.log"
test ! -e "$lab/state/worker.pid"

Production shell services require deeper design around signal races, subprocess groups, traps while waiting, lock cleanup, and supervisor integration. Prefer purpose-built service code for complex concurrency.

6. Implement bounded escalation

Escalation should be explicit and observable. Send TERM once, wait for a defined interval, capture diagnostics if the target remains, then use KILL only under an approved policy.

terminate_with_timeout() {
  local pid=$1 timeout=${2:-10} started current

  ps -o user,pid,ppid,lstart,stat,comm,args -p "$pid" || return 1
  started=$(stat -c %Y "/proc/$pid" 2>/dev/null) || return 1
  kill -TERM "$pid"

  for (( current=0; current<timeout; current++ )); do
    if ! kill -0 "$pid" 2>/dev/null; then
      return 0
    fi
    # Abort if the /proc instance appears to have changed.
    [[ $(stat -c %Y "/proc/$pid" 2>/dev/null) == "$started" ]] || return 1
    sleep 1
  done

  printf 'PID %s exceeded %ss grace period; escalating.\n' "$pid" "$timeout" >&2
  kill -KILL "$pid"
}

# Test only against a process created by this shell.
sleep 300 &
pid=$!
terminate_with_timeout "$pid" 5
wait "$pid" 2>/dev/null || true

The directory timestamp check is only a lab safeguard, not a universal process-instance identifier. Production supervisors use stronger lifecycle ownership, pidfds, cgroups, or service-unit state.

7. Common mistakes

Starting with SIGKILL

Forced termination can lose buffered data, leave partial work, and hide shutdown defects.

Signaling by broad name

Shared executables and pattern matches can terminate unrelated workloads.

Assuming SIGHUP always reloads

Signal meaning beyond the default action is application-specific. Read the service documentation.

Killing a child instead of its supervisor

The parent may recreate it immediately. Use the service manager or intended lifecycle owner.

8. Knowledge check

Question 1. Why is SIGTERM preferred before SIGKILL?

Question 2. What does kill -0 PID do?

Question 3. Why should you use pgrep before pkill?

9. Summary

Signals are precise lifecycle tools when the target and application semantics are understood. Verify identity, prefer service-manager operations, request graceful termination with SIGTERM, wait for a bounded interval, preserve diagnostics, and escalate to SIGKILL only when necessary. Robust services make shutdown an explicit, tested operating path.

Next lesson

Priorities, nice, cgroups, and Resource Limits

Next, you will constrain how processes compete for CPU, memory, descriptors, and other resources.

10. Further reading

  • signal(7), kill(1), kill(2), pgrep(1), and pkill(1).
  • Bash Reference Manual sections on signals and traps.
  • systemd service stop behavior and container-orchestrator termination-grace documentation.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.