Lion Cipher Lab
Build a Unix shell with job control: fork, exec, pipes, and signals
- Foundations
- Advanced
- ~90 min
- Free · no account required
Why it matters
You already built a shell that runs commands, but press Ctrl-Z in it and nothing useful happens; type a long pipeline and you cannot push it to the background; there is no `jobs`, no `fg`, no `bg`. Those are not extra features bolted on — they are a whole kernel subsystem your shell has to cooperate with, and it is the part of a shell almost nobody understands until they implement it. Job control is how ONE terminal multiplexes MANY jobs: each pipeline gets its own PROCESS GROUP, the terminal has exactly one FOREGROUND group at a time, and the shell's job is to hand the terminal to a job…
What you'll learn
- the process-group model of job control: each pipeline runs in its own PROCESS GROUP (setpgid), a SESSION groups the pipelines of one login, and exactly one group at a time is the terminal's FOREGROUND group
- handing the controlling terminal to the foreground job with tcsetpgrp, and reclaiming it for the shell when the job stops or finishes — the step that makes Ctrl-C and Ctrl-Z reach the right process
- the shell's signal posture: it IGNORES the terminal-generated signals (SIGINT, SIGTSTP, SIGTTOU, SIGTTIN, SIGQUIT) so the terminal driver delivers them to the foreground job's group instead, and each child RESTORES them to SIG_DFL before exec
- why setpgid must be called in BOTH the parent and the child after fork — a race where either one may run first, so the job is placed in its group no matter the order
- waitpid with WUNTRACED / WIFSTOPPED so a Ctrl-Z (SIGTSTP) is seen as a STOPPED job the shell records, not a dead child the shell hangs waiting for
- implementing jobs / fg / bg / & : a job table of {pgid, state, command}, SIGCONT to resume a stopped group, and a non-blocking waitpid poll (WNOHANG|WUNTRACED|WCONTINUED) to report background state changes
- the tcsetpgrp SIGTTOU failure mode: any process that is NOT in the foreground group touching the terminal is sent SIGTTOU and stopped — the classic 'my shell froze' bug — and why the shell ignores SIGTTOU to survive it
- the SIGCHLD reaping race: a forked child can exit and its SIGCHLD arrive BEFORE the parent records it in the job table, so bookkeeping must block SIGCHLD across fork (or reap synchronously) or the job is lost/leaked
- orphaned process groups and zombie leaks: a stopped background group whose shell dies, and un-reaped children that pile up as defunct entries — the two ways a job-control shell rots if it skips the bookkeeping
- the judgment: what a real shell (bash, dash) adds on top of this skeleton, when synchronous reaping beats a SIGCHLD handler, and why job control lives in the kernel's terminal/session layer, not in your parser
Steps in this lab
- Read this first: your own box, your own programs, an interactive terminal
- What job control actually is: process groups, sessions, and the foreground
- The shell's side of the deal: ignore the terminal signals, hand off the terminal
- Set up a scratch box and initialize the shell
- The whole shell: jsh.c (write it now, then we dissect it)
- Mechanism 1 — the setpgid race: why the call appears twice
- Mechanism 2 — tcsetpgrp: handing over the terminal (and why Ctrl-Z now works)
- Mechanism 3 — WUNTRACED and the background poll: stopped is not dead
- Break it on purpose #1 — the tcsetpgrp SIGTTOU freeze
- Break it on purpose #2 — the SIGCHLD reaping race and orphaned/zombie jobs
- The judgment: where your shell sits, and what a real one adds