Interrupts, Exceptions and Signals
Interrupt latency (time from signal to handler) and masking (temporarily disabling interrupts) are central in embedded and real-time systems.
Concept
Three Ways the CPU Leaves Normal Execution Interrupt (hardware) Devices, timers, other cores
Timer tick, network packet, key press, inter-processor interrupt Exception: fault The current instruction; restartable
Page fault, divide error, general protection fault Exception: trap syscall, breakpoint (int3) Exception: abort Unrecoverable hardware error
A device raises an interrupt line; the interrupt controller (APIC on x86, GIC on ARM, PLIC on RISC-V) forwards it to a CPU.
After the current instruction, the CPU saves the minimal state (PC, flags), switches to kernel mode and jumps through the interrupt vector/descriptor table (IDT) to the handler.
The handler acknowledges the device and does the minimum urgent work ("top half"); longer work is deferred ("bottom half": softirqs, tasklets, workqueues on Linux).
Return from interrupt (iret) restores the interrupted code.
Interrupt latency (time from signal to handler) and masking (temporarily disabling interrupts) are central in embedded and real-time systems. Polling is the alternative: repeatedly checking a device status register — simpler, wastes CPU unless events are very frequent.
The timer interrupt is what makes preemptive multitasking possible: it lets the kernel regain control and switch to another task even if the current one never makes a system call.
Signals: Software Interrupts for Processes The kernel converts events into signals delivered to processes:
Invalid memory access (the page fault couldn't be resolved) Arithmetic error (integer divide by zero) SIGTERM / SIGKILL
Termination request / unconditional kill (can't be caught) A child process changed state
Handlers run asynchronously, so they may only call async-signal-safe functions (write, _exit, setting a volatile sig_atomic_t flag) — not printf or malloc.
Why It Matters
Every crash (Segmentation fault) is an exception turned into a signal; every responsive system depends on interrupts; embedded developers write interrupt service routines daily; servers must handle SIGTERM for graceful shutdown.
Syntax
struct sigaction sa = {0};
sa.sa_handler = on_term;
sigaction(SIGTERM, &sa, NULL);
kill(pid, SIGTERM);
alarm(5);Example
12.1 CPU exceptions and timers delivered as signals 12.2 Interrupt counters kept by the kernel
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <string.h>
#include <unistd.h>
static sigjmp_buf recover;
static volatile sig_atomic_t alarms;
static void on_fault(int sig, siginfo_t *info, void *ctx)
{
(void)ctx;
/* only async-signal-safe calls belong here; printf is used for demonstration only */
printf(" caught %s (signal %d), faulting address %p\n", strsignal(sig), sig, info->si_addr);
siglongjmp(recover, 1);
}
static void on_alarm(int sig) { (void)sig; alarms++; }
int main(void)
{
struct sigaction sa = {0};
sa.sa_sigaction = on_fault;
sa.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGFPE, &sa, NULL);
/* 1. page fault on an invalid address -> CPU exception -> kernel -> SIGSEGV */
if (sigsetjmp(recover, 1) == 0) {
printf("writing through a NULL pointer...\n");
volatile int *p = NULL;
*p = 1;
}
/* 2. integer division by zero -> CPU #DE exception -> SIGFPE */
if (sigsetjmp(recover, 1) == 0) {
printf("dividing by zero...\n");
volatile int zero = 0;
volatile int r = 10 / zero;
(void)r;
}
/* 3. time
…$ awk 'NR>1 && $1 ~ /^(RES|CAL|TLB|HYP|HVS|NMI|LOC):$/ {s=0; for(i=2;i<=9;i++) s+=$i; d=""; for(i=10;i<=NF;i++) d=d" "$i; printf "%-5s %8d %s\n", $1, s, d}' /proc/interrupts
NMI: 0 Non-maskable interrupts
LOC: 0 Local timer interrupts
RES: 402 Rescheduling interrupts
CAL: 19269 Function call interrupts
TLB: 0 TLB shootdowns
HYP: 12656 Hypervisor callback interrupts
HVS: 10984 Hyper-V stimer0 interrupts
$ grep -E '^(ctxt|intr)' /proc/stat | cut -c1-40
intr 44115 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ctxt 67383Expected Result
Writing through NULL caused a page fault; the kernel found no mapping at address 0 and delivered SIGSEGV with the faulting address (nil).
Integer division by zero raised the CPU's divide-error exception (#DE), delivered as SIGFPE; the reported address is the instruction that faulted.
The alarm arrived asynchronously and woke pause().
(The handler uses printf only for demonstration; production handlers must restrict themselves to async-signal-safe calls.)
The counts are summed over the 8 logical CPUs. Because this Linux runs as a Hyper-V virtual machine (WSL2), its timer and device interrupts arrive as hypervisor interrupts (HYP, HVS) instead of local APIC timer interrupts (LOC = 0); on bare-metal Linux, LOC counts timer ticks. CAL and RES are inter-processor interrupts one core sends another (to run a function or to reschedule). ctxt is the total number of context switches since boot.
writing through a NULL pointer...
caught Segmentation fault (signal 11), faulting address (nil)
dividing by zero...
caught Floating point exception (signal 8), faulting address 0x618f514ca30d
waiting for an alarm signal...
SIGALRM delivered 1 time(s)
after raise(SIGUSR1) the counter is 2$ awk 'NR>1 && $1 ~ /^(RES|CAL|TLB|HYP|HVS|NMI|LOC):$/ {s=0; for(i=2;i<=9;i++) s+=$i; d=""; for(i=10;i<=NF;i++) d=d" "$i; printf "%-5s %8d %s\n", $1, s, d}' /proc/interrupts
NMI: 0 Non-maskable interrupts
LOC: 0 Local timer interrupts
RES: 402 Rescheduling interrupts
CAL: 19269 Function call interrupts
TLB: 0 TLB shootdowns
HYP: 12656 Hypervisor callback interrupts
HVS: 10984 Hyper-V stimer0 interrupts
$ grep -E '^(ctxt|intr)' /proc/stat | cut -c1-40
intr 44115 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ctxt 67383Common Mistakes
Doing heavy work (or calling printf/malloc) inside a signal handler or ISR.
Forgetting volatile for flags shared between an ISR/handler and the main loop.
Leaving interrupts disabled too long (missed events, latency spikes).
Trying to catch SIGKILL or SIGSTOP (impossible by design).
Recovering from SIGSEGV and continuing as if nothing happened (the demo's siglongjmp is only safe because the faulting code is trivial).
Best Practices
Keep ISRs and signal handlers short: set a flag or push to a lock-free queue, handle the rest in normal context.
Use sigaction (not signal) with explicit flags; consider signalfd or a self-pipe to handle signals in the main loop.
Handle SIGTERM for graceful shutdown; let crashes produce core dumps for debugging.
Interview Relevance
Very high (embedded especially): "interrupt vs exception vs trap?", "what happens when an interrupt occurs?", "polling vs interrupts?", "what is interrupt latency?", "why can't you call printf in an ISR?", "what is a page fault?", "how does Ctrl+C stop a program?".
Practice Questions
Install a SIGINT handler that counts Ctrl+C presses and exits after three.
Send SIGTERM to a child process and have it clean up before exiting.
Compare /proc/interrupts before and after downloading a file; which counters increase?
Explain top half vs bottom half with a network card example.
Quick Quiz
Q1. (Multiple choice) A page fault is a: A) Hardware interrupt B) Fault-type exception C) Trap D) Signal sent by another process Answer: B.
Q2. (Predict the output) What signal does integer division by zero produce on Linux x86-64?
Answer: SIGFPE (signal 8) — verified.
Q3. (True/False) Without a timer interrupt, a preemptive OS could still take the CPU away from a process in an infinite loop.
Answer: False. The timer interrupt is what gives the kernel control back.
Q4. (Find the bug) An ISR sets int data_ready = 1; and main loops while (!data_ready) {} forever even after the interrupt.
Answer: Without volatile (or an atomic), the compiler may keep data_ready in a register and never reread it.
Coding Challenge
Write a program that installs a SIGINT handler setting a volatile sig_atomic_t flag, then sends itself SIGINT three times with raise and prints how many were received — using only async-signal-safe operations in the handler.
#include <stdio.h>
#include <signal.h>
static volatile sig_atomic_t interrupts;
static void on_int(int sig) { (void)sig; interrupts++; } /* async-signal-safe: only touches a flag */
int main(void)
{
struct sigaction sa = {0};
sa.sa_handler = on_int;
sigaction(SIGINT, &sa, NULL);
for (int i = 0; i < 3; i++) raise(SIGINT);
printf("SIGINT received %d times, program still running\n", (int)interrupts);
return 0;
}