Reading — step 1 of 5
Read
Signals & Process Control
Pipes let processes send data. Signals let the kernel (or another process) send a process a notification — "interrupt received," "child died," "you violated memory," "please terminate." They're the software analog of hardware interrupts: asynchronous, numbered events that yank a process's attention away from whatever it was doing. Every Ctrl-C, every crash, every kill command rides this mechanism, and by the end you'll have implemented its delivery logic.
A signal is an interrupt for a process
The parallel to the Interrupts lesson is exact and intentional:
| Hardware interrupt | Signal |
|---|---|
| device raises a line | kernel/process raises a signal |
| CPU vectors to an IDT handler | process jumps to a signal handler |
| numbered (vector) | numbered (SIGINT=2, SIGKILL=9, …) |
| can be masked | can be blocked |
The signals you'll meet constantly:
- SIGINT (2) — Ctrl-C. "Please stop." Catchable.
- SIGTERM (15) — polite kill (what
kill pidsends). Catchable — programs trap it to clean up before exiting. - SIGKILL (9) — the unstoppable one (
kill -9). Cannot be caught, blocked, or ignored — the kernel's guarantee that anything can be killed. - SIGSEGV (11) — you dereferenced bad memory (the page-fault handler couldn't resolve it — C-course alumni know this crash).
- SIGCHLD (17) — a child changed state; default action ignore, but the shell traps it to reap zombies (the process lesson's cleanup).
- SIGPIPE (13) — wrote to a pipe with no reader (last lesson!).
Disposition: what a signal does when it arrives
Each signal has a per-process disposition — three choices:
- Default — usually terminate (SIGINT, SIGTERM, SIGSEGV), sometimes ignore (SIGCHLD), sometimes stop.
- Ignore — set it, and the signal is discarded on arrival.
- Handler — a function the kernel calls when the signal arrives, interrupting normal flow, then resuming. This is how a text editor catches SIGTERM to save your file before dying.
SIGKILL and SIGSTOP are the exceptions that prove the rule: their disposition cannot be changed. No handler, no ignore, no block. If any process could catch SIGKILL, an unkillable process would exist — so the kernel forbids it. Your simulator enforces exactly this: HANDLE KILL then SIGNAL KILL still terminates.
Blocking, pending, and the delivery window
A process can temporarily block (mask) signals — not ignore, defer. A blocked signal that arrives becomes pending: remembered, delivered the instant it's unblocked. This is the critical-section tool for signal-land: a process updating a data structure a handler also touches blocks the signal first, finishes the update, then unblocks — and the pending signal fires safely afterward. (The race it prevents is the spinlocks lesson's race, between mainline code and an asynchronous handler.)
One honest, famous subtlety your simulator captures in spirit: standard signals don't queue — the pending state is a set, so ten blocked SIGINTs collapse into one pending SIGINT. (Real-time signals queue; classic ones don't. It's why signals are for notification, not reliable counting.) And on unblock, multiple different pending signals deliver in a defined order — your exercise delivers them in arrival order.
Delivery mechanics (the part that's wilder than it looks)
When the kernel delivers a signal with a handler, it does something remarkable: it modifies the process's user stack to make it look like the process called the handler function — pushes a fake return frame, points the instruction pointer at the handler, and returns to user mode (the iret / privilege machinery from the User Mode lesson). The handler runs in ring 3, and when it returns, a special trampoline invokes sigreturn to restore the original context, as if nothing happened. The process is hijacked, serviced, and resumed — the software mirror of how a hardware interrupt hijacks the CPU. Your exercise models the decisions (deliver? pend? terminate?); this paragraph is what those decisions set in motion.
Your exercise: Signal Delivery
Track dispositions (HANDLE/IGNORE), the blocked set (BLOCK/UNBLOCK, with pending delivery on unblock), and deliver SIGNALs by the rules — including uncatchable KILL and the death-stops-processing behavior of a terminating signal. The graded corners are the real ones: KILL ignoring every disposition, blocked signals pending then delivering in order on unblock, CHLD's ignore-by-default, and a TERMINATE ending the process so later lines don't run. It's a small state machine over dispositions and a pending set — and it's the exact logic behind every Ctrl-C that stops your program and every clean shutdown that saves your work first.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…