Linux System Programming · beginner · ~8 min

What Are Signals in C and Linux?

- 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.

Overview

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."

Why it matters

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.

Core concepts

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.

Where signals come from

There are exactly three sources:

  1. You, at the terminal. The line discipline translates keystrokes into signals sent to the foreground process group. Ctrl+C sends SIGINT, Ctrl+\ sends SIGQUIT, Ctrl+Z sends SIGTSTP.
  2. Another process. 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.
  3. The kernel itself. When your program does something illegal, the kernel synchronously delivers a fault signal: a bad memory access is 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)

The default action

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. kill with no number sends SIGTERM (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 a SIGTERM handler or set the disposition to ignore.

Catching, ignoring, and the three dispositions

For most signals a process can choose one of three dispositions:

  • Default — let the kernel do the standard thing.
  • Ignore (SIG_IGN) — the signal is discarded on arrival.
  • Catch — the kernel runs a handler function you installed with signal() or, preferably, sigaction(). (Writing handlers is the next lesson; here you only need to know the disposition exists.)

The two uncatchable signals

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 vs. real-time signals

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 (SIGRTMINSIGRTMAX) 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.

Syntax notes

#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_IGNignore 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.

Lesson

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.

Where signals come from

Signals have three sources:

  1. You, through the terminal. Ctrl+C sends SIGINT. Ctrl+\ sends SIGQUIT.
  2. Another process, through kill(pid, signo) or the shell's kill command.
  3. The kernel itself, when your program does something illegal. A bad pointer dereference triggers SIGSEGV. An integer divide-by-zero triggers SIGFPE.

Default actions

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).

The two exceptions

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.

Code examples

/* 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;
}

Line by line

  • Lines 1–3 — a comment stating the demo is hermetic: it signals only itself via raise/kill(getpid(), ...), so it needs no terminal input, no second process, and no root. Safe for a grader.
  • Lines 4–9 — headers: <signal.h> for signals, <string.h> for memset/strsignal/strerror, <unistd.h> for getpid, <errno.h> for errno.
  • Lines 14–15 — the flags shared between handler and 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.
  • Lines 18–21on_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.
  • Lines 24–34install() 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.
  • Line 37 — prints our own PID, so you could signal it from another shell if you wanted.
  • Lines 40–44 — Demo 1: install a 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.
  • Lines 47–50 — Demo 2: set 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.
  • Lines 53–57 — Demo 3: attempt to install a handler for SIGKILL. The kernel refuses; sigaction returns -1 and sets errno to EINVAL, which we print — hard proof that SIGKILL is uncatchable.
  • Line 59 — reaching "done" shows we survived a signal that normally kills, because we chose to ignore it.

Common mistakes

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; }

Debugging tips

  • See a program's dispositions live: cat /proc/<pid>/status and read SigBlk, SigIgn, SigCgt (blocked/ignored/caught bitmasks). Decode a mask with SigCgt=0000000000010000 → bit for signal 17.
  • Trace signal delivery: strace -e trace=signal ./prog (Linux) prints every signal sent and received, e.g. --- SIGUSR1 {si_signo=SIGUSR1, ...} ---. Invaluable for "who signalled me?"
  • After a crash, identify the killer signal: the shell shows Segmentation fault (core dumped); programmatically, WIFSIGNALED(status) and WTERMSIG(status) on a wait()ed child tell you which signal ended it.
  • Enable and inspect core files: ulimit -c unlimited, reproduce the crash, then gdb ./prog core and bt to see where SIGSEGV/SIGABRT fired.
  • In gdb, control signal handling: info signals lists dispositions; handle SIGUSR1 nostop noprint pass stops gdb from intercepting a signal you want your handler to receive.
  • Quick printf check: print getpid() at startup (as the demo does), then from another shell kill -USR1 <pid> and watch whether your handler reacts.

Memory safety

Signals introduce asynchrony hazards, not the usual heap bugs:

  • Handlers run at arbitrary points. A handler can fire between any two instructions of 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.
  • Only async-signal-safe functions are legal in a handler. 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.
  • Coalescing loses events. Treat a standard-signal flag as "at least one arrived," never as a count.
  • Interrupted syscalls. A blocking call (read, accept) hit by a signal returns -1/EINTR unless you set SA_RESTART. Code that ignores EINTR can misbehave.

Real-world uses

  • nginx / Apache: SIGHUP triggers a graceful config reload without dropping connections; SIGUSR1 reopens log files after rotation.
  • systemd service shutdown: sends 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.
  • Debuggers (gdb, lldb): intercept SIGSEGV/SIGTRAP before the kernel's default terminates the process, so you can inspect the crash site.
  • Databases and editors: catch 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.

Practice tasks

  1. Predict the disposition. Without running anything, write down the default action (Term / Core / Ign / Stop / Cont) for each of SIGINT, SIGSEGV, SIGCHLD, SIGSTOP, SIGCONT. Then confirm against man 7 signal.
  2. Ignore then restore. Extend the demo: after ignoring 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.
  3. Report the killer. Write a program that 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.
  4. Prove coalescing. Have 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.
  5. Real-time contrast. Repeat task 4 using 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.

Summary

  • A signal is a kernel-delivered software interrupt identified by a number (use the name in code, e.g. SIGINT, not 2).
  • Signals come from three places: the terminal (Ctrl+C → SIGINT), other processes (kill), and the kernel reporting faults (SIGSEGV, SIGFPE).
  • Each signal has a default action — Term, Core, Ign, Stop, or Cont; most default to terminate, which is why an unhandled kill ends a program.
  • A process may set a signal's disposition to default, ignore (SIG_IGN), or a caught handler — except SIGKILL (9) and SIGSTOP (19), which can never be caught, blocked, or ignored.
  • Standard signals (1–31) coalesce (one pending bit); real-time signals (SIGRTMIN+) queue each delivery. Prefer sigaction over signal, and keep handlers tiny.

Practice with these exercises