Linux System Programming · beginner · ~8 min
- Explain what a signal is and why it is called a software interrupt. - Name the three sources of signals: the terminal, other processes, and the kernel. - Describe the five default actions and predict what happens when a signal arrives unhandled. - State which two signals can never be caught, blocked, or ignored, and why that matters. - Distinguish standard (coalescing) signals from real-time (queuing) signals.
You already know how to write and call functions — a caller invokes a function, control jumps into it, and returns when it finishes. A signal is a twist on that idea: the kernel calls a function inside your program at an unpredictable moment, without your main() ever asking. This lesson introduces signals as concepts — what they are, where they come from, and what happens by default. Installing your own handler functions is the topic of the next lessons; here we build the mental model those lessons depend on.
A signal is a software interrupt the kernel delivers to a process. It is the simplest inter-process communication Unix offers: no shared memory, no socket, just a small integer (like SIGINT = 2) plus the event of delivering it. Think of it as a tap on the shoulder — the kernel interrupts whatever your process is doing to say "something happened."
Every long-running C program — a server, a daemon, a CLI that holds a lock — must react cleanly to SIGINT and SIGTERM. A program that ignores them can be killed mid-write, leaving corrupt files, stale lock files, half-flushed buffers, or orphaned network sockets. Signals are also how the kernel reports fatal bugs: a wild pointer dereference becomes SIGSEGV, an integer divide-by-zero becomes SIGFPE. Understanding the default actions tells you exactly why a crashing program prints "Segmentation fault" and sometimes drops a core file — and, defensively, how orchestration tools like systemd escalate from a polite SIGTERM to an unstoppable SIGKILL.
A signal is a number and an event. When the kernel decides your process should receive signal N, it interrupts the process, and either runs a handler you installed or performs the signal's default action. Because delivery can happen between any two machine instructions, signals are asynchronous — this is what makes them powerful and also what makes them subtle.
There are exactly three sources:
Ctrl+C sends SIGINT, Ctrl+\ sends SIGQUIT, Ctrl+Z sends SIGTSTP.kill(pid, signo) (the syscall) or the shell's kill command sends a signal to a target process — subject to permission checks, so you cannot signal arbitrary processes you do not own.SIGSEGV, a divide-by-zero is SIGFPE, an illegal instruction is SIGILL. Ctrl+C (terminal) kill(pid, SIGTERM) bad *ptr (fault)
| | |
v v v
+----------------------------- KERNEL --------------------------------+
| looks up the target process's disposition for this signal number |
+--------------------------------------------------------------------+
| | |
v v v
run installed handler perform DEFAULT action signal is IGNORED
(your function) (terminate / core / stop) (no effect)
Every signal has a disposition — what happens if you have not arranged otherwise. There are five possible default actions:
| Default action | Meaning | Example signals |
|---|---|---|
| Term | Terminate the process | SIGINT, SIGTERM, SIGHUP, SIGALRM, SIGUSR1, SIGUSR2 |
| Core | Terminate and write a core dump for debugging |
SIGSEGV, SIGABRT, SIGFPE, SIGQUIT, SIGILL |
| Ign | Do nothing; the signal is discarded | SIGCHLD, SIGURG |
| Stop | Suspend (freeze) the process | SIGSTOP, SIGTSTP |
| Cont | Resume a stopped process | SIGCONT |
Notice that most signals default to terminate. That is why kill -USR1 <pid> will kill an ordinary program even though USR1 sounds harmless — nobody installed a handler, so the default (Term) runs.
Knowledge check: a program installs no handlers. Someone runs kill <pid> (no signal number). Does the program keep running?
No.
killwith no number sendsSIGTERM(15), whose default action is terminate. With no handler installed, the default runs and the process exits. To survive it, the program would have to install aSIGTERMhandler or set the disposition to ignore.
For most signals a process can choose one of three dispositions:
SIG_IGN) — the signal is discarded on arrival.signal() or, preferably, sigaction(). (Writing handlers is the next lesson; here you only need to know the disposition exists.)Two signals are the kernel's escape hatch and can never be caught, blocked, or ignored:
| Signal | Number* | Action | Why it exists |
|---|---|---|---|
SIGKILL |
9 | Terminate immediately | Guarantees an operator can always kill a runaway process |
SIGSTOP |
19 | Suspend immediately | Guarantees a process can always be frozen |
*Numbers vary by platform for many signals; 9 and 19 are stable on Linux. Always use the name, not the number, in code. If a program could block SIGKILL, a buggy or malicious process could become truly unstoppable — so the kernel forbids it by design. In the demo below, sigaction(SIGKILL, ...) deliberately fails with EINVAL to prove this.
Standard signals (numbers 1–31) coalesce: if three SIGINT arrive while the process has that signal blocked, they collapse into a single pending delivery — a signal is one bit in a pending set, not a counter. Real-time signals (SIGRTMIN … SIGRTMAX) queue: each send is delivered separately and can carry a small payload. If you are counting events, standard signals will silently lose some; use real-time signals when every occurrence must be seen.
#include <signal.h>
typedef void (*sighandler_t)(int); // a handler: takes the signo, returns void
int raise(int sig); // send `sig` to the calling process; 0 on success
int kill(pid_t pid, int sig); // send `sig` to process `pid`; 0 on success, -1 + errno on error
sighandler_t signal(int signo, sighandler_t handler); // set disposition; simple but portability-quirky
int sigaction(int signo, const struct sigaction *act,
struct sigaction *oldact); // set disposition; 0 on success, -1 + errno; PREFERRED
char *strsignal(int sig); // human-readable name, e.g. "Interrupt" (do not free)
Special disposition values usable with signal()/sigaction:
SIG_DFL — restore the signal's default action.SIG_IGN — ignore the signal.struct sigaction key fields:
| Field | Meaning |
|---|---|
sa_handler |
Function to run, or SIG_DFL / SIG_IGN |
sa_mask |
Extra signals to block while the handler runs (build with sigemptyset/sigaddset) |
sa_flags |
Options; SA_RESTART auto-restarts syscalls interrupted by the signal |
Notes: sigaction returns -1 and sets errno (e.g. EINVAL for SIGKILL/SIGSTOP). Always zero the struct with memset before filling it — it has fields not shown here. Nothing here needs freeing or closing; strsignal returns a pointer to static/internal storage you must not free.
A signal is a software interrupt that the kernel delivers to a process.
It is the simplest form of inter-process communication (IPC) that Unix offers. There is no shared memory and no socket. There is just a number, such as SIGINT = 2 or SIGTERM = 15, plus the event of delivering it.
Signals have three sources:
Ctrl+C sends SIGINT. Ctrl+\ sends SIGQUIT.kill(pid, signo) or the shell's kill command.SIGSEGV. An integer divide-by-zero triggers SIGFPE.Each signal has a default action. Most of the time it is "terminate the process." Sometimes it is "terminate and dump core," and sometimes it is "ignore."
A process can override the default by installing a signal handler (see the next lesson).
Two signals, SIGKILL and SIGSTOP, cannot be caught, blocked, or ignored. They are the kernel's escape hatch for forcing a process to stop no matter what.
/* signals-intro: a hermetic demo. No terminal input, no other process, no root.
* We send signals to OURSELVES with raise()/kill(getpid(), ...) and watch what
* happens. This is safe to run in a grader or CI. */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
/* The ONLY safe way for a handler to talk to main: a flag of this exact type.
* volatile stops the compiler from caching it; sig_atomic_t is read/written
* in one uninterruptible step. */
static volatile sig_atomic_t got_usr1 = 0;
static volatile sig_atomic_t last_signo = 0;
/* An async-signal handler. Do almost nothing here: just record what arrived. */
static void on_signal(int signo) {
last_signo = signo;
if (signo == SIGUSR1) got_usr1 = 1;
}
/* Install a handler for `signo` using sigaction (portable, well-defined). */
static void install(int signo) {
struct sigaction sa;
memset(&sa, 0, sizeof sa); /* zero every field first */
sa.sa_handler = on_signal; /* our function */
sigemptyset(&sa.sa_mask); /* block no extra signals during handler */
sa.sa_flags = SA_RESTART; /* auto-restart interrupted syscalls */
if (sigaction(signo, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
}
int main(void) {
printf("pid = %d\n", (int)getpid());
/* 1. Catch SIGUSR1 and prove the handler ran. */
install(SIGUSR1);
printf("raising SIGUSR1...\n");
raise(SIGUSR1); /* == kill(getpid(), SIGUSR1) */
printf("handler saw signo=%d, got_usr1=%d (%s)\n",
(int)last_signo, (int)got_usr1, strsignal(last_signo));
/* 2. Ask the kernel to IGNORE a signal, then send it: nothing happens. */
signal(SIGTERM, SIG_IGN);
printf("SIGTERM set to ignore; raising it...\n");
raise(SIGTERM); /* would normally terminate us */
printf("still alive: an ignored signal has no effect\n");
/* 3. Prove SIGKILL cannot be caught. sigaction must FAIL for it. */
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = on_signal;
if (sigaction(SIGKILL, &sa, NULL) == -1)
printf("cannot install handler for SIGKILL: %s\n", strerror(errno));
printf("done\n");
return 0;
}
raise/kill(getpid(), ...), so it needs no terminal input, no second process, and no root. Safe for a grader.<signal.h> for signals, <string.h> for memset/strsignal/strerror, <unistd.h> for getpid, <errno.h> for errno.main. The type is volatile sig_atomic_t: volatile forbids the compiler from caching the value in a register (the handler can change it at any instant), and sig_atomic_t guarantees reads/writes happen in one uninterruptible step. This exact type is the only portable way for a handler to communicate with the rest of the program.on_signal, the handler. It does the bare minimum: record which signal arrived and set a flag. Doing more (calling printf, malloc, etc.) from a handler is unsafe; that discipline is covered later.install() wraps sigaction. memset zeroes the whole struct first; sa_handler points at our function; sigemptyset(&sa.sa_mask) says "block no extra signals during the handler"; SA_RESTART asks the kernel to auto-restart syscalls the signal interrupts. If sigaction returns -1, we report and exit.SIGUSR1 handler, then raise(SIGUSR1) (identical to kill(getpid(), SIGUSR1)). By the next line the handler has already run, so got_usr1 is 1 and strsignal prints the human name.SIGTERM to SIG_IGN, then raise it. Normally SIGTERM terminates; ignored, it has no effect, and the program keeps running — proving the ignore disposition works.SIGKILL. The kernel refuses; sigaction returns -1 and sets errno to EINVAL, which we print — hard proof that SIGKILL is uncatchable.1. Assuming standard signals queue like a message queue.
// send 100 SIGUSR1 fast; expect the counter to reach 100
for (int i = 0; i < 100; i++) kill(pid, SIGUSR1);
Why it breaks: standard signals coalesce — a pending signal is one bit, not a count. Many of the 100 collapse into far fewer deliveries, so the counter under-reports.
// use a real-time signal, which queues each delivery separately
for (int i = 0; i < 100; i++) kill(pid, SIGRTMIN);
2. Forgetting SIGKILL/SIGSTOP are uncatchable.
signal(SIGKILL, my_cleanup); // silently useless; cleanup never runs
Why it breaks: the kernel never lets a handler run for SIGKILL (or SIGSTOP). Your cleanup is dead code.
signal(SIGTERM, my_cleanup); // catch the *catchable* shutdown signal instead
3. Using the numeric value instead of the name.
kill(pid, 9); // "9 is always KILL, right?"
Why it breaks: only a handful of numbers are stable across platforms; most differ, so numeric literals are non-portable and unreadable.
kill(pid, SIGKILL); // names are portable and self-documenting
4. Sharing an ordinary variable between handler and main.
int flag = 0; // plain int, no volatile
void h(int s){ (void)s; flag = 1; }
while (!flag) { /* work */ } // compiler may cache flag; loop never ends
Why it breaks: without volatile sig_atomic_t, the compiler may hoist flag into a register and never re-read it, and the update may not be atomic.
static volatile sig_atomic_t flag = 0;
void h(int s){ (void)s; flag = 1; }
cat /proc/<pid>/status and read SigBlk, SigIgn, SigCgt (blocked/ignored/caught bitmasks). Decode a mask with SigCgt=0000000000010000 → bit for signal 17.strace -e trace=signal ./prog (Linux) prints every signal sent and received, e.g. --- SIGUSR1 {si_signo=SIGUSR1, ...} ---. Invaluable for "who signalled me?"Segmentation fault (core dumped); programmatically, WIFSIGNALED(status) and WTERMSIG(status) on a wait()ed child tell you which signal ended it.ulimit -c unlimited, reproduce the crash, then gdb ./prog core and bt to see where SIGSEGV/SIGABRT fired.info signals lists dispositions; handle SIGUSR1 nostop noprint pass stops gdb from intercepting a signal you want your handler to receive.getpid() at startup (as the demo does), then from another shell kill -USR1 <pid> and watch whether your handler reacts.Signals introduce asynchrony hazards, not the usual heap bugs:
main, including in the middle of updating a data structure. If both touch the same non-atomic state, you get a data race and undefined behaviour. Restrict handler↔main communication to a single volatile sig_atomic_t flag.printf, malloc, and most of libc are not on the safe list (POSIX signal-safety(7)); calling them from a handler can deadlock or corrupt state if the signal arrived while the same function was mid-execution. The demo keeps its handler to plain assignments for this reason.errno can be clobbered. If a handler calls a function that sets errno, it can overwrite the value main was about to check. A careful handler saves and restores errno.read, accept) hit by a signal returns -1/EINTR unless you set SA_RESTART. Code that ignores EINTR can misbehave.SIGHUP triggers a graceful config reload without dropping connections; SIGUSR1 reopens log files after rotation.SIGTERM and waits TimeoutStopSec; if the service has not exited, it escalates to the uncatchable SIGKILL. Well-behaved services catch SIGTERM to flush and close cleanly.SIGSEGV/SIGTRAP before the kernel's default terminates the process, so you can inspect the crash site.SIGTERM/SIGINT to checkpoint or save a recovery file before exiting.Best practice: install handlers for SIGINT and SIGTERM in any long-running program; prefer sigaction over signal; keep handlers tiny (set a flag, return) and do the real cleanup in the main loop; and never assume you can catch SIGKILL — design so that even an abrupt kill leaves recoverable state on disk.
SIGINT, SIGSEGV, SIGCHLD, SIGSTOP, SIGCONT. Then confirm against man 7 signal.SIGTERM and proving it has no effect, restore the default with signal(SIGTERM, SIG_DFL), then raise(SIGTERM) and observe the process now terminates. Note where "done" no longer prints.fork()s a child which raises SIGSEGV on itself; in the parent, wait() and use WIFSIGNALED/WTERMSIG + strsignal to print exactly which signal ended the child.main block SIGUSR1 (with sigprocmask), raise it several times, unblock it, and count how many times a SIGUSR1 handler runs. Explain why the count is 1, not N.SIGRTMIN instead of SIGUSR1 and show that the handler runs once per send. Summarise the practical difference between standard and real-time signals in two sentences.SIGINT, not 2).SIGINT), other processes (kill), and the kernel reporting faults (SIGSEGV, SIGFPE).kill ends a program.SIG_IGN), or a caught handler — except SIGKILL (9) and SIGSTOP (19), which can never be caught, blocked, or ignored.SIGRTMIN+) queue each delivery. Prefer sigaction over signal, and keep handlers tiny.