Linux System Programming · beginner · ~8 min

Common signals: SIGINT, SIGTERM, SIGKILL, SIGSEGV, SIGALRM

## What you will learn - Identify the five signals you will meet almost every day, their numbers, and who sends them. - State the **default action** the kernel takes for each signal when no handler is installed (terminate, terminate + core dump, stop, ignore). - Explain why `SIGKILL` and `SIGSTOP` cannot be caught, blocked, or ignored, and what that means for shutdown logic. - Distinguish a *polite* termination request (`SIGTERM`) from a *forced* kill (`SIGKILL`), and choose the right one when stopping a process. - Recognise `SIGSEGV` as the kernel's response to an invalid memory access, and connect it to bugs you write in C. - Use `strsignal()` and `kill -l` to look up signal names and numbers at runtime instead of memorising every value.

Overview

Every running program on Linux can be poked from the outside by a tiny message called a signal. You met the general idea in the prerequisite lesson What are signals in C and Linux? — a signal is an asynchronous notification delivered by the kernel that interrupts the normal flow of a process to say "something happened." This lesson is the field guide: it names the specific signals you will actually encounter and tells you what each one does by default.

Why a field guide? Because in practice you don't deal with all 30-odd signals equally. A handful show up constantly: when you press Ctrl+C in a terminal, when systemd shuts down a service, when you run kill -9 on a stuck process, when your C code dereferences a bad pointer, or when a timer you set expires. Knowing these five — SIGINT, SIGTERM, SIGKILL, SIGSEGV, SIGALRM — covers the vast majority of real situations.

Two terms anchor everything below. A signal number is the small integer the kernel uses internally (for example, SIGINT is 2). A default action is what the kernel does to your process if you have not installed your own handler for that signal. The four default actions you will see are: Term (terminate the process), Core (terminate and write a core dump for debugging), Stop (pause the process so it can be resumed later), and Ign (ignore — the signal is discarded). This lesson focuses on recognising signals and their defaults; the next lesson, signal() vs sigaction(), shows how to override those defaults safely.

Why it matters

Why this matters

Signals are how the outside world tells your program to stop, reload, or wake up — so getting them right is the difference between software that shuts down cleanly and software that loses data or hangs.

  • Graceful shutdown. Orchestrators like systemd, Docker, and Kubernetes stop your process by sending SIGTERM and waiting a few seconds before escalating to SIGKILL. A service that ignores SIGTERM gets killed hard, dropping in-flight requests and skipping cleanup (flushing buffers, closing files, finishing database transactions).
  • Debugging crashes. When you see a program die with Segmentation fault (core dumped), that is SIGSEGV. Recognising it instantly tells you the bug is a bad memory access — a null pointer, an out-of-bounds array, or a use-after-free — not a logic error in your output.
  • Operations. Knowing that SIGKILL (kill -9) cannot be caught explains why a wedged process sometimes only dies with -9, and why reaching for -9 first is a bad habit: it gives the process no chance to clean up.
  • Timeouts and watchdogs. SIGALRM from alarm() is the classic way to put a time limit on a blocking operation so your program never hangs forever.

Core concepts

The five signals to know cold

A signal is a short, numbered message the operating system (or another process) sends to a running program to report an event. If the program has not installed a handler, the kernel performs the signal's default action.

Signal Number* Sent by Default action Catchable?
SIGINT 2 terminal Ctrl+C terminate yes
SIGTERM 15 kill <pid> (default), systemd terminate yes
SIGKILL 9 kill -9 <pid>, kernel last resort terminate no
SIGSEGV 11 kernel, on invalid memory access terminate + core dump yes**
SIGALRM 14 alarm(), setitimer() terminate yes

*Numbers shown are for x86/ARM Linux. They are not guaranteed by the C standard — always use the names, never the raw numbers. **SIGSEGV can be caught, but doing useful work in the handler is tricky; usually you let it dump core.

Concept 1 — SIGINT: the interactive interrupt

Definition. SIGINT ("interrupt") is sent to the foreground process group of a terminal when you press Ctrl+C.

How it works internally. The terminal driver watches for the interrupt character (normally Ctrl+C). When it sees it, the kernel delivers SIGINT to every process in the foreground group. The default action is to terminate, which is why Ctrl+C stops a runaway program.

When to use / not use. Catch SIGINT when you want Ctrl+C to trigger a clean exit (save state, then quit) instead of an abrupt one. Do not silently ignore it in an interactive tool — users expect Ctrl+C to work.

Pitfall. Beginners assume Ctrl+C "kills" the program forcibly. It does not: it merely requests termination via a catchable signal. A program with a SIGINT handler can refuse to die.

Concept 2 — SIGTERM: the polite shutdown request

Definition. SIGTERM ("terminate") is the standard, catchable request asking a process to shut down. It is what kill <pid> sends by default.

How it works internally. Service managers send SIGTERM, then wait (e.g. systemd's TimeoutStopSec, often 90 s). If the process exits, great. If not, they escalate to SIGKILL.

Structure of a good response. A well-behaved program installs a SIGTERM handler that sets a flag; the main loop notices the flag, finishes the current unit of work, releases resources, and exits. (You will build exactly this in the related exercise Graceful SIGTERM.)

When to use / not use. Always prefer SIGTERM to stop a process you want to behave well. Don't use it to pause a process — that is what stop signals are for.

Pitfall. Doing heavy work directly inside the handler. Handlers run asynchronously and most library functions are not safe to call there; set a flag and return.

Concept 3 — SIGKILL: the unstoppable kill

Definition. SIGKILL immediately and unconditionally terminates a process. It is kill -9.

How it works internally. The kernel never delivers SIGKILL to the target's code at all — it tears the process down directly. That is why it cannot be caught, blocked, or ignored. The same is true of SIGSTOP.

When to use / not use. Use it only as a last resort, after SIGTERM has failed and the process is wedged. Never make it your default — the process gets zero chance to flush buffers, close files, or remove lock files, which can corrupt data.

Pitfall. Writing a handler "just in case." signal(SIGKILL, handler) either fails or is silently ignored; the handler will never run.

  kill -9 (SIGKILL)                kill / SIGTERM
        |                                |
        v                                v
  +-----------+   handler? NO    +----------------+  handler? YES
  | kernel    |----------------->| your process   |---------> your code runs
  | tears the |  (never reaches  | normal delivery |          (flag, cleanup)
  | process   |   your code)     +----------------+
  | down      |
  +-----------+

Concept 4 — SIGSEGV: the segmentation fault

Definition. SIGSEGV ("segmentation violation") is sent by the kernel when your process touches memory it is not allowed to touch.

How it works internally. The CPU's memory-management unit (MMU) checks every access against the page table. An access to an unmapped page, or a write to a read-only page, raises a hardware fault; the kernel converts that fault into SIGSEGV for your process. The default action terminates the process and writes a core dump (a snapshot of memory) you can inspect in a debugger.

Common triggers in C: dereferencing NULL, indexing past the end of an array, using a pointer after free(), or writing to a string literal.

When to catch / not catch. Almost never catch it in normal code — a SIGSEGV means your program is already in an undefined state. Let it crash, then debug the core dump. (Specialised tools catch it, but that is advanced.)

Pitfall. Treating a segfault as a printing or output problem. The crash happens at the memory access, which may be lines away from where you see wrong output.

 int *p = NULL;
 *p = 42;          <-- write to address 0
        |
        v
   MMU: address 0 is not mapped  --> hardware fault
        |
        v
   kernel delivers SIGSEGV --> default: terminate + core dump

Concept 5 — SIGALRM: the timer alarm

Definition. SIGALRM ("alarm") is delivered when a timer set by alarm() or setitimer() expires.

How it works internally. alarm(n) asks the kernel to send SIGALRM after n real seconds. The default action terminates the process, so you almost always install a handler to do something useful (record a timeout flag, abort a blocking call).

When to use / not use. Use it to bound the time of an operation that might block forever (a slow read, a hung network call). Avoid mixing alarm() with sleep() — on some systems they share the same timer.

Pitfall. Forgetting that the default action is terminate. If you call alarm(3) and install no handler, your program simply dies after 3 seconds.

A few more worth knowing

  • SIGHUP (1) — "hang up." Originally meant the terminal line dropped; daemons traditionally treat it as "re-read your configuration."
  • SIGUSR1 / SIGUSR2 — reserved for your application to use however you like.
  • SIGCHLD — tells a parent that a child changed state; servers use it to reap zombie children.
  • SIGPIPE (13) — sent when you write to a pipe or socket whose reader has gone; default is to terminate, which surprises many beginners writing network code.

Knowledge check 1 (explain): In your own words, what is the difference between a signal's number and its default action?

Knowledge check 2 (predict): A program installs no signal handlers and runs alarm(2); then loops forever. What happens about 2 seconds later, and why?

Knowledge check 3 (find-the-reasoning): A teammate adds signal(SIGKILL, cleanup) and is confused that cleanup never runs when they use kill -9. What is the one-sentence explanation?

Syntax notes

Syntax notes

Signal names are macros from <signal.h>; they expand to small integers. Use the names, not the numbers.

#include <signal.h>   /* SIGINT, SIGTERM, ... and kill() prototype  */
#include <string.h>   /* strsignal()                                 */
#include <unistd.h>   /* alarm()                                     */

/* Look up a human-readable name for a signal number at runtime: */
const char *name = strsignal(SIGSEGV);   /* e.g. "Segmentation fault" */

/* Set a one-shot timer: deliver SIGALRM after 5 real seconds.      */
alarm(5);

/* Send a signal to a process by PID (here, ask it to terminate):   */
kill(pid, SIGTERM);   /* returns 0 on success, -1 on error          */

From the shell, kill -l lists every signal name/number on your system, and kill -TERM <pid> (or kill <pid>) sends SIGTERM. kill -9 <pid> sends SIGKILL. Note the deliberate asymmetry: there is no portable way to catch signal 9.

Lesson

The five signals to memorise

You will run into these five signals constantly. The table below is worth committing to memory.

A signal is a short message the operating system or another process sends to a running program to tell it something happened. The default action is what the program does if it has not installed its own handler for that signal.

Signal Number Sent by Default action
SIGINT 2 terminal Ctrl+C terminate
SIGTERM 15 kill <pid> (default), systemd terminate
SIGKILL 9 kill -9 <pid>, kernel last resort terminate (uncatchable)
SIGSEGV 11 kernel, on bad memory access terminate + core dump
SIGALRM 14 alarm(), setitimer() terminate

A few more worth knowing

  • SIGHUP (1) means "your terminal hung up." Daemons (long-running background services) traditionally re-read their configuration when they receive it.
  • SIGUSR1 and SIGUSR2 are reserved for your own application to use however you like.
  • SIGCHLD tells a parent that one of its child processes changed state. Server processes use it to reap zombies (clean up finished child processes).

Code examples

#include <signal.h> #include <string.h> /* strsignal */ #include <stdio.h> #include <errno.h>

/*

  • Print the common signals with their number, short name, and a

  • one-line description, so you can see name <-> number mappings

  • the way the kernel sees them on THIS machine. / int main(void) { / The signals this lesson focuses on, by name (portable). */ int common[] = { SIGINT, SIGTERM, SIGKILL, SIGSEGV, SIGALRM }; size_t n = sizeof(common) / sizeof(common[0]);

    printf("%-4s %-9s %s\n", "num", "name", "description"); printf("---- --------- -----------------------------\n");

    for (size_t i = 0; i < n; i++) { int sig = common[i]; /* strsignal never returns NULL on Linux, but guard anyway. */ const char desc = strsignal(sig); if (desc == NULL) desc = "(unknown signal)"; / sigabbrev_np gives the short name ("INT") on glibc >= 2.32; * we print the number instead to stay portable. */ printf("%-4d %-9s %s\n", sig, "-", desc); }

    /* Show that catchable signals differ from SIGKILL by attempting

    • to IGNORE each. Ignoring SIGKILL must fail with EINVAL. / printf("\nCan we ignore each signal?\n"); for (size_t i = 0; i < n; i++) { int sig = common[i]; / Save the old disposition so we can restore it. */ void (old)(int) = signal(sig, SIG_IGN); if (old == SIG_ERR) printf(" signal %2d: NO (%s)\n", sig, strerror(errno)); else { printf(" signal %2d: yes\n", sig); signal(sig, old); / restore the previous handler */ } } return 0; }

Line by line

Walkthrough of the example

  1. Includes. <signal.h> gives the SIGINTSIGALRM macros and signal(); <string.h> gives strsignal() and strerror(); <errno.h> gives errno, set when a call fails.
  2. int common[] = { ... }; We list the five signals by name. At compile time these expand to the platform's numbers (2, 15, 9, 11, 14 on typical Linux), but the source stays portable.
  3. size_t n = sizeof(common) / sizeof(common[0]); The standard idiom for an array's element count: total bytes divided by one element's bytes.
  4. First loop. For each signal we call strsignal(sig), which returns a human-readable description such as "Interrupt" or "Segmentation fault". We guard against a NULL return even though Linux never returns one — defensive habit.
  5. Second loop — the interesting part. signal(sig, SIG_IGN) asks the kernel to ignore that signal and returns the previous disposition. For catchable signals it succeeds, so we print yes and immediately restore the old handler with signal(sig, old).
  6. The SIGKILL case. When sig is SIGKILL, the call returns SIG_ERR and sets errno to EINVAL ("invalid argument") because the kernel forbids ignoring it. We print NO plus strerror(errno).

What you should see

The first table lists num/name/description for the five signals. The second block prints yes for SIGINT, SIGTERM, SIGSEGV, and SIGALRM, and NO (Invalid argument) for SIGKILL — concrete proof that signal 9 cannot be intercepted.

Step sig signal(sig, SIG_IGN) returns Printed
1 SIGINT old handler (not SIG_ERR) yes
2 SIGTERM old handler yes
3 SIGKILL SIG_ERR, errno=EINVAL NO (Invalid argument)
4 SIGSEGV old handler yes
5 SIGALRM old handler yes

(Exact descriptions and ordering of the first table come from your C library; the catch/no-catch result is identical everywhere.)

Common mistakes

Common mistakes

Mistake 1 — Trying to catch SIGKILL "just in case"

/* WRONG: this handler will never run. */
signal(SIGKILL, my_cleanup);

Why it is wrong. The kernel tears down the process without ever delivering SIGKILL to your code. signal() returns SIG_ERR and sets errno = EINVAL; you have a false sense of safety.

/* CORRECT: do cleanup on the catchable signal, and design so that
 * a hard kill is survivable (e.g. atomic file writes). */
signal(SIGTERM, request_shutdown);   /* set a flag; clean up in main */

How to prevent it. Remember the rule: SIGKILL and SIGSTOP are the two signals you can never catch. Put cleanup on SIGTERM/SIGINT, and make your on-disk state robust to a sudden kill.

Mistake 2 — Reaching for kill -9 first

kill -9 $(pidof myserver)    # WRONG as a first move

Why it is wrong. The process gets no chance to flush logs, finish a transaction, or remove a lock file, which can leave corrupt or stale state.

kill $(pidof myserver)       # CORRECT: SIGTERM first
sleep 5                      # give it time to shut down cleanly
kill -9 $(pidof myserver)    # only if it is still alive

Mistake 3 — Forgetting SIGALRM's default action

alarm(3);          /* no handler installed */
for (;;) { /* work */ }   /* program dies after 3 s — surprise! */

Why it is wrong. The default action of SIGALRM is terminate. Without a handler, your program is killed when the timer fires.

signal(SIGALRM, on_timeout);   /* install a handler first */
alarm(3);

Mistake 4 — Reading wrong output as a logic bug when it is really a segfault

If a program prints garbage and then says Segmentation fault (core dumped), the real problem is an invalid memory access (SIGSEGV), not your printf. Investigate pointers and array indices, not the formatting.

Debugging tips

Debugging tips

Identifying which signal killed a process

  • The shell reports it: Segmentation fault = SIGSEGV, Killed = SIGTERM/SIGKILL, Alarm clock = SIGALRM, Interrupt = SIGINT.
  • A process killed by signal N exits with status 128 + N. So echo $? printing 139 means 128 + 11 = SIGSEGV; 137 means 128 + 9 = SIGKILL.
  • kill -l <status> translates a number back to a name.

Debugging a SIGSEGV

  1. Enable core dumps: ulimit -c unlimited.
  2. Compile with debug info and no optimisation: gcc -g -O0 prog.c -o prog.
  3. Run, reproduce the crash, then load the core in the debugger: gdb ./prog core.
  4. Type bt (backtrace) to see exactly which line dereferenced bad memory.
  5. Better still, run under a sanitizer: gcc -fsanitize=address,undefined -g prog.c — it pinpoints the offending access with a readable report.

Common errors

  • Compiler: implicit declaration of 'alarm' → you forgot #include <unistd.h>.
  • Runtime: signal() returns SIG_ERR → check errno; EINVAL usually means you tried to handle SIGKILL or SIGSTOP.
  • Logic: a handler that calls printf/malloc may deadlock or crash because those functions are not async-signal-safe; set a volatile sig_atomic_t flag instead.

Questions to ask when it doesn't work

  • Which signal actually arrived? (Check the exit status / shell message.)
  • Is the signal catchable at all? (SIGKILL/SIGSTOP are not.)
  • Did I install the handler before the event that triggers the signal?

Memory safety

Memory safety and robustness notes

This is a non-security lesson, so the focus is on correctness and undefined behaviour rather than attackers.

  • SIGSEGV is the symptom, undefined behaviour is the disease. A segfault is the lucky outcome of a memory bug — the kernel caught it. The same bug might instead silently corrupt memory and produce wrong results later. Treat every SIGSEGV as evidence of a real defect: null dereference, out-of-bounds index, use-after-free, or writing through an uninitialised pointer.
  • Not every memory bug segfaults. Reading one byte past an array often hits valid-but-wrong memory and doesn't fault. Rely on AddressSanitizer/Valgrind, not on "it didn't crash," to prove memory correctness.
  • Handlers must be async-signal-safe. Inside a signal handler, only call functions on the async-signal-safe list (e.g. write, _exit). Calling printf, malloc, or free there is undefined behaviour because the signal may have interrupted those very functions mid-update.
  • Use volatile sig_atomic_t for flags. A flag set in a handler and read in main must be volatile sig_atomic_t so the compiler doesn't optimise the read away and so the write is atomic with respect to delivery.
  • strsignal() returns a pointer to a static/internal buffer. Don't free() it and don't assume it stays valid across another strsignal() call.

Real-world uses

Real-world uses and best practices

Concrete uses

  • Container and service shutdown. Docker stop and Kubernetes pod termination send SIGTERM, wait a grace period (default 30 s in Kubernetes), then SIGKILL. Web servers (nginx, your own services) catch SIGTERM to stop accepting new connections, finish in-flight requests, and exit — "graceful shutdown."
  • Config reload without downtime. Long-running daemons catch SIGHUP to re-read configuration files while staying up; this is the classic admin workflow kill -HUP <pid>.
  • Interactive tools. REPLs, editors, and CLIs catch SIGINT so Ctrl+C cancels the current operation instead of quitting the whole program.
  • Timeouts. SIGALRM (via alarm()) bounds a blocking call so a hung network read can't freeze the process forever — the basis of the shell timeout command's behaviour.
  • Crash diagnostics. SIGSEGV plus a core dump is how production C/C++ crashes are post-mortem debugged.

Best-practice habits

Beginner rules

  • Always use signal names, never raw numbers.
  • Send SIGTERM before SIGKILL; give the process time to exit.
  • In handlers, just set a volatile sig_atomic_t flag and return.
  • Install handlers before the triggering event can occur.

Advanced habits

  • Prefer sigaction() over signal() for predictable semantics (the topic of the next lesson).
  • Make on-disk state crash-safe (atomic rename, fsync) so a SIGKILL cannot corrupt it.
  • Log which signal initiated shutdown and how long graceful drain took, to tune timeouts.
  • Mask signals around critical sections with sigprocmask when needed.

Practice tasks

Practice tasks

Beginner 1 — Signal lookup table

Objective. Print each of SIGINT, SIGTERM, SIGKILL, SIGSEGV, SIGALRM with its number and strsignal() description.

Requirements. Use an array of the signal names; loop over it; print number description.

Example output (descriptions vary by system):

 2  Interrupt
15  Terminated
 9  Killed
11  Segmentation fault
14  Alarm clock

Hints. sizeof(arr)/sizeof(arr[0]) for the count; strsignal() from <string.h>. Concepts: signal numbers vs names.

Beginner 2 — Decode an exit status

Objective. Given an integer exit status on the command line, report whether the process was killed by a signal and which one.

Requirements. A process killed by signal N exits with 128 + N. Read argv[1], and if it is between 129 and 159, print the signal number (status - 128) and its name via strsignal(). Otherwise print "normal exit".

Example. Input 139 → "killed by signal 11 (Segmentation fault)". Input 0 → "normal exit".

Hints. atoi/strtol for parsing; validate argc. Concepts: exit status encoding.

Intermediate 1 — Prove SIGKILL is uncatchable

Objective. Write a program that tries to install a handler for each of the five signals and reports which installs succeed.

Requirements. Call signal(sig, my_handler); if it returns SIG_ERR, print the signal name and strerror(errno); otherwise print "installed". Restore handlers you don't need.

Expected. All succeed except SIGKILL (and SIGSTOP if you add it), which fails with EINVAL.

Hints. <errno.h>, strerror. Concepts: catchable vs uncatchable signals.

Intermediate 2 — Timeout with SIGALRM

Objective. Read a line from standard input, but give up after 5 seconds.

Requirements. Install a SIGALRM handler that sets a volatile sig_atomic_t timed_out = 1. Call alarm(5), then read. If the read is interrupted and timed_out is set, print "timed out"; otherwise echo the line and alarm(0) to cancel.

Hints. A blocking read/fgets returns with errno == EINTR when a signal interrupts it. Concepts: SIGALRM, handler flags, async-signal-safety.

Challenge — Graceful shutdown loop

Objective. Build a worker loop that does periodic work and shuts down cleanly on either Ctrl+C or kill <pid>.

Requirements. Install handlers for SIGINT and SIGTERM that set one shared volatile sig_atomic_t stop flag. The main loop checks stop each iteration; when set, it prints which signal asked it to stop, performs cleanup (e.g. "flushing..."), and exits 0. Print your own PID at startup so you can kill it from another terminal.

Constraints. No work in the handler beyond setting the flag (and optionally recording the signal number). No busy-wait that ignores the flag.

Hints. Store the signal number in a second volatile sig_atomic_t. This mirrors the related exercise Graceful SIGTERM. Concepts: SIGTERM/SIGINT handling, flag pattern, clean shutdown.

Summary

Summary

  • A signal is a numbered message from the kernel or another process; its default action is what happens with no handler installed (terminate, terminate + core dump, stop, or ignore).
  • The five to know cold: SIGINT (Ctrl+C), SIGTERM (polite kill / default kill), SIGKILL (forced kill -9, uncatchable), SIGSEGV (invalid memory access, terminate + core dump), SIGALRM (timer from alarm()).
  • SIGKILL and SIGSTOP cannot be caught, blocked, or ignored — the kernel acts on them directly. Put cleanup on SIGTERM/SIGINT and send SIGTERM before resorting to SIGKILL.
  • A process killed by signal N exits with status 128 + N (e.g. 139 = SIGSEGV, 137 = SIGKILL).
  • Use signal names, not numbers; look them up with strsignal() or kill -l.
  • In handlers, just set a volatile sig_atomic_t flag and return — most functions are not async-signal-safe. Next up: why sigaction() is the right tool for installing those handlers.

Practice with these exercises