Module 12 — Processes, Subshells, and Job Control

Updated 3 September 2026

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)

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.

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

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

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

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.

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.

🧪 Exercise 12.1 — watch the copy live and die
bash
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]"
Expected result — click to reveal
plain text
my shell PID: 4821
subshell PID: 4822
after: pwd=/home/zaeem/bash-course x=[unset]
braces: y=[sticky]

What to read out of it (your PIDs and path differ): two different PIDs — the parentheses really made a second process ($BASHPID shows the current process's PID; $$ inside a subshell keeps showing the original's, a subtlety worth one mental note). The subshell cd'd and assigned — and the after-line shows neither survived. The braces line is the contrast: same shell, y stuck. Every "why didn't my variable survive?" mystery in bash reduces to this exercise: find the invisible parentheses.

Interview questions — Part A

🎯 "What is a subshell? How do you use subshells in Bash?" — asked verbatim at Zero To Mastery; Edureka asks "How to print PID of the current shell?"

The direct answer: a subshell is a child copy of the shell process — same variables and directory at birth, separate life, changes discarded at death. Created by ( ), $( ), pipeline stages, and &. Deliberate uses: scoped directory changes ((cd /tmp && tar xf …) — the caller never moves), scoped environment tweaks, and parallel branches.

Going deeper: the inheritance nuance — a subshell inherits everything (a fork), while a child script gets only exported variables (fork + exec of a fresh bash); that one distinction untangles most "why can/can't it see my variable" confusion. $$ vs $BASHPID: $$ is the original shell's PID even inside a subshell; $BASHPID is always the current process.

The details that separate candidates: enumerating the implicit subshells (command substitution, every pipeline stage) rather than just ( ) — because that is where the bugs live; and naming the cost model: forks are cheap enough for correctness, expensive enough that $( ) inside hot loops is a smell (Module 9's review heuristic, now with its mechanism).

🎯 "What are the various stages of a Linux process it passes through?" — asked verbatim at Edureka

The direct answer: running (or runnable — on/awaiting a CPU), sleeping/waiting (blocked on I/O or a resource — where processes spend most of their lives), stopped (suspended, e.g. by Ctrl-Z or a signal — resumable), and zombie (finished, but its exit status has not yet been collected by its parent — a dead entry still in the process table).

Going deeper: read states from ps -o statR running, S sleeping, T stopped, Z zombie — and explain the zombie precisely: it is not a runaway process (it consumes no CPU) but an uncollected verdict; the parent reaps it with wait. A parent that never waits leaks zombies; if the parent dies, init adopts and reaps them.

The details that separate candidates: connecting zombies to this module's tools — every wait in your scripts is literally the reaping call, so a script that backgrounds jobs and never waits is manufacturing zombies until it exits; and the triage instinct that a few zombies are cosmetic while a growing count indicts a buggy parent (restart the parent, not the zombies — they cannot be killed, being already dead).

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

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

Modules 6 and 10 promised this explanation. Each stage of a pipeline runs in its own subshell — so in cmd | while read -r line; do count=$((count+1)); done, the entire while loop is the doomed photocopy: it counts perfectly, finishes, and dies, taking count with it. The parent's count never moved. Nothing warns you; the loop ran.

Two escapes. Escape one — redirect instead of pipe when the producer is a file: while read … done < file (the loop stays in your shell; only fair, there was no pipeline). Escape two — process substitution when the producer is a command: while read … done < <(cmd). The <(cmd) form runs cmd and presents its output as a filename — so the loop reads "a file" and remains in the current shell, while cmd does its work alongside. The same tool unlocks a famous one-liner family: diff <(command1) <(command2) — comparing two command outputs with no temp files.

🧪 Exercise 12.2 — lose the count, then keep it

The first pipeline loses data on purpose.

bash
count=0
printf 'a\nb\nc\n' | while read -r l; do count=$((count+1)); done
echo "after pipe:    count=$count"
count=0
while read -r l; do count=$((count+1)); done < <(printf 'a\nb\nc\n')
echo "with < <(cmd): count=$count"
diff <(printf 'a\nb\n') <(printf 'a\nc\n')
Expected result — click to reveal (contains a deliberate loss)
plain text
after pipe:    count=0
with < <(cmd): count=3
2c2
< b
---
> c

What to read out of it: the piped loop counted to three in a process that no longer exists — the parent reads 0. Process substitution kept the loop home: 3 survives. Then diff compared two commands' outputs directly — the <( ) filenames made programs-as-files a reality (run echo <(true) sometime to see the strange /dev/fd/63-style name it passes). File the shape permanently: pipe into a loop = loop in a subshell; < <(cmd) = loop stays home.

Interview questions — Part B

🎯 "Why is my counter still 0 after the while loop counted the lines?" — the production-shaped form of the most-reported bash gotcha; the mechanics live inside Zero To Mastery's subshell answer, and Module 10 already cited it as the mapfile pipeline trap

The direct answer: the loop ran in a pipeline stage — a subshell — and its variables died with it. Fix by keeping the loop in the current shell: done < file, done < <(cmd), or collect with mapfile -t arr < <(cmd) and loop the array.

Going deeper: the diagnosis generalizes — any state set in any pipeline stage (variables, arrays, cd) evaporates; the probe is echo $BASHPID inside and outside the suspect construct. Mention bash's shopt -s lastpipe (runs the last stage in the current shell, in non-interactive shells) as the existing-code bandage, with the caveat that the portable, readable fix is process substitution.

The details that separate candidates: stating the rule's other victims unprompted — cmd | mapfile (Module 10), a pipeline stage's exit killing only itself (Module 8's audit nuance) — showing you hold the mechanism, not the single symptom; and knowing <( ) is unavailable in POSIX sh (bashism, Module 1's dialect line — the portable alternative is a temp file, mktemp, Module 14).

Part C — Background jobs: &, wait, and controlled parallelism

C1. & starts it and doesn't look back; wait collects the verdict

command & launches the command as a child and returns your prompt immediately — the shell does not wait. Bash records the child's PID in $! (capture it at once — it is as perishable as $?). Later, wait PID blocks until that child finishes and hands you its exit status as wait's own — the verdict machinery of Module 3, extended across time. Bare wait waits for all children; wait -n waits for whichever finishes next (the key to bounded fan-out below). Around the terminal, the job-control household tools: jobs lists background jobs, fg/bg move them between foreground and background, disown detaches one from the shell, and nohup command & starts it immune to the terminal's death — the classic "keep running after I log out" (its cleaner modern relatives are tmux/systemd, but nohup is the interview answer).

🧪 Exercise 12.3 — launch, tag, collect
bash
sleep 2 &
pid=$!
echo "started sleeper as PID $pid; prompt is already back"
wait "$pid"
echo "sleeper finished with $?"
( exit 3 ) & p1=$!
( exit 0 ) & p2=$!
wait "$p1"; echo "job1 verdict: $?"
wait "$p2"; echo "job2 verdict: $?"
Expected result — click to reveal
plain text
started sleeper as PID 5210; prompt is already back
sleeper finished with 0
job1 verdict: 3
job2 verdict: 0

What to read out of it (PIDs yours): the first echo printed while the sleeper still slept — that is &'s whole meaning. The second block is the important pattern: two children, two captured PIDs, two waits, and each child's individual exit code came back through its wait — failure detection survives parallelism. Scripts that background jobs and never wait get neither verdicts nor cleanliness (Part A's zombies are exactly un-waited children).

C2. Parallelism that pays, fan-out that stays bounded

Why bother? Arithmetic. Three 0.3-second sleeps in sequence: ~0.9s. The same three as background jobs with one wait: ~0.3s — the cost of the longest, not the sum. That is the fleet math of Module 6's warning ("500 hosts × 2s = 17 minutes") solved: parallel probes finish in the time of the slowest host. But unbounded fan-out — 500 simultaneous ssh sessions — is a self-inflicted denial of service; production bounds the width. The pure-bash bounded pattern, built from parts you own:

bash
for host in "${hosts[@]}"; do
    probe "$host" &
    while [ "$(jobs -r | wc -l)" -ge 8 ]; do wait -n; done   # ≤ 8 in flight
done
wait   # collect the stragglers

(jobs -r lists running jobs; when 8 are in flight, wait -n blocks until one finishes, opening a slot. The same shape, outsourced: xargs -P 8 — Module 11 — with the trade-off that xargs can't easily update your shell's variables, subshells being subshells.)

🧪 Exercise 12.4 — feel the parallel arithmetic
bash
time ( sleep 0.3; sleep 0.3; sleep 0.3 )
time ( sleep 0.3 & sleep 0.3 & sleep 0.3 & wait )
Expected result — click to reveal
plain text
real	0m0.906s
user	0m0.008s
sys	0m0.000s
real	0m0.306s
user	0m0.008s
sys	0m0.000s

What to read out of it (small variations normal): sequential ≈ 0.9s, parallel ≈ 0.3s — the sum versus the max, measured. Note user time is near zero in both: sleeps cost no CPU, so parallelism here is pure wall-clock win — exactly the profile of network-bound fleet work (ssh, curl, probes), which is why ops parallelism pays so well. CPU-bound work divides less cleanly (cores are finite); measure before promising.

Now imagine this at 500 hosts. This is the module where the fleet finally speeds up: probe "$host" & pids+=($!) per host, bounded to a sane width, wait per PID with the verdict routed to ok.txt / failed.txt (Module 6's auditable-round files, now filled in parallel). Production wisdom that survives contact: bound the width to what the target can take, not what your box can spawn (8–32 for ssh floods, lower for API endpoints with rate limits); capture $! immediately into an array slot keyed by host so verdicts stay attributable; and log per-job output to per-host files (probe "$host" > "logs/$host.out" 2>&1 &) because 500 interleaved stdouts are write-only noise. GNU parallel and xargs -P are the grown-up tools; knowing the raw pattern is what lets you trust them.

Interview questions — Part C

🎯 "How do you manage background processes in Bash?" — asked verbatim at Zero To Mastery

The direct answer: start with command &; track with $! and jobs; move with fg/bg; wait with wait [pid] (which returns the job's exit status); detach with disown; survive logout with nohup command & (or start under tmux/systemd for anything serious).

Going deeper: the verdict story is what separates scripting from terminal habits — wait "$pid" returning the child's code means parallel steps keep Module-3 discipline; wait -n enables bounded pools; and un-waited children become zombies until the script exits (the Edureka process-stages question, connected).

The details that separate candidates: the Ctrl-Z / bg / disown rescue sequence for "I started a 4-hour job in the foreground of an ssh session" — a genuinely asked scenario; knowing nohup's actual mechanics (SIGHUP immunity + output to nohup.out — Module 13 will name the signal); and the honest boundary that long-running services belong in systemd units, not nohup'd scripts — saying so marks operational maturity.

🎯 "Suppose you execute a command using exec, what will be the status of your current process in the shell?" — asked verbatim at Edureka

The direct answer: exec command replaces the shell process with the command — same PID, no new process created, and nothing after the exec line ever runs; when the command exits, the process is gone (your terminal closes, or your script ends there).

Going deeper: the two production uses — wrapper scripts ending in exec real-tool "$@" (Module 8's Ticket 1: the wrapper vanishes, the tool inherits the PID, signals and exit codes flow directly — beloved by container entrypoints, where PID 1 should be the app, not a lingering bash), and exec > logfile 2>&1 with no command, which permanently rewires the current shell's own streams (Module 4's redirections, applied to yourself — how scripts self-log).

The details that separate candidates: the fork/exec vocabulary — normal commands are fork then exec (copy, then transform the copy); exec alone skips the fork — one sentence that shows the whole model; and the container connection (signal delivery to PID 1) which turns a trivia question into an ops answer.

Part D — The map: where does my code actually run?

D1. The decision

Note: the diagram below is a Mermaid code block. Notion does not render it automatically — click the block and switch it from "Code" to "Preview" (or "Split") to see the flowchart.
Diagram source
flowchart TD
    A["A piece of my script<br>is about to run"] --> B{"Written how?"}
    B -->|"plain command<br>(external)"| C["child process:<br>fork + exec"]
    B -->|"builtin, function,<br>braces block"| D["current shell —<br>changes stick"]
    B -->|"( ... ) or $( ... )<br>or a pipeline stage"| E["subshell: full copy,<br>changes die with it"]
    B -->|"./script.sh"| F["child bash: sees only<br>exported variables"]
    B -->|"source script.sh"| G["current shell —<br>as if typed here"]
    B -->|"command &"| H["background child:<br>capture $!, wait later"]
    B -->|"exec command"| I["REPLACES this process —<br>nothing after it runs"]

One diagram, twelve modules of mysteries: trace any surprising behavior to its box, and the behavior stops being surprising. The two boxes that decide most real bugs are E (invisible copies — variables lost) and F (children see exports only — variables missing).

Interview questions — Part D

🎯 "What is the difference between running a script with ./script.sh, source script.sh, and bash script.sh — at the process level?" — Module 1's question, re-asked the way senior interviewers ask it; the substance behind Zero To Mastery's and Hirist's source-vs-execute questions

The direct answer: ./script.sh and bash script.sh both create a child process (fork + exec of a bash that reads the file) — the child sees only exported variables and its changes die with it; the difference between them is only who chooses the interpreter (kernel-via-shebang vs you). source script.sh creates no process at all — the current shell reads the lines itself; everything sticks, including its exit.

Going deeper: add the two remaining boxes — subshells (fork without exec: a copy that sees everything, exports or not) and exec (exec without fork: replacement, no return) — and you have named all four combinations of fork/exec, which is the entire process model in one 2×2.

The details that separate candidates: presenting it as that 2×2 (fork+exec: commands and scripts; fork only: subshells; exec only: exec; neither: builtins, functions, source) — interviewers remember candidates who compress twelve behaviors into one table; and one concrete probe per box ($BASHPID, exported-vs-not variables, pwd drift) showing you can demonstrate the model, not just recite it.

Part E — Production

E1. 🏭 Production practices

These are the practices in production — how the things this module taught are actually done on real systems:

State-changing loops never sit downstream of a pipe. done < file, done < <(cmd), or mapfile-then-loop — the piped-while subshell is a review-blocking defect with a mechanical fix.

Every & is paired with a captured $! and an eventual wait. Verdicts are collected per job and routed to outcome files; un-waited children (zombie factories) fail review.

Fan-out is bounded, attributed, and logged per target. A width matched to the target's capacity, wait -n slot management (or xargs -P / GNU parallel), and per-host output files — never 500 interleaved stdouts.

Wrappers and container entrypoints end in exec. The tool inherits the PID; signals and exit codes flow without a bash middleman.

Deliberate subshells are used for scoping(cd "$dir" && tar xf "$archive") beats cd-work-cd-back every time (the return trip can fail; the subshell's scope cannot).

Anything meant to outlive the session runs under a supervisor. nohup answers the interview question; tmux answers the emergency; systemd units answer production.

E2. Production-practice table

SymptomWhat is really happeningWhat to runThe fix
Counter/array still empty after a loop that visibly processed linesThe loop was a pipeline stage — a subshell took the state to its graveecho $BASHPID inside vs outside the loopdone < <(cmd) or mapfile-then-loop; never pipe into stateful loops
Script sees none of the variables you set in your terminalChildren inherit exports only — plain variables stay behind (fork+exec)set | grep name vs printenv name (Module 2's pair)export what children need, or pass as arguments
Background jobs "succeeded" but failures went unnoticed& without wait: verdicts never collected (and zombies accumulate meanwhile)jobs before script exit; check for un-waited PIDsCapture $! per job; wait "$pid" and route each verdict
Parallel run flattened a rate-limited API / overwhelmed a hostUnbounded fan-out — every job launched at onceCount in-flight jobs at peak: jobs -r | wc -lBound with the wait -n slot pattern or xargs -P N
Growing count of Z-state processes under one parentThe parent backgrounds children and never reaps (waits for) themps -o pid,ppid,stat,comm | grep Z and find the common PPIDFix or restart the parent; zombies themselves cannot be killed
Lines after a certain command never execute; terminal closes on script endThat command is an exec — the process was replaced, there is no "after"Read the line; exec at its start is the whole storyRemove exec if return was wanted; keep it (deliberately, last line) for wrappers

E3. 🎓 Capstone — four tickets from the queue

Work each ticket yourself before opening the answer. Everything needed was taught in Modules 1–12.
🎓 Ticket 1 — "Health-check script tallies failures: check_hosts | while read -r host status; do [ \"$status\" = down ] && failures=$((failures+1)); done — then alerts if failures > 0. It has never alerted once, including the night 30 hosts were down."

Diagnosis. B1's gotcha carrying a pager: the while loop is a pipeline stage — a subshell; failures incremented 30 times in a doomed copy and the parent's stayed 0 forever. "Has never alerted once" is this bug's signature phrase: the code is not flaky, it is structurally incapable of alerting.

Work the steps: the probe: print $BASHPID inside the loop and outside; different numbers end the argument. Reproduce with Exercise 12.2's printf version.

Fix and prevention: while read -r host status; do …; done < <(check_hosts) — loop stays home, tally survives. Team rule (E1's first line): state-changing loops never sit downstream of a pipe; ShellCheck flags many instances (SC2031/SC2030 family) as mechanical backup.

🎓 Ticket 2 — "To speed up backups, someone added & to each of the four dump commands. Backups now 'finish' in 40 seconds instead of 20 minutes — and the success marker is written while dumps are visibly still running. Also, ops reports a slowly growing pile of defunct processes on that box."

Diagnosis. & without wait, twice over (C1): the script reaches its final line while four children still work — the marker lies exactly as Module 3 Ticket 1's did, by a new mechanism (nobody collected the verdicts, or even the finishes). The defunct pile is the same omission wearing its zombie costume: children finished, verdicts never reaped — and since this parent script is long-running between rounds, the zombies accumulate visibly.

Work the steps: ps -o pid,ppid,stat,comm | grep Z — common PPID = the backup script. Read the script: four &, zero wait.

Fix and prevention: collect properly: pids=(); dump1 & pids+=($!); … ; fail=0; for p in "${pids[@]}"; do wait "$p" || fail=1; done; [ "$fail" -eq 0 ] && touch .success — parallel speed and honest verdicts and reaped children, one pattern. The speedup was never the problem; the missing bookkeeping was.

🎓 Ticket 3 — "A deploy helper does cd \"$RELEASE_DIR\" && unpack && cd - — and once a month, when unpack fails oddly, the rest of the script runs in the wrong directory and litters files into the releases tree."

Diagnosis. The cd-and-return anti-pattern: when unpack fails, the && chain skips cd -, and the script continues wherever it stood — every subsequent relative path lands in the wrong place. The return trip is exactly the fragile part (Module 3's chain semantics meeting directory state).

Work the steps: simulate: cd /tmp && false && cd -; pwd — you are still in /tmp. One line reproduces a month of mystery.

Fix and prevention: the deliberate subshell (E1): ( cd "$RELEASE_DIR" && unpack ) — the directory change is scoped; the parent never moved, so there is no return trip to forget. Rule for review: any cd not inside ( ) (or not the script's single working-directory setup at the top) must justify itself. Subshells-for-scoping is the rare place where spawning a process is the safety feature.

🎓 Ticket 4 — "Our container keeps ignoring docker stop and getting SIGKILLed after the 10-second grace period. The entrypoint is entrypoint.sh, which ends with: python3 server.py."

Diagnosis. The missing exec (Part C's interview answer, in the wild): the entrypoint bash stays alive as PID 1 with python as its child. docker stop sends its stop signal to PID 1 — the bash — which (as you'll formalize in Module 13) does not forward it to the child while waiting; the app never hears the request, grace expires, everyone gets SIGKILLed. Unclean shutdowns, dropped connections, the works.

Work the steps: docker exec <ctr> ps -o pid,ppid,comm — PID 1 is bash, python is its child. That topology is the bug.

Fix and prevention: last line becomes exec python3 server.py — the bash replaces itself; python is PID 1; signals arrive directly; exit codes flow undiluted. Add the E1 rule to the container base-image docs: entrypoint scripts end in exec (or use an init like tini when multiple children are genuinely needed). One keyword, one topology change, whole failure class gone.

E4. Documentation reference

TopicAuthoritative referenceWhat you'll find there
Subshells and execution environmentsBash manual — Command Execution EnvironmentExactly which constructs spawn subshells, and what a subshell inherits
( ) vs { } groupingBash manual — Command GroupingThe subshell/current-shell grouping pair, with the brace syntax rules
Jobs: &, jobs, fg, bg, waitBash manual — Job Control BasicsThe job table and the control commands around it
Process substitutionBash manual — Process Substitution<( ) and >( ) — commands as filenames
Inspecting processes; surviving logoutps(1) — man7.org · nohup(1) — man7.orgProcess states and columns; nohup's exact redirection behavior

E5. Self-assessment

Answer from memory, out loud or on paper. Every answer is in this module.

  1. What is a process, what is a PID, and what does the parent-child tree have to do with Module 2's briefing pack?
  2. Name the four constructs that run in subshells. Which everyday two are the "invisible" ones?
  3. Subshell vs child script: what does each see of your variables, and why (fork vs fork+exec)?
  4. $$ vs $BASHPID inside a subshell — which changes?
  5. ( cd /tmp; x=1 ) vs { cd /tmp; x=1; } — aftermath of each?
  6. Why does cmd | while read lose your counter? Give both escapes and say when each applies.
  7. What does diff <(cmd1) <(cmd2) do mechanically — what do the parentheses become?
  8. What exactly does & do, where does the PID go, and why must you grab it immediately?
  9. What does wait "$pid" return, and what is wait -n for? Sketch the bounded fan-out pattern.
  10. What is a zombie precisely, what creates a growing pile of them, and why can't you kill them?
  11. What does exec command do to the current process, and name both production uses (wrapper tails; stream rewiring).
  12. Three sleeps of 0.3s: sequential vs parallel wall-clock — and why does near-zero user time predict good parallel returns for fleet work?

E6. Sources

Interview questions in this module were captured verbatim from:

Zero To Mastery — Bash Interview Prep: 31 Essential Questions and Answers — "What is a subshell? How do you use subshells in Bash?", "How do you manage background processes in Bash?" (published June 18, 2026)

Edureka — Top 60 Shell Scripting Interview Questions and Answers — "What are the various stages of a Linux process it passes through?", "How to print PID of the current shell?", "Suppose you execute a command using exec, what will be the status of your current process in the shell?" (page updated Dec 9, 2024)

A note on the corpus: the subshell/pipeline-variable gotcha — this module's centerpiece — is among the most-asked scenario questions in real screens and among the least-published as fixed wording; the two scenario questions above are labeled accordingly. The process-substitution and fork/exec material appears in tutorials rather than question lists. No questions were invented. All expected-result outputs were produced by running the commands on Ubuntu 24.04 / bash 5.2; PIDs, paths, and small timing variations are flagged as machine-dependent.

Next: processes can now be started, copied, and awaited — but not yet spoken to. Signals are the messages processes send each other: what Ctrl-C really is, why kill rarely kills, and how trap lets your scripts clean up even while dying: Module 13 — Signals and Traps.
Spotted a mistake or want something added? Send me a note.