Module 7: Processes & Signals

The Unix process model

Lessons

IDTitleDuration
7-1fork and the Process Model30 min
7-2exec — replacing the process image25 min
7-3Pipes and IPC30 min
7-4Signals30 min
7-5setjmp and longjmp20 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' — unchanged

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