// linux fundamentals — module 05
Module 5 — Processes, Signals, and I/O
Thesis: This module is the mechanics of running programs: how every process is born (one two-step, no exceptions), how it dies, how you steer it (signals), and how its input and output are wired (file descriptors, redirection, pipes). It's the module that turns you from someone who runs commands into someone who orchestrates them — and it's the direct foundation for systemd.
Prerequisite: Module 4 — Users, Groups, and Permissions — processes carry UIDs, and you know what that means.
5.1 fork and exec: how everything is born
Module 2 defined a process; here's where they come from. Linux creates processes with a two-step so elegant it hasn't changed since 1969, performed via two syscalls you met in §2.2:
Step 1 — fork(): clone yourself. A process calls fork, and the kernel duplicates it: same program, same variables, same open files, same UID — an almost-identical twin with a new PID. Both continue from the same line of code; the only difference is fork's return value (the child sees 0, the parent sees the child's PID), which is how each knows who it is. (Cheap in practice: §2.4's page tables let the kernel share memory pages until one twin writes — copy-on-write.)
Step 2 — execve(): become someone else. The child then calls exec with a path: the kernel replaces the process's program and memory with the new program, and it starts from the top. Same process, same PID — new identity. Exec is a brain transplant, not a birth.
Every program launch on the system is this pair. When you type ls:
- Your shell forks — now two shells.
- The child shell execs
/usr/bin/ls— it becomesls. - The parent shell calls
wait()— sleeping (§2.3) until the child finishes. lsexits; the kernel wakes the parent; the prompt returns.
Why the split into two calls? Because the gap between fork and exec is where the child — still running shell code — rearranges its own world before becoming the new program: redirecting output, changing directory, dropping privileges. That gap is where everything in §5.5 happens, and where much of what systemd does per-service (Module 7) happens too.
Two loose ends this immediately ties:
- Inheritance, explained. Environment variables (§1.8), UID (§4.1), working directory, open files — children inherit all of it because fork copies it. "Inherit" was always just "was in the memory that got cloned."
- Why
cdmust be a builtin (Module 1's teaser): ifcdwere a program, the shell would fork, the child would change its own directory and exit, and the parent shell would stand unmoved. Anything that must change the shell's own state has to run inside the shell.
5.2 The process lifecycle
States you'll see in ps output (the letter is the STAT column):
- Running / runnable (R) — on a core, or in line for one (§2.3).
- Sleeping (S) — waiting for something: input, a timer, a child. Most of every system, most of the time. (D, uninterruptible sleep, is the variant waiting on disk/hardware I/O — notable because not even
kill -9moves it until the hardware answers; a process stuck in D is a hardware/filesystem clue.) - Stopped (T) — paused by job control (§5.4); frozen, resumable.
- Zombie (Z) — finished, but still in the process table. Worth understanding precisely, since it sounds alarming and mostly isn't: when a process exits, the kernel keeps a stub — PID and exit status — so the parent can collect it via
wait(). Until then, the stub is a zombie: it consumes a PID and nothing else, and cannot be killed (it's already dead — that's the point of the name). One or two are trivia; hundreds mean a buggy parent never reaping.
The other direction: if the parent dies first, the orphan child is re-parented to PID 1, which dutifully reaps it on exit. This is why PID 1 is the ancestor of everything (§2.5) in a maintenance sense too — and "daemonizing" (how background services traditionally detach: fork, let the parent exit, get adopted by PID 1) exploits it deliberately. Keep that in your pocket for Modules 6–7.
5.3 Exit codes: how programs report back
Every process ends with an exit code, 0–255. The universal convention: 0 = success, anything else = failure (programs document their nonzero meanings). The shell stores the last command's code in $?:
ls /etc ; echo $? # 0
ls /nonsense ; echo $? # 2 — and this is how scripts "know" something failed
The shell's control operators read exit codes directly — this is the grammar of one-liners and all shell scripting:
cmd1 && cmd2 # run cmd2 only if cmd1 succeeded
cmd1 || cmd2 # run cmd2 only if cmd1 FAILED
cmd1 ; cmd2 # run both regardless
And systemd will use exit codes to decide whether a service "failed" and whether to restart it (Module 7). Small number, load-bearing.
5.4 Signals: steering processes
A signal is a tiny numbered message the kernel delivers to a process — the standard way to ask a process to stop, die, or reload. The ones to know cold:
| Signal | # | Meaning | Default reaction |
|---|---|---|---|
SIGTERM |
15 | "Please terminate." | die (but may clean up first, or handle it) |
SIGKILL |
9 | Die. Not deliverable to the process — the kernel just destroys it. | die, unconditionally |
SIGINT |
2 | Interrupt — this is what Ctrl-C sends |
die |
SIGHUP |
1 | "Hangup" (the terminal went away); repurposed by daemons as "reload your config" | die |
SIGSTOP / SIGTSTP |
19/20 | Freeze (TSTP is Ctrl-Z) |
stop (uncatchable for STOP) |
SIGCONT |
18 | Thaw | resume |
SIGSEGV |
11 | Kernel-sent: you touched memory outside your page table (§2.4) | die ("segfault") |
Processes may install handlers — custom reactions — for most signals: catch SIGTERM to flush files before exiting, catch SIGINT to print "really quit?". Two exceptions exist precisely so control is never lost: SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
Sending them:
kill 1234 # SIGTERM to PID 1234 (kill = "send a signal"; the name is historical)
kill -9 1234 # SIGKILL
kill -HUP 1234 # by name
pkill firefox # signal by process name; pgrep firefox just finds the PIDs
The etiquette, which is also engineering: always SIGTERM first — give the process its chance to clean up (databases particularly). SIGKILL is the fallback, not the habit: the process gets zero opportunity to flush buffers or remove lock files, and you inherit whatever mess that leaves. Permission rule, straight from Module 4: you may signal only processes running as your UID; root signals anything.
5.5 File descriptors, redirection, and pipes
Now the I/O half. When a process opens a file (§2.2's open), the kernel returns a file descriptor (fd): a small integer that indexes the process's table of open files. All subsequent I/O names the fd, not the path. Three fds exist by convention in every process, wired up before it starts:
- 0 — stdin (standard input): where it reads from
- 1 — stdout (standard output): where results go
- 2 — stderr (standard error): where complaints go — separate from stdout on purpose, so errors stay visible (or get captured) independently of data
In a terminal, all three point at the terminal device (§3.3). Redirection is the shell rewiring them — in the fork/exec gap from §5.1, before the program starts, which is why programs never know or care; they write to fd 1 wherever it leads:
cmd > out.txt # stdout → file (create/truncate)
cmd >> out.txt # stdout → file (append)
cmd < in.txt # stdin ← file
cmd 2> err.txt # stderr → file
cmd > out.txt 2> err.txt # split streams into separate files
cmd > all.txt 2>&1 # stderr → wherever stdout points → one merged file
cmd 2> /dev/null # discard complaints (§3.3's bottomless bin, in its natural habitat)
(That 2>&1 reads as "make fd 2 a copy of fd 1" — and order matters, since it copies where fd 1 points right now.)
Pipes are the crown jewel. cmd1 | cmd2 tells the shell: create a kernel byte-channel (a pipe), wire cmd1's fd 1 into it, wire cmd2's fd 0 from it, run both concurrently. Data flows as it's produced; the kernel handles pacing (a fast producer blocks until the slow consumer catches up). Chain freely:
ps -ef | grep firefox | wc -l # every process → only firefox lines → count them
sort access.log | uniq -c | sort -rn | head # the classic: count occurrences, show top
This is §0.6's Unix philosophy made physical: ps doesn't know how to count, wc doesn't know what a process is, and together they answer a question neither could alone — because everything speaks text through fds. Module 3's named pipes (p in the type table) are exactly this channel, given a filename.
5.6 Job control: many tasks, one terminal
The shell can juggle multiple jobs in one terminal:
sleep 300 & # & = start in the background; prompt returns immediately
jobs # list this shell's jobs: [1]+ Running sleep 300 &
fg %1 # bring job 1 to the foreground
Ctrl-Z # SIGTSTP (§5.4): suspend the foreground job — stopped, not dead
bg %1 # resume that stopped job, in the background (SIGCONT)
kill %1 # signal it by job number
The full loop worth internalizing: run something in the foreground → Ctrl-Z to freeze it → bg to let it continue behind your prompt → keep working → fg to reclaim it. Job control is signals (TSTP/CONT) wearing an ergonomic costume.
One trap with an instructive cause: background jobs are children of your shell — close the terminal, and they get SIGHUP (§5.4's original meaning) and die. nohup cmd & (or a systemd user service, once you have Module 7) is how something survives you leaving. The real lesson: "background job" ≠ "service." Services need a supervisor that isn't your login session — which is systemd's cue to enter.
5.7 Watching processes live
Your inspection kit, all reading /proc under the hood (proved in Lab 2):
ps -ef— every process: UID, PID, parent PID, command.ps aux— same idea, BSD flavor, adds CPU/RAM percentages and STAT letters (§5.2).ps -ef --forest— as a tree.top— live-updating dashboard; better:htop(install it): sortable, tree view (F5), kill from inside (F9 — it offers you a signal menu, which after §5.4 you can read fluently).pgrep -a name/pidof name— find PIDs by name.- Load average (in
top/uptime): three numbers = average count of runnable (R-state) processes over 1/5/15 minutes. Rule of thumb: sustained load ≈ core count means saturated CPU; the three-number trend says whether it's rising or passing.
5.8 → NixOS
systemd — next module, and the thing NixOS configures constantly — is, at its mechanical heart, a program that does this module professionally: it forks and execs every service (§5.1), wires their stdout/stderr into its journal instead of a terminal (§5.5), sends SIGTERM-then-SIGKILL to stop them (§5.4, etiquette included), watches exit codes to decide about restarts (§5.3), and reaps every orphan as PID 1 (§5.2). Every systemctl action you'll ever run resolves to the primitives on this page. Master them here, and Module 7 is recognition rather than learning.
Lab 5
See fork/exec with your own eyes. Run:
strace -f -e trace=clone,execve bash -c 'ls'(
-f= follow children.) Find theclone(fork's modern spelling) and then, in the child,execve("/usr/bin/ls"…). The two-step, in the wild. Thenecho $$, runbash,echo $$inside it (new PID — a child), andexit.The full job-control loop. Run
sleep 300.Ctrl-Z— read[1]+ Stopped. Checkjobs, thenps aux | grep sleepand find stateT. Resume withbg, confirm stateS. Reclaim withfg, thenCtrl-Cit. Narrate every step in signal names.Kill etiquette drill. Start
sleep 600 &. Find it withpgrep -a sleep. Send SIGTERM (kill <pid>), verify death (pgrepagain). Restart it; kill by name (pkill sleep). Restart it;kill -9it and say out loud what the process got to do first (nothing — that's the point).Make a zombie on purpose.
bash -c 'sleep 5 & exec sleep 30'In another terminal within those 30 s (after the first 5):
ps aux | grep -w Z— there's your zombie,<defunct>: its parent (sleep 30, which never callswait) hasn't reaped it. Try tokill -9the zombie — nothing, it's already dead. Then watch it vanish when the parent exits (adopted and reaped by PID 1). §5.2, fully witnessed.Split the streams. Run:
ls /etc /nonsense > out.txt 2> err.txtInspect both files — data went one way, the complaint went the other. Then merge:
ls /etc /nonsense > all.txt 2>&1. Then discard:ls /etc /nonsense 2>/dev/null. Explain each in fd language.Build a real pipeline, stage by stage. Goal: "the 5 biggest files in
/etc, human-readable." Build incrementally, running after every stage:du -a /etc 2>/dev/null du -a /etc 2>/dev/null | sort -rn du -a /etc 2>/dev/null | sort -rn | head -5Narrate what flows through each
|. Note the mid-pipeline2>/dev/nulldoing exactly its §5.5 job (du complains about unreadable dirs — Module 4 explains why they're unreadable).Exit codes drive logic. Run
ls /etc; echo $?andls /nope; echo $?. Then predict before running:ls /nope && echo YES,ls /nope || echo NO,ls /etc && echo YES || echo NO.Drive htop. Install it; run it. Sort by memory, switch to tree view (F5) and find your terminal→shell→htop lineage (Lab 2's pstree, live), then start
sleep 500 &in another tab and kill it from inside htop with F9, choosing SIGTERM from the menu — reading that menu with full comprehension.
✅ Mastery Check — do not proceed until true
Answer out loud, without notes:
- Describe fork and exec precisely, then narrate the four-step story of the shell running
ls, includingwait. Why mustcdbe a builtin? - Why does a child inherit environment, UID, and working directory? (One-word core: what does fork do?)
- Name the five lifecycle states with their
psletters. What exactly is a zombie, why can't you kill it, and who cleans up orphans? - Give the signal (name and number) for: polite termination, unconditional kill,
Ctrl-C,Ctrl-Z, "reload config." Which two signals can never be caught, and why does that design exist? Why TERM before KILL? - What is a file descriptor? What are fds 0, 1, 2? Explain what the shell (not the program) does for
cmd > f 2>&1, and why order matters in that line. - What does
|actually construct, who runs concurrently, and what happens when the producer outpaces the consumer? - What does
$?hold, and what do&&and||do with it? - Why does closing a terminal kill background jobs, and what does that imply about running services?
And perform cold:
- Suspend, background, foreground, and kill jobs, narrating the signals involved.
- Redirect stdout and stderr to separate files, merge them, discard one.
- Compose a three-stage pipeline to answer a question no single tool answers.
- Find any process and send it a chosen signal, politely first.
When all of that is effortless: Module 6 — The Boot Sequence