Interrupts for your process

Real-World Analogy

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.

Python — signal module
import signal, time

def handler(signum, frame):
    print("Caught SIGINT!")

signal.signal(signal.SIGINT, handler)
while True:
    time.sleep(1)   # Ctrl+C triggers handler
C — sigaction (preferred API)
#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 safe in a signal handler

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

SignalNumberDefault actionCause
SIGINT2TerminateCtrl+C from keyboard
SIGTERM15TerminateDefault signal from kill command — graceful shutdown
SIGKILL9Terminate (forced)Cannot be caught or ignored — immediate kill
SIGSEGV11Core dump + terminateSegmentation fault (invalid memory access)
SIGCHLD17IgnoredChild process stopped or exited
SIGALRM14TerminateTimer set by alarm() expired
SIGHUP1TerminateControlling terminal closed
SIGPIPE13TerminateWrite 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 */
Key rule: The signal handler function signature is always 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);
}
Critical distinction: When using 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

FieldTypeMeaning
si_signointSignal number (e.g. SIGSEGV = 11)
si_codeintCause code — SEGV_MAPERR (unmapped address) or SEGV_ACCERR (permission violation)
si_addrvoid *Faulting memory address that triggered the segfault
si_pidpid_tSending process PID (for signals sent via kill())
si_valuesigval_tAttached value for real-time signals (sigqueue())
Async-signal-safe functions only

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

Example 1 — SIGINT handler using sigaction (from lecture) Week 5 Lecture — Pages 23-26
#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;
}
Sample run (user presses Ctrl+C)
Now we wait... (press Ctrl+C)
^C
Oh.. you didn't like waiting
Program terminated
Example 2 — Ignoring a signal and restoring default with SIG_IGN / SIG_DFL Week 5 Lecture + Wk7A Tutorial
#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;
}
Behavior
SIGINT ignored. Ctrl+C has no effect for 3 seconds.
Default restored. Ctrl+C will now terminate the process.
Example 3 — Using write() safely inside a signal handler Wk7A Tutorial — async-signal-safe functions
#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;
}
On Ctrl+C
^C
Caught SIGINT — use write(), not printf()!
Example 4 — kill() to send a signal to another process Week 5 Lecture — Page 19
#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> */
Output (default SIGUSR1 terminates the process)
My PID is 12345
Sending SIGUSR1 to myself...
User defined signal 1

Practice problems with solutions

P1 — Write a SIGINT handler that counts how many times Ctrl+C is pressed Week 5 Lecture + Wk7A Tutorial

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;
}
Key points: 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.
P2 — Identify which of these operations is NOT safe inside a signal handler Wk7A Tutorial — async-signal-safe

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

printf("hi\n") — NOT safe. printf() uses internal locks and a buffer. If interrupted mid-printf, calling it again deadlocks or corrupts the buffer.

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.
P3 — Predict the output: what happens when SIGKILL is sent? Week 5 Lecture — Page 18

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;
}
handle_kill is never called. SIGKILL (signal 9) cannot be caught, blocked, or ignored. The 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.
P4 — Why is volatile needed for sig_atomic_t? Week 5 Lecture — Pages 21-22

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.

The problem: The C compiler is allowed to optimize a loop like 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.
P5 — What is the difference between signal() and sigaction()? Week 5 Lecture — Pages 20-26

List two concrete advantages of sigaction() over the older signal() function. When would you still see signal() in real code?

Advantage 1 — Portability: The behavior of 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

Card 1 of 11
Question — click to flip
Answer
Click card to flip • Use buttons to navigate

Test your understanding

Topic 20 Quiz — Signals Score: 0 / 7
1
Which signal is sent when you press Ctrl+C in the terminal?LO2
multiple choice
2
True or False: SIGKILL can be caught and handled by a user-defined signal handler.LO2
true / false
3
Why must you use volatile sig_atomic_t for a flag shared between a signal handler and the main program?LO2
multiple choice
4
Which of these is safe to call from inside a signal handler?LO2
multiple choice
5
Fill in the blank: signal(SIGINT, ________); causes SIGINT (Ctrl+C) to be silently ignored.LO2
fill in the blank
6
What is the correct signature for a signal handler function?LO2
multiple choice
7
A SIGSEGV handler reads info->si_code == SEGV_MAPERR. What does this tell you about the fault?LO2
multiple choice
0/6
Quiz complete!