Beej's Guide to Unix IPC — Chapter 3

Unix Signals

The simplest IPC: one process taps another on the shoulder and says "hey, this happened."

Prerequisites: fork() + Basic C. That's it.
8
Chapters
3+
Simulations
0
Assumed Knowledge

Chapter 0: Why Signals?

You have two processes running. One of them needs to tell the other: "stop what you're doing" or "time's up" or "your child just died." The processes don't share memory, and you don't want to set up a pipe just for a simple notification.

Enter signals: lightweight, asynchronous notifications from one process to another (or from the kernel to a process). When you hit Ctrl+C in a terminal, you're sending the SIGINT signal. When you type kill -9, you're sending SIGKILL. Signals are the simplest form of IPC — no data is transmitted, just "this event happened."

The mental model: A signal is like a tap on the shoulder. The receiving process can ignore it, handle it with a custom function, or take the default action (which is often "die"). You don't pass data — just the signal number.
Signal Delivery

Click a signal button to send it to the running process. Watch how the process responds based on its installed handler.

What kind of information does a signal carry?

Chapter 1: What's a Signal?

A signal is an integer. Each number has a name and a default behavior. When a process receives a signal, one of three things happens:

Option 1: Default
Run the default handler (usually: terminate)
or
Option 2: Catch
Run your custom signal handler function
or
Option 3: Ignore
Set handler to SIG_IGN — signal is discarded

Two signals cannot be caught or ignored: SIGKILL (signal 9) and SIGSTOP. These are the kernel's escape hatches — they guarantee you can always kill or freeze a process, no matter how badly it's behaving.

Key insight: When you type kill pid in the shell (without -9), you're sending SIGTERM, not SIGKILL. The process can catch SIGTERM and clean up. Only kill -9 sends the uncatchable SIGKILL. Always try SIGTERM first.

Signals can be sent with the kill() system call (confusingly named — it sends any signal, not just deadly ones) or the raise() function to signal yourself.

c
kill(child_pid, SIGTERM);  /* send SIGTERM to child */
raise(SIGUSR1);            /* send SIGUSR1 to myself */
Which signal CANNOT be caught or ignored?

Chapter 2: Catching Signals

To catch a signal, you write a signal handler — a function that gets called automatically when the signal arrives. The process is running along normally, the signal arrives, execution jumps to your handler, and then returns to where it left off.

c
void sigint_handler(int sig) {
    write(1, "Caught SIGINT!\n", 15);
}

int main(void) {
    struct sigaction sa;
    sa.sa_handler = sigint_handler;
    sa.sa_flags = 0;
    sigemptyset(&sa.sa_mask);

    sigaction(SIGINT, &sa, NULL);
    /* Now Ctrl+C runs sigint_handler instead of killing us */

    while(1) pause(); /* wait for signals */
}

Notice we use write() in the handler, not printf(). That's not an accident — we'll explain why in the Async Safety chapter.

The handler signature: Every signal handler takes a single int argument: the signal number that triggered it. This lets you use the same handler for multiple signals and switch on which one arrived.
What is a signal handler?

Chapter 3: sigaction() — The Right Way

There's an older function called signal() for catching signals, but Beej (and POSIX) strongly recommend sigaction() instead. It gives you more control and more reliable behavior.

The struct sigaction has three important fields:

FieldPurpose
sa_handlerPointer to your handler function (or SIG_IGN / SIG_DFL)
sa_maskSet of signals to block while this handler runs
sa_flagsFlags to modify behavior (e.g., SA_RESTART)

The sa_mask is a signal set. While your handler is running, any signals in this mask are blocked (queued until the handler returns). You build the set with sigemptyset(), sigaddset(), etc.

SA_RESTART: When a signal interrupts a system call like read(), the call normally returns with error EINTR. Setting sa_flags = SA_RESTART tells the kernel to automatically retry the interrupted call instead. This saves you from writing retry loops everywhere.
c
struct sigaction sa;
sa.sa_handler = my_handler;
sa.sa_flags = SA_RESTART;      /* auto-restart interrupted calls */
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGTERM); /* block SIGTERM during handler */

sigaction(SIGINT, &sa, NULL);
What does the SA_RESTART flag do?

Chapter 4: Async Safety — The Handler's Limits

Signal handlers are tricky. They can fire at any moment — even in the middle of a malloc() or a printf(). If your handler calls printf() while the main program is also inside printf(), you get undefined behavior. Corruption. Crashes.

This is why only a specific list of functions are async-signal-safe — safe to call from within a signal handler. The big ones you'll use:

Safe to call: write(), _exit(), signal(), sigaction(), fork(), read(), close(), open(), wait(), raise()
NOT safe: printf(), malloc(), free(), fopen(), exit() (use _exit() instead)

The safest pattern is to set a global flag in the handler and check it in your main loop. The flag must be declared volatile sig_atomic_t to ensure correct behavior:

c
volatile sig_atomic_t got_signal = 0;

void handler(int sig) {
    got_signal = 1;  /* just set a flag */
}

int main() {
    /* ... install handler ... */
    while (!got_signal) {
        printf("Working...\n"); /* safe: in main, not handler */
        sleep(1);
    }
    printf("Signal received! Cleaning up.\n");
}
volatile sig_atomic_t: volatile tells the compiler not to optimize away reads of this variable (since it changes asynchronously). sig_atomic_t guarantees the read/write is atomic — the signal can't corrupt it mid-access.
Why can't you call printf() inside a signal handler?

Chapter 5: Common Signals

Unix defines dozens of signals. Here are the ones you'll encounter most:

SignalDefault ActionCatchable?When It's Sent
SIGINTTerminateYesUser presses Ctrl+C
SIGTERMTerminateYeskill command (default signal)
SIGKILLTerminateNokill -9 — the nuclear option
SIGSTOPStopNoFreezes process (Ctrl+Z sends SIGTSTP)
SIGCONTContinueYesResumes a stopped process
SIGCHLDIgnoreYesChild terminated or stopped
SIGPIPETerminateYesWrote to a pipe with no reader
SIGUSR1TerminateYesUser-defined — whatever you want
SIGUSR2TerminateYesUser-defined — whatever you want
SIGSEGVCore dumpYes*Invalid memory access
SIGUSR1 and SIGUSR2: These two are reserved for your own use. They have no predefined meaning. Beej suggests using them for things like "advance to next track" in a CD player. You give them meaning through your handler.
What signal does Ctrl+C send to a foreground process?

Chapter 6: Signal Simulator

Here's the big interactive demo. You control multiple processes and can send signals between them. Watch how each process responds based on its installed handlers.

Multi-Process Signal Simulator

Select a source process and a target process, then choose a signal to send. Processes with custom handlers will catch signals; others take the default action.

Source Target
If a process has NOT installed a handler for SIGTERM, what happens when it receives SIGTERM?

Chapter 7: Beyond Signals

Signals are powerful for notifications but limited for data transfer. You can't send a string or a struct through a signal — just a number. For real data exchange, you need pipes, message queues, or shared memory.

FeatureSignalsPipes / MQs
DataSignal number onlyArbitrary bytes / structures
DirectionOne-way notificationOne-way or two-way
AsyncYes (interrupts execution)Usually blocking
ReliabilitySignals can be lost if not queuedData is buffered
ComplexityVery simpleMore setup required
Use signals for: graceful shutdown (SIGTERM), child death notification (SIGCHLD), timer events (SIGALRM), and custom notifications (SIGUSR1/2). Use pipes and queues for everything else.

Beej also mentions topics we haven't covered: realtime signals, signal masks with sigprocmask(), mixing signals with threads, and longjmp() from handlers. These are advanced topics for when you need them.

"Have you ever typed 'kill -9 nnnn' to kill a runaway process? You were sending it SIGKILL." — Beej

What is the main limitation of signals as an IPC mechanism?