Master the fundamental concepts of inter-process communication through this focused micro-challenge.
You have read the whole brief, and the concepts above stay free on every task. Writing and running the code needs a plan.
Three hints are available for this task, revealed one at a time inside the code workspace so you can struggle productively before seeing them.
Every task includes starter code, theory, and hidden tests so you can implement and verify locally in the browser.
How it worksSignals are software interrupts delivered to processes: SIGINT from Ctrl-C, SIGCHLD when children exit, SIGSEGV on bad memory. Handlers run asynchronously unless masked, complicating reentrant code.
Safe patterns:
sigaction, not signalFor example, a SIGUSR1 handler setting got_signal = 1 lets the main thread drain work cooperatively.
SIGTERM-triggered graceful shutdown is exactly what systemd sends before SIGKILL when stopping a service, and Kubernetes relies on the same contract to give a pod's main process a chance to drain connections before its termination grace period expires. The volatile sig_atomic_t requirement here isn't pedantic: calling non-async-signal-safe functions like printf or malloc inside a real handler is a documented cause of heap corruption and deadlocks, since a signal can interrupt malloc mid-operation.
Before you call the implementation done, walk failure modes on purpose. Test empty structures, single-element edge cases, maximum concurrency, and errno paths that must not crash the program. OS code usually fails in production when happy-path tests pass but invariants break under contention or memory pressure.
Keep structures small and name fields after kernel counterparts when possible. That lets you read man pages and kernel source side by side while you work. Print observable events during development; remove noisy logs once tests pass reliably.
You will register handlers, send signals with kill, and reap children on SIGCHLD. This exercise requires demonstrating restart of slow syscalls with SA_RESTART vs EINTR.
Explore POSIX signals through a script of operations. Install handlers with sigaction and send signals with raise and kill. Block signals with sigprocmask and watch them become pending. Note that ordinary signals don't queue: two identical signals raised while blocked are delivered once. Reap a child through SIGCHLD, and shut a child down gracefully with SIGTERM, the contract systemd and Kubernetes rely on.
Only these names appear: HUP, INT, USR1, USR2, TERM, CHLD. Always print names, never numbers (the numbers differ between systems).
| Command | Effect | Output |
|---|---|---|
handle SIG | sigaction with a handler that increments a per-signal volatile sig_atomic_t counter | handle SIG: installed |
ignore SIG / default SIG | SIG_IGN / SIG_DFL | ignore SIG: ok / default SIG: ok |
block SIG / unblock SIG | sigprocmask(SIG_BLOCK / SIG_UNBLOCK) | block SIG: blocked; unblock prints unblock SIG: delivered, count N if it was pending, else unblock SIG: nothing pending |
raise SIG | raise(SIG) | raise SIG: handled (count N), raise SIG: pending (blocked), or raise SIG: ignored |
pending | sigpending | pending: USR1 TERM (in the order above) or pending: none |
child N | fork a child that exits with status N; wait for SIGCHLD with sigsuspend if CHLD is handled, then waitpid | child: exited with status N (CHLD count C), where C is the handler's count, or - if CHLD isn't handled |
kill-child SIG | fork a child that keeps the default disposition and waits in pause(); send it SIG with kill | kill-child SIG: child killed by SIG |
term-child | fork a child that installs a SIGTERM handler, tells the parent it is ready (through a pipe), waits for the signal, prints child: SIGTERM received, cleaning up, and exits 0; the parent sends SIGTERM | the child's line, then term-child: exited with status 0 |
raise on a handled, unblocked signal runs the handler before raise returns, so the count is already updated. The script never raises a signal whose default action would kill the main process.
Input:
cLoading…
Output:
cLoading…
The second pending USR1 merged with the first: the count went from 1 to 2, not 3.
sigaction (not signal), and keep handlers async-signal-safe: they only increment a counter.SA_RESTART and retry waitpid on EINTR: a SIGCHLD handler can interrupt it. For child and term-child, avoid races: block SIGCHLD before forking and wait with sigsuspend; for term-child, send SIGTERM only after the child signals that its handler is installed. Flush stdout before every fork.Hidden tests cover SIGCHLD reaping with and without a handler, a child killed by a default-action signal, graceful SIGTERM shutdown, several different signals pending at once, and restoring a default disposition.