Module 7: Processes & Signals
The Unix process model
Lessons
| ID | Title | Duration |
|---|---|---|
| 7-1 | fork and the Process Model | 30 min |
| 7-2 | exec — replacing the process image | 25 min |
| 7-3 | Pipes and IPC | 30 min |
| 7-4 | Signals | 30 min |
| 7-5 | setjmp and longjmp | 20 min |
Project
| 7-proj | Project: Minimal Shell | 90 min |
Key Concepts
fork, signals, pipes, copy-on-write, signal-mask
Pedagogical Notes
What this module is really teaching: The Unix process model is a study in
controlled aliasing. fork() creates two processes sharing physical pages;
exec() replaces the image; pipes share kernel buffers via file descriptors.
Every primitive is a handle to shared kernel state — the safety comes from the
rules around when state is shared vs. private.
The central trap students fall into: Treating fork() as “copying the
process”. It doesn’t copy pages — it copies the page table and marks pages
copy-on-write. Students who don’t understand COW are surprised when a 4 GiB
process forks in microseconds, or confused when a child’s RSS climbs dramatically
after a write but the parent’s doesn’t.
Signal handlers: the other trap. Students write handlers that call printf()
or malloc() and it “works” most of the time — then crashes intermittently under
load. The fundamental issue is re-entrancy: if the main thread is inside malloc()
when the signal fires and the handler also calls malloc(), the allocator’s
internal linked list gets corrupted. The fix (volatile sig_atomic_t flag +
write() only) feels overly restrictive until you’ve seen the crash.
setjmp/longjmp: students coming from languages with exceptions find this
familiar-but-wrong — longjmp doesn’t call destructors, doesn’t free locals,
doesn’t close FDs. It’s a register restore, not an unwind. The Lua/SQLite
comparison (using it for top-level error recovery only, never returning to the
main flow) is the key pedagogical anchor.
Key Code Examples
Copy-on-write in action:
char *buf = malloc(64 * 1024 * 1024);
memset(buf, 'A', 64*1024*1024); // touch pages — now backed
pid_t pid = fork();
if (pid == 0) {
// Child's RSS climbs 64 MiB here:
memset(buf, 'B', 64*1024*1024);
printf("child: buf[0]='%c'\n", buf[0]); // 'B'
_exit(0);
}
waitpid(pid, NULL, 0);
printf("parent: buf[0]='%c'\n", buf[0]); // 'A' — unchangedThe only safe signal handler pattern:
static volatile sig_atomic_t g_stop = 0;
static void handle_sigint(int sig) {
(void)sig;
g_stop = 1; // atomic flag
(void)write(STDERR_FILENO, "\n", 1); // write() is safe
}
// In main loop:
while (!g_stop) { /* ... */ }Closing unused pipe ends (the most common pipe bug):
int pipefd[2];
pipe(pipefd);
pid_t pid = fork();
if (pid == 0) {
close(pipefd[1]); // child doesn't write
// read from pipefd[0]...
_exit(0);
}
close(pipefd[0]); // parent doesn't read
// write to pipefd[1]...
// If parent forgets to close(pipefd[0]), child's read() never returns EOF.Cross-Module Dependencies
- Requires m02-pointers-memory-model (m2):
fork()works on virtual address spaces; understanding that pointers are virtual addresses (not physical) is essential for understanding why fork + COW is fast and why post-fork modifications are independent. - Requires m06-file-io-syscalls (m6): pipe(), dup2(), and open() are all file descriptor operations. The three-table FD model (per-process fd table → open file table → inode/socket) is the same model as regular files.
- Informs m08-sockets-networking (m8): A socket is a file descriptor. The accept() loop is a fork-based concurrency pattern. Signal handling for SIGPIPE is universal in network servers.
- Informs m09-concurrency (m9):
fork()is one of two concurrency primitives (the other being threads). The signal-mask model introduced here (per-thread mask, blocked/pending state) directly applies to thread signal handling.
PDF Status
- PDF ch08 (Processes and Signals) — Complete (written 2026-04-09)
- Key Concepts: 8-row table (fork, exec, waitpid, pipe, signal, signal mask, volatile sig_atomic_t, setjmp/longjmp) + process vs. thread callout
- Going Deeper: COW mechanics with ASCII diagram; signal delivery model with pending/blocked state diagram and self-pipe trick; setjmp/longjmp internals table (what’s saved vs. not); exec boundary table + O_CLOEXEC callout
- Real-World Connection: Redis sigsegvHandler() — why unsafe-but-works is acceptable for crash-only handlers
- Solutions: 8.1 (fork + /proc RSS measurement), 8.2 (sigprocmask critical section), 8.3 (mini shell with SIGINT forwarding), 8.4 (pipe + dup2 pipeline)
- Extended Project: job-control shell (process groups, fg/bg/jobs, SIGCHLD reaping, tcsetpgrp)
- Test files: tests/ch08/ex8_1–ex8_4 (all compile clean under -Wall -Wextra -Werror -std=c11)