<aside> ⚙️

Module 12 — Processes, Subshells, and Job Control. Why cd inside a pipeline doesn't stick, what & really does, and how to run things in parallel and wait for them — the process model underneath every script.

🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)

</aside>

<aside> 📋

Before you start. You need Modules 1–11. This module is where a dozen earlier IOUs get paid at once: the child-shell story (Module 1 C1), export's copy-down (Module 2 C1), pipeline verdicts (Module 4), the piped-while variable loss (Modules 6 and 10), $(fn) subshells (Module 8), and the fork-cost of $( ) in loops (Module 9). If those felt like rules, today they become one mechanism. Tools: nothing new.

</aside>

Part A — The process model: parents, children, copies

A1. Everything running is a process with a PID and a parent

<aside> 📖

Official docs: ps(1) — man7.org · Bash manual — Special Parameters

</aside>

Every running program is a process: a live instance with its own memory, its own copies of the environment (Module 2's briefing pack), and a numeric PID. Every process has a parent — the process that started it — so the whole system forms a family tree, and ps -o pid,ppid,stat,comm shows any branch of it: PID, parent PID, state, command. Your shell is a process (echo $$ prints its PID); every external command you run becomes its child for the duration; your scripts run as children of your shell — which is Module 1's "second bash" with its birth certificate finally attached.

The one operational consequence to hold from the start: children inherit copies at birth and can never write back (Module 2's one-way briefing pack — now you know it's not a bash rule but how processes work: separate memory, no shared jars). Everything counter-intuitive in this module is that single fact viewed from different angles.

A2. Subshells: bash's copies of itself

<aside> 📖

Official docs: Bash manual — Command Execution Environment · Bash manual — Command Grouping

</aside>

Bash routinely makes copies of itself to run pieces of your code — subshells — and the copy starts with everything the original had (all variables, exported or not, the current directory, open files) but is still a separate process: its changes die with it. You have been creating subshells for ten modules without the name. The complete list of what runs in one: ( commands ) — explicit grouping in parentheses; $( commands ) — every command substitution; each stage of a pipeline; and anything ending in & (Part C). By contrast, { commands; } with braces groups without copying — same shell, changes stick — which is the grouping you used in Module 7's guards.

<aside> 🧩

Real-world analogy — the photocopied notebook. A subshell is handing a photocopy of your entire working notebook to an assistant: they can read every page (unlike a child script, which gets only the exported pages — that difference matters), scribble anywhere, tear pages out — and when they finish, their copy goes in the bin. Braces { } are handing the assistant your actual notebook: same notes, real consequences.

Where the analogy stops working. Photocopying a notebook is slow; forking a subshell is fast enough that bash does it on every $( ) — thousands of times in a naive loop. Fast is not free (Module 9's fork tax), and invisible is the real hazard: nothing in count=$(...) or | while read looks like a photocopy, yet each is one, and variables set inside are scribbles on the doomed copy.

</aside>

🧪 Exercise 12.1 — watch the copy live and die

echo "my shell PID: $$"
( echo "subshell PID: $BASHPID"; cd /tmp; x=changed )
echo "after: pwd=$(pwd) x=[${x:-unset}]"
{ y=sticky; }
echo "braces: y=[$y]"

Interview questions — Part A

Part B — The pipeline subshell: the most-hit gotcha in bash

B1. Why | while read loses your variables — and the two escapes