The simplest IPC: one process taps another on the shoulder and says "hey, this happened."
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."
Click a signal button to send it to the running process. Watch how the process responds based on its installed handler.
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:
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.
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 */
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.
int argument: the signal number that triggered it. This lets you use the same handler for multiple signals and switch on which one arrived.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:
| Field | Purpose |
|---|---|
| sa_handler | Pointer to your handler function (or SIG_IGN / SIG_DFL) |
| sa_mask | Set of signals to block while this handler runs |
| sa_flags | Flags 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_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);
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:
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 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.Unix defines dozens of signals. Here are the ones you'll encounter most:
| Signal | Default Action | Catchable? | When It's Sent |
|---|---|---|---|
| SIGINT | Terminate | Yes | User presses Ctrl+C |
| SIGTERM | Terminate | Yes | kill command (default signal) |
| SIGKILL | Terminate | No | kill -9 — the nuclear option |
| SIGSTOP | Stop | No | Freezes process (Ctrl+Z sends SIGTSTP) |
| SIGCONT | Continue | Yes | Resumes a stopped process |
| SIGCHLD | Ignore | Yes | Child terminated or stopped |
| SIGPIPE | Terminate | Yes | Wrote to a pipe with no reader |
| SIGUSR1 | Terminate | Yes | User-defined — whatever you want |
| SIGUSR2 | Terminate | Yes | User-defined — whatever you want |
| SIGSEGV | Core dump | Yes* | Invalid memory access |
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.
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.
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.
| Feature | Signals | Pipes / MQs |
|---|---|---|
| Data | Signal number only | Arbitrary bytes / structures |
| Direction | One-way notification | One-way or two-way |
| Async | Yes (interrupts execution) | Usually blocking |
| Reliability | Signals can be lost if not queued | Data is buffered |
| Complexity | Very simple | More setup required |
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