Linux System Programming · beginner · ~8 min
## 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.
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.
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.
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).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.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.SIGALRM from alarm() is the classic way to put a time limit on a blocking operation so your program never hangs forever.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.
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.
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.
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 |
+-----------+
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
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.
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 thatcleanupnever runs when they usekill -9. What is the one-sentence explanation?
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.
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 |
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).#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
<signal.h> gives the SIGINT…SIGALRM macros and signal(); <string.h> gives strsignal() and strerror(); <errno.h> gives errno, set when a call fails.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.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.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.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).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).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.)
/* 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.
kill -9 firstkill -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
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);
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.
SIGSEGV, Killed = SIGTERM/SIGKILL, Alarm clock = SIGALRM, Interrupt = SIGINT.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.ulimit -c unlimited.gcc -g -O0 prog.c -o prog.gdb ./prog core.bt (backtrace) to see exactly which line dereferenced bad memory.gcc -fsanitize=address,undefined -g prog.c — it pinpoints the offending access with a readable report.implicit declaration of 'alarm' → you forgot #include <unistd.h>.signal() returns SIG_ERR → check errno; EINVAL usually means you tried to handle SIGKILL or SIGSTOP.printf/malloc may deadlock or crash because those functions are not async-signal-safe; set a volatile sig_atomic_t flag instead.SIGKILL/SIGSTOP are not.)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.write, _exit). Calling printf, malloc, or free there is undefined behaviour because the signal may have interrupted those very functions mid-update.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.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."SIGHUP to re-read configuration files while staying up; this is the classic admin workflow kill -HUP <pid>.SIGINT so Ctrl+C cancels the current operation instead of quitting the whole program.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.SIGSEGV plus a core dump is how production C/C++ crashes are post-mortem debugged.Beginner rules
SIGTERM before SIGKILL; give the process time to exit.volatile sig_atomic_t flag and return.Advanced habits
sigaction() over signal() for predictable semantics (the topic of the next lesson).SIGKILL cannot corrupt it.sigprocmask when needed.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.
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.
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.
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.
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.
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.128 + N (e.g. 139 = SIGSEGV, 137 = SIGKILL).strsignal() or kill -l.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.