Jul 16, 2026 7 min read

Linux Signals Explained — SIGTERM, SIGKILL and Process Control

# Linux Signals Explained — SIGTERM, SIGKILL and Process Control Pressing `Ctrl+C` looks simple until the day it stops working. You hit it in a terminal. One Python script exits immediately. A Docker container hangs for ten seconds before disappearing. A runaway process ignores you completely unti

Linux signals process control diagram showing SIGTERM, SIGKILL and SIGINT moving from kernel to running processes in a terminal

Pressing Ctrl+C looks simple until the day it stops working.

You hit it in a terminal. One Python script exits immediately. A Docker container hangs for ten seconds before disappearing. A runaway process ignores you completely until kill -9 ends the argument.

That difference is signals.

I teach Cambridge Computer Science, and this is the gap the syllabus never bridges cleanly. Students learn hardware interrupts, interrupt priority levels, and the interrupt dispatch table. Then they sit down at a Linux terminal and meet SIGINT, SIGTERM, and SIGKILL with no explanation of how those ideas connect. In practice, signals are the missing link. They are how the kernel talks to processes.

My view is blunt: signals should be taught right after interrupt handling, not left as Unix trivia for students to trip over later.

Ctrl+C is SIGINT, not magic

When you press Ctrl+C, the terminal sends SIGINT to the foreground process group. The default action is termination, but a program can catch it and decide what to do next.

That is why a shell script can react to it:

trap 'echo "Caught SIGINT"; exit' INT
while true; do
  sleep 1
  echo "still running"
done

Run that, press Ctrl+C, and the script does not just vanish. It receives a signal, runs the handler, prints the message, and exits cleanly.

Python does the same thing in its own way. When you press Ctrl+C during a Python program, Python translates SIGINT into KeyboardInterrupt. Students often think the traceback is the problem. It is actually the clue. The interpreter is telling you that the kernel interrupted the process and Python surfaced it in a readable form.

This matters because it shows the first rule of signals: a signal is not always an instant kill switch. Sometimes it is a request. The process gets a chance to respond.

SIGTERM is the polite knock

If you run kill 1234 with no extra option, Linux sends SIGTERM.

That is the normal shutdown signal. It tells the process to finish what it is doing and leave properly. A well-behaved process catches SIGTERM, flushes buffers, closes files, saves state, and exits with dignity.

This is the distinction I care about on my own Linux boxes. When I stop services on a Docker host, I do not want a process ripped out of memory halfway through a write. I want it to shut down cleanly.

Here is the simplest Python example:

import signal
import time
import sys

running = True

def shutdown(signum, frame):
    global running
    print(f"Received signal {signum}. Closing cleanly...")
    running = False

signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)

while running:
    time.sleep(1)
    print("working...")

print("Buffers flushed. Connections closed.")
sys.exit(0)

It also explains what Docker is doing. docker stop does not begin with SIGKILL. It sends SIGTERM first, waits for the grace period, then escalates only if the process refuses to go.

That is why a bad container shutdown usually looks like a ten-second pause. Docker is giving PID 1 inside the container time to behave.

If you have read my post on building a container engine from scratch, this is the practical side of the same kernel mechanics. Containers are just processes with isolation wrapped around them. They still live and die by signals.

The Redis example is a good one here. In my Redis 8.8 homelab guide, the container is useful only if its shutdown is clean. Databases, caches, and queue workers need time to flush work to disk. If you know Docker sends SIGTERM first and SIGKILL later, stop_grace_period stops looking like a random YAML setting and starts looking like what it really is: extra time for the process to do the right thing.

Docker graceful shutdown timeline showing a container receiving SIGTERM, a 10-second grace period, and SIGKILL only if the process does not exit in time

SIGKILL is the last resort

Signal 9 cannot be caught, blocked, or ignored. The kernel does not negotiate. It removes the process from the scheduler, tears down its resources, and that is the end of it.

This is why kill -9 works when Ctrl+C does not.

You can prove the difference with a deliberately stubborn script:

import signal
import time

signal.signal(signal.SIGINT, signal.SIG_IGN)

while True:
    time.sleep(1)

Run it. Press Ctrl+C. Nothing happens, because the process is ignoring SIGINT.

Now find the PID:

pgrep -af python

Then send:

kill -9 <PID>

Students remember this one because it feels dramatic, but the real lesson is not to reach for kill -9 as a habit. The real lesson is the opposite. Use it only when the polite options failed.

SIGKILL gives the process no chance to flush, save, or clean up. If you fire it at a database, log writer, or container doing disk work, you are choosing force over correctness.

It matters in container land too. Docker eventually escalates to SIGKILL for the same reason you do: sometimes a process is stuck and has to go. The Linux OOM killer may terminate a process using SIGKILL. A container killed with SIGKILL commonly exits with code 137. Running out of memory is one possible cause.

The signals students meet without realising it

Here are the ones worth knowing first. If you want the canonical reference for default actions and edge cases, the Linux signal(7) page at man7.org is the one worth bookmarking.

Signal Number Default action What it usually means
SIGHUP 1 Terminate Terminal closed, or daemon reload
SIGINT 2 Terminate Ctrl+C from keyboard
SIGKILL 9 Terminate Forced kill by kernel or admin
SIGSEGV 11 Core dump Invalid memory access
SIGPIPE 13 Terminate Wrote to a pipe with no reader
SIGTERM 15 Terminate Normal shutdown request
SIGCONT 18 Continue Resume a stopped process
SIGSTOP 19 Stop Forced stop that cannot be ignored
SIGTSTP 20 Stop Ctrl+Z from terminal

SIGPIPE: why yes | head -5 stops

This one is all over Linux even if nobody names it.

Run:

yes | head -5

yes would happily print forever. head reads five lines and closes its end of the pipe. When yes tries to write again, the kernel sends SIGPIPE. Default action: terminate.

That is why the command ends cleanly instead of flooding your terminal forever.

If you have already worked through my post on Linux pipes and redirection, this is the next piece of the picture. Pipes are not just text plumbing. They are kernel objects with rules, and one of those rules is that a writer with no readers gets told to stop.

SIGHUP: why a remote job dies when SSH drops

SIGHUP began as the hangup signal for a terminal line going away. The name is old, but the behaviour still matters.

A student starts a long-running script over SSH, closes the laptop, comes back later, and the process is gone. That is usually not mysterious. The session closed, the shell's children got SIGHUP, and the default action terminated them.

The fix is not magic either. Use nohup, tmux, or screen so the process is not tied to that one terminal session.

SIGSTOP and SIGCONT: pausing a process without killing it

Start a job:

sleep 1000

Press Ctrl+Z and the shell sends SIGTSTP, which is the terminal stop signal. Then you can continue it with fg or send SIGCONT yourself.

You can also force the stop with:

kill -STOP <PID>
kill -CONT <PID>

That maps nicely onto Cambridge process states. A stopped process is not finished. It is paused until the kernel allows it to continue.

Segmentation faults are signals too

A segmentation fault is usually SIGSEGV, signal 11. The process touched memory it was not allowed to touch, so the kernel terminated it. That is process protection doing exactly what it should do.

This is one of the clearest places where operating system theory becomes real. The OS is enforcing memory boundaries between processes. Without that rule, one broken program could scribble over everything else in RAM.

So when you see:

Segmentation fault (core dumped)

that is not just an error message. It is the kernel reporting a memory protection failure and sending the signal that ends the process.

The Cambridge connection: signals and interrupt handling

This is the part students should be taught explicitly.

In Chapter 16, Cambridge students learn the interrupt model: a device raises an interrupt, the CPU saves state, the interrupt dispatch table is consulted, a service routine runs, then the previous state is restored and execution continues.

Signal delivery follows the same pattern at process level.

  1. An event occurs: keyboard interrupt, parent command, broken pipe, invalid memory access.
  2. The kernel marks a signal as pending for the process or thread.
  3. When control is returning to user mode, the kernel checks whether that signal is blocked, ignored, or caught.
  4. If a handler exists, the kernel arranges for that handler to run.
  5. After the handler finishes, sigreturn restores the saved state.
  6. The process continues, exits, stops, or dumps core depending on the signal and disposition.

That is not a loose analogy. It maps neatly onto the same interrupt pattern students already know, just at process level rather than hardware level.

In You're the OS, I teach interrupt handling as a game because students understand it faster when they have to make the decisions themselves. Signals fit that same lesson perfectly. The kernel is still the traffic controller. The only difference is that now the conversation is between the kernel and a process rather than the CPU and a hardware device.

You can even inspect part of this on a live Linux system in /proc/<pid>/status. Try grep -E "Sig(Pnd|Blk|Ign|Cgt)" /proc/1/status and you will see the signal bitmaps immediately. Fields such as SigPnd, SigBlk, SigIgn, and SigCgt show which signals are pending, blocked, ignored, or caught. That is not abstract theory. That is the kernel's process-control dashboard sitting in plain text.

Kernel-to-process signal flow diagram showing event generation, pending signal check, handler execution, and sigreturn restoring process state — the software mirror of interrupt handling

What students should remember

If you remember only four things, remember these:

  • Ctrl+C sends SIGINT.
  • kill sends SIGTERM by default.
  • kill -9 sends SIGKILL, and the process gets no say in the matter.
  • A lot of weird Linux behaviour is not weird at all once you know which signal fired.

That includes broken SSH jobs, stuck containers, dead pipelines, and segfaults.

Signals are not obscure Unix trivia. They are core process control. They explain how the kernel interrupts, stops, resumes, warns, and kills running programs. For CS students, they are the cleanest bridge between textbook interrupt handling and the real behaviour of a Linux system.

The next time a process ignores Ctrl+C, ask a better question: which signal did it get, and what is it allowed to do with it?

Techie Mike
Techie Mike
Computer Science teacher in Thailand. 10+ years Cambridge IGCSE, 4 years AS/A Level. BSc Computer Science & Engineering. Ex-Intel, Virgin Media. Practical exam prep, past paper walkthroughs and tech tutorials.