Signals
Asynchronous notifications to processes — signal handlers, sigaction, common signals, and signal safety.
Interrupts for your process
A signal is like a tap on the shoulder from the operating system. Your program is doing its thing — looping, reading, calculating — and suddenly the OS interrupts it, forces it to run a special function (your signal handler), and then lets it continue from where it was. You cannot predict exactly when the tap will happen. This is what asynchronous means.
In Python, you can use the signal module: signal.signal(signal.SIGINT, handler) — this is actually a thin wrapper over the same POSIX mechanism you use in C. Java handles signals less directly; it exposes shutdown hooks for SIGTERM but not the full signal model. In C, you have direct access to the kernel signal API.
Where do signals come from? Three sources: (1) the keyboard — pressing Ctrl+C sends SIGINT to the foreground process, (2) the kernel — when your program dereferences a null pointer, the kernel sends SIGSEGV, (3) another process — using the kill(pid, sig) system call or the kill shell command.
Default actions: If you do not install a handler, each signal has a default action: most terminate the process, some also dump a core file (for debugging), SIGSTOP suspends, SIGCONT resumes. You can override the default by registering a function, or suppress it with SIG_IGN.
The critical constraint — async-signal safety: Your signal handler can be called at any instant, even while you are inside malloc() or printf(). Those functions are not reentrant — calling them from a handler while they are already running can corrupt internal state. Only async-signal-safe functions are permitted in handlers. The safe rule: set a volatile flag in the handler, do all real work in your main loop.
import signal, time
def handler(signum, frame):
print("Caught SIGINT!")
signal.signal(signal.SIGINT, handler)
while True:
time.sleep(1) # Ctrl+C triggers handler
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t got_int = 0;
void handler(int sig) { got_int = 1; }
int main(void) {
struct sigaction act = {0};
act.sa_handler = handler;
sigaction(SIGINT, &act, NULL);
while (!got_int) usleep(10);
}
printf() is not async-signal-safe because it uses internal locks and buffers. Calling it from a signal handler while the main thread is inside printf() causes undefined behavior. Use write() (fd 1 or 2) instead — it is async-signal-safe. Best practice: set a flag in the handler, check the flag in the main loop.
signal(), sigaction(), kill(), volatile sig_atomic_t
Common signals table
| Signal | Number | Default action | Cause |
|---|---|---|---|
SIGINT | 2 | Terminate | Ctrl+C from keyboard |
SIGTERM | 15 | Terminate | Default signal from kill command — graceful shutdown |
SIGKILL | 9 | Terminate (forced) | Cannot be caught or ignored — immediate kill |
SIGSEGV | 11 | Core dump + terminate | Segmentation fault (invalid memory access) |
SIGCHLD | 17 | Ignored | Child process stopped or exited |
SIGALRM | 14 | Terminate | Timer set by alarm() expired |
SIGHUP | 1 | Terminate | Controlling terminal closed |
SIGPIPE | 13 | Terminate | Write to pipe with no reader |
/* ── signal() — old API, still commonly seen ── */ /* #include <signal.h> */ typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); /* handler can be: your function, SIG_IGN (ignore), SIG_DFL (restore default) */ /* ── sigaction() — preferred, portable, more control ── */ int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); /* returns 0 on success, -1 on error */ struct sigaction { void (*sa_handler)(int); /* handler function — same signature as signal() */ sigset_t sa_mask; /* signals to block during handler execution */ int sa_flags; /* SA_RESTART, SA_SIGINFO, etc. */ }; /* ── kill() — send a signal to another process ── */ int kill(pid_t pid, int sig); /* pid: target process; sig: signal number or name */ /* ── volatile sig_atomic_t — correct shared flag ── */ volatile sig_atomic_t flag = 0; /* volatile: tells compiler to always read from memory (never cache in register) */ /* sig_atomic_t: an integer type that can be written atomically by the kernel */
void handler(int signo). The signo parameter tells you which signal was received — useful when one handler covers multiple signals.
SA_SIGINFO — receiving extra fault information (Week 8 coredump handler)
SA_SIGINFO is a flag for sa_flags that changes the signal handler signature from one parameter to three. When set, you must assign your handler to sa.sa_sigaction (not sa.sa_handler), and the handler receives a siginfo_t * pointer containing rich information about why the signal was delivered. This is essential for writing a useful SIGSEGV handler — instead of just knowing a segfault occurred, you can inspect exactly which address was accessed and whether it was unmapped or a permissions violation.
/* ── SA_SIGINFO handler — 3-parameter signature ── */ void sigsegv_handler(int sig, siginfo_t *info, void *context) { /* info->si_addr — the memory address that caused the fault */ /* info->si_code — WHY it faulted: */ /* SEGV_MAPERR — address not mapped (NULL deref, use-after-free, etc.) */ /* SEGV_ACCERR — address IS mapped but wrong permissions (write to RO) */ write(STDERR_FILENO, "Segfault!\n", 10); /* async-signal-safe — OK */ /* printf() is NOT async-signal-safe — use write() in signal handlers */ if (info->si_code == SEGV_MAPERR) { /* NULL pointer deref, use-after-free, stack overflow, etc. */ } else if (info->si_code == SEGV_ACCERR) { /* Writing to read-only memory (string literal, mprotect'd page) */ } _exit(1); /* do NOT return from SIGSEGV handler — undefined behavior */ } int main(void) { struct sigaction sa = {0}; sa.sa_sigaction = sigsegv_handler; /* sa_sigaction, NOT sa_handler */ sa.sa_flags = SA_SIGINFO; /* REQUIRED to enable 3-arg handler */ sigemptyset(&sa.sa_mask); sigaction(SIGSEGV, &sa, NULL); }
SA_SIGINFO, you assign to sa.sa_sigaction (the 3-arg member), not sa.sa_handler. Mixing them is undefined behavior. The context parameter is a ucontext_t * — rarely needed in coursework but available for inspecting CPU registers at fault time.
Key siginfo_t fields
| Field | Type | Meaning |
|---|---|---|
si_signo | int | Signal number (e.g. SIGSEGV = 11) |
si_code | int | Cause code — SEGV_MAPERR (unmapped address) or SEGV_ACCERR (permission violation) |
si_addr | void * | Faulting memory address that triggered the segfault |
si_pid | pid_t | Sending process PID (for signals sent via kill()) |
si_value | sigval_t | Attached value for real-time signals (sigqueue()) |
Never call printf, malloc, free, or any non-async-signal-safe function inside a signal handler — including SIGSEGV handlers. Only use write(), _exit(), and functions listed in man 7 signal-safety. Calling unsafe functions risks deadlock or heap corruption.
Complete programs you can compile and run
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
/* volatile: always read from memory, never cache in register */
/* sig_atomic_t: safe to write from signal handler (atomic) */
volatile sig_atomic_t interrupted = 0;
/* Signal handler — called asynchronously when SIGINT arrives */
/* Signature MUST be: void handler(int signo) */
void impatient(int signo) {
(void)signo; /* suppress "unused parameter" warning */
interrupted = 1; /* just set a flag — no printf! */
}
int main(void) {
/* sigaction is preferred over signal() */
struct sigaction act = { 0 }; /* zero-initialize */
act.sa_handler = impatient;
sigaction(SIGINT, &act, NULL); /* NULL = don't care about old handler */
printf("Now we wait... (press Ctrl+C)\n");
/* Main loop checks the flag — handler only sets the flag */
while (!interrupted) {
usleep(10000); /* 10ms sleep */
}
printf("Oh.. you didn't like waiting\n");
printf("Program terminated\n");
return 0;
}
^C
Oh.. you didn't like waiting
Program terminated
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
/* Ignore SIGINT — Ctrl+C will do nothing */
signal(SIGINT, SIG_IGN);
printf("SIGINT ignored. Ctrl+C has no effect for 3 seconds.\n");
sleep(3);
/* Restore the default action — Ctrl+C will terminate again */
signal(SIGINT, SIG_DFL);
printf("Default restored. Ctrl+C will now terminate the process.\n");
sleep(5); /* if you press Ctrl+C now, process terminates */
printf("Done.\n");
return 0;
}
Default restored. Ctrl+C will now terminate the process.
#include <signal.h>
#include <unistd.h>
#include <string.h>
/* write() is async-signal-safe. printf() is NOT. */
void handle_sigint(int sig) {
/* Safe: write() is in the async-signal-safe list */
const char msg[] = "\nCaught SIGINT — use write(), not printf()!\n";
write(STDERR_FILENO, msg, strlen(msg));
/* _exit() is safe. exit() is NOT (it flushes stdio buffers). */
_exit(1);
}
int main(void) {
struct sigaction act = { 0 };
act.sa_handler = handle_sigint;
sigaction(SIGINT, &act, NULL);
/* Simulate long work */
for (;;) {
usleep(100000);
}
return 0;
}
Caught SIGINT — use write(), not printf()!
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
pid_t my_pid = getpid();
printf("My PID is %d\n", my_pid);
printf("Sending SIGUSR1 to myself...\n");
/* Send SIGUSR1 to ourselves */
/* kill(pid, sig): returns 0 on success, -1 on error */
kill(my_pid, SIGUSR1);
/* SIGUSR1 default action is to terminate — so this line may not print */
printf("Still alive (would need a handler to get here)\n");
return 0;
}
/* shell equivalent: kill -USR1 <pid> or kill -10 <pid> */
Sending SIGUSR1 to myself...
User defined signal 1
Practice problems with solutions
Write a complete program that installs a SIGINT handler. The handler should count how many times SIGINT is received. After the third press of Ctrl+C, the program should exit. Use volatile sig_atomic_t for the counter.
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
volatile sig_atomic_t sigint_count = 0;
void handle_sigint(int sig) {
(void)sig;
sigint_count++;
}
int main(void) {
struct sigaction act = { 0 };
act.sa_handler = handle_sigint;
sigaction(SIGINT, &act, NULL);
printf("Press Ctrl+C three times to exit.\n");
while (1) {
if (sigint_count >= 3) {
printf("\nThree presses received. Exiting.\n");
exit(0);
}
printf("Count so far: %d\r", (int)sigint_count);
fflush(stdout);
usleep(100000);
}
return 0;
}
volatile sig_atomic_t is the correct type for a variable modified in a signal handler and read in the main loop. volatile prevents the compiler from caching the value in a register. sig_atomic_t guarantees the write is atomic. The handler does minimal work — it only increments the flag. All printing happens in the main loop.
Which of the following can you safely call from a signal handler? Explain why for each: printf("hi\n"), write(1, "hi\n", 3), malloc(100), exit(0), _exit(0), flag = 1 (where flag is volatile sig_atomic_t).
write(1, "hi\n", 3) — SAFE. write() is listed in the POSIX async-signal-safe functions. It is a simple syscall with no internal state.
malloc(100) — NOT safe. malloc() maintains the heap free-list using locks. A signal during malloc() and then malloc() again in the handler deadlocks or corrupts the heap.
exit(0) — NOT safe. exit() flushes stdio buffers (which may be partially written), calls atexit() handlers, etc. None of that is safe from a signal handler.
_exit(0) — SAFE. _exit() (with underscore) immediately terminates without flushing buffers or running handlers. It is async-signal-safe.
flag = 1 (volatile sig_atomic_t) — SAFE. Writing to a
volatile sig_atomic_t is atomic and safe by definition.
Consider this code. If another process calls kill(getpid(), SIGKILL) while the program is sleeping, what happens? Can the handle_kill function ever be called?
void handle_kill(int sig) {
printf("Caught SIGKILL!\n");
}
int main(void) {
signal(SIGKILL, handle_kill); /* attempt to catch SIGKILL */
sleep(10);
return 0;
}
signal(SIGKILL, handle_kill) call will fail silently (or return SIG_ERR) — the OS refuses to install a handler for SIGKILL. When SIGKILL is sent, the kernel immediately terminates the process. This is a fundamental Unix guarantee: there is always a way to kill a misbehaving process. The same applies to SIGSTOP — it cannot be caught or ignored either.
Explain what goes wrong if you declare a signal flag as int flag = 0 instead of volatile sig_atomic_t flag = 0. Give a concrete scenario where the program loops forever even though the signal handler sets flag = 1.
while (!flag) { usleep(10); } by reading flag once into a CPU register at the start and never re-reading it from memory. If the signal handler then writes flag = 1 to memory, the main loop still sees the stale register value (0) and loops forever.volatile tells the compiler: "always load this variable fresh from memory on every access — never cache it in a register." This ensures the main loop sees the updated value after the handler runs.sig_atomic_t ensures the write and read are atomic — the handler cannot write a partial value. On most platforms int is already atomic for simple assignments, but the C standard only guarantees atomicity for sig_atomic_t.
List two concrete advantages of sigaction() over the older signal() function. When would you still see signal() in real code?
signal() is implementation-defined in some cases (e.g. on some systems it resets to SIG_DFL after each delivery, creating a race condition). sigaction() behavior is precisely specified by POSIX.Advantage 2 — sa_mask:
sigaction() lets you specify which other signals to block while your handler is running via sa_mask. This prevents a second signal from interrupting your handler, avoiding re-entrancy issues.Where you still see signal(): Simple programs, old codebases, and code written for maximum portability across non-POSIX systems. The lecture shows both — the "deprecated" signal() first for simplicity, then the "new" sigaction() as the correct approach.
Key concepts to memorize
Test your understanding
volatile sig_atomic_t for a flag shared between a signal handler and the main program?LO2signal(SIGINT, ________); causes SIGINT (Ctrl+C) to be silently ignored.LO2info->si_code == SEGV_MAPERR. What does this tell you about the fault?LO2