Computing FoundationsInside the Machine › Day 7

Hands-on lab — Day 7: Processes, Threads, and Scheduling

Commands

Setup

cd labs/sections/computing-foundations/day-007-processes-threads-and-scheduling

Run

bash examples/process_playground.sh
bash starter/process_playground.sh

Test

bash tests/run_tests.sh

File tree

examples/process_playground.sh
expected-output/FIELDS.md
expected-output/sample-run-macos.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/process_playground.sh
starter/process-worksheet.md
tests/run_tests.sh
troubleshooting.md

Lab README

Day 007 lab — Processes in the Wild: Spawn, Watch, Signal

Lesson

Purpose

Day 7's lesson explains processes, threads, and the scheduler. This lab puts your hands on the controls: you count the process population on your own machine, trace your shell's ancestry through the process tree, spawn a background process, watch it through both the shell's eyes (jobs) and the system's (ps), terminate it politely with a signal, and verify its death by exit code — the exact spawn-observe-signal-verify cycle you will use on real training jobs later in the course.

Learning objectives

  • Start a background process with & and capture its PID from $! immediately.
  • Observe a process with jobs -l and ps -o pid,ppid,stat,command, and read its state.
  • Trace a parent chain: your script's PID, its parent shell, and upward toward PID 1.
  • Terminate a process politely with SIGTERM, collect its exit status with wait, and explain why it is 143.
  • Observe the difference between kill (SIGTERM) and kill -9 (SIGKILL) on processes you started.
  • Complete a working shell script by filling in five well-specified exercises and pass an automated test suite.

Prerequisites

  • The Day 7 lesson (read it first — it explains every concept this lab exercises).
  • The Day 1 lab (comfort running commands in a terminal).
  • A terminal: Terminal.app (macOS), any terminal (Linux), or WSL (Windows).

Supported operating systems

  • macOS — fully supported (tested on macOS with Apple Silicon).
  • Linux — fully supported (any distribution; only standard ps, kill, sleep are used).
  • Windows — run the scripts unmodified inside WSL; native PowerShell equivalents (Get-Process, Stop-Process) exist but are not covered by the tests.

Hardware requirements

Any computer that runs a shell. The lab spawns nothing heavier than a sleep, which uses effectively zero CPU and memory.

Required software

  • bash (3.2 or newer — preinstalled on macOS and Linux).
  • Standard OS utilities only: ps, kill, sleep, wc, tail, tr, sed, date, plus the shell built-ins jobs, wait, and trap. All preinstalled.

Free and open-source options

Everything in this lab is free: bash and every command used ship with your operating system. No account, API key, or purchase is needed — this is true of every lab in the course wherever possible, and any exception is labelled.

Installation

None. Clone the repository (or copy this directory) and you are ready:

cd labs/sections/computing-foundations/day-007-processes-threads-and-scheduling

File structure

day-007-processes-threads-and-scheduling/
├── README.md                          ← you are here
├── metadata.yml                       ← machine-readable lab metadata
├── starter/
│   ├── process_playground.sh          ← YOUR working file (5 exercises)
│   └── process-worksheet.md           ← worksheet for the practice assignment
├── examples/
│   └── process_playground.sh          ← completed reference implementation
├── tests/
│   └── run_tests.sh                   ← automated checks
├── expected-output/
│   ├── sample-run-macos.txt           ← real captured run (macOS, Apple Silicon)
│   └── FIELDS.md                      ← required fields; why your PIDs will differ
├── requirements/
│   └── README.md                      ← dependency statement (none beyond the OS)
├── troubleshooting.md
└── security.md

How to run

From this directory:

## 1. See the finished result first
bash examples/process_playground.sh

## 2. Your task: complete the five exercises in the starter, then run it
bash starter/process_playground.sh

## 3. Check your work
bash tests/run_tests.sh

What the commands do

  • bash examples/process_playground.sh — runs the reference script: counts every process (ps -e | tail -n +2 | wc -l), prints its own PID ($$) and walks its parent chain with ps -o pid,ppid,command, starts sleep 300 &, captures the worker's PID from $!, shows the worker via jobs -l and ps -o pid,ppid,stat,command, sends SIGTERM with kill, collects exit status 143 with wait, and proves the PID is gone with ps -p.
  • bash starter/process_playground.sh — the same skeleton with five exercises left for you; each exercise comment names the exact command to use. The script runs safely even before you start (the spawn/kill steps activate once Exercise 3 is done).
  • bash tests/run_tests.sh — runs both scripts and checks: exit code 0, header/footer, a real numeric process count, that the script spawned its own worker and captured its PID, that the worker died to SIGTERM (exit status 143), that the script verified the death, and — independently — that the worker's PID no longer exists afterward (no orphaned sleeps). The tests only read the process table; they never signal anything themselves.

Expected output

See expected-output/sample-run-macos.txt — a real captured run:

=== Process Playground ===
Generated on: 2026-07-12
Processes running right now: 912
This script's PID: 25751
Parent chain (this script and up to 5 ancestors):
  25751     1 bash examples/process_playground.sh
      1     0 /sbin/launchd
Started background worker: sleep 300 (PID 25776)
The worker as seen by jobs:
  [1]+ 25776 Running                 sleep 300 &
The worker as seen by ps:
    PID  PPID STAT COMMAND
  25776 25751 SN   sleep 300
Sent SIGTERM to 25776; wait reported exit status 143 (143 = 128 + 15, death by SIGTERM)
Verified: PID 25776 is gone
=== End of playground ===

Every PID (and the process count) will differ on your run — the kernel assigns PIDs at spawn time. What must match is the shape: the worker's PPID equals the script's PID, the exit status is exactly 143, and the verified-gone line names the spawned PID. Run from an interactive terminal, your parent chain will be longer than the sample's (your shell, your terminal app, up toward PID 1); the sample was captured from a detached run, so its chain ends at /sbin/launchd. expected-output/FIELDS.md lists exactly which fields must appear.

Validation steps

  1. Run bash starter/process_playground.sh — it must exit without errors.
  2. Confirm no line contains unknown.
  3. Confirm the worker's PPID in the ps output equals the script's own PID — parent and child.
  4. Confirm the exit status line reports 143, and the final line reads Verified: PID <yours> is gone.
  5. Run pgrep -l sleep — no sleeper of yours should remain.
  6. Run the tests (next section) — all checks must pass.

Tests

bash tests/run_tests.sh

Expected final line: 17 checks, 0 failure(s). (12 strict checks against the reference script, and 5 structural checks against your starter — which become 12 strict checks once you have replaced every unknown, for 24 checks, 0 failure(s). in total). The command exits 0 on success, non-zero on any failure, so it can run in CI.

Cleanup

The scripts clean up after themselves: each sets a trap so its own worker is terminated even if the script fails midway, and the tests verify no sleeper survives. If you experimented manually and lost track of a sleeper, see the orphaned-sleeps entry in troubleshooting.md — or simply wait: a sleep 300 exits by itself within five minutes. To reset your work: git checkout -- starter/process_playground.sh.

Troubleshooting

See troubleshooting.md for the full list (stale PIDs, wait refusing foreign children, per-shell jobs, orphaned sleeps and cleanup, 137-versus-143 confusion, WSL notes).

Security notes

See security.md. Short version: the scripts only ever signal the one process they started themselves, captured from $!never signal a process you did not start, never sudo kill, and treat kill -9 as a last resort because it forbids cleanup.

Extension exercises

  1. Add a second worker to your completed script (sleep 300 & again, capturing a second PID), terminate one with kill and the other with kill -9, and print both exit statuses (143 versus 137) with a line explaining the 128 + signal-number arithmetic.
  2. Freeze and thaw: send your worker kill -STOP <PID>, show its T state in ps -o stat, resume it with kill -CONT <PID>, then terminate it politely.
  3. Make the scheduler visible: run yes > /dev/null & (a pure CPU burner), watch it with top, add more burners than you have cores, observe the sharing, then terminate every burner you started — politely, by the PIDs you captured.
  • Previous day: Day 6 — Operating Systems: What They Do and Why (labs/sections/computing-foundations/day-006-operating-systems-what-they-do-and/).
  • Next day: Day 8 — the first day of Week 2, The Command Line (labs/sections/computing-foundations/command-line/week-02/, to be written).
  • Weekly project: Annotated Machine Teardown (labs/sections/computing-foundations/project/).

Expected output

FIELDS.md

# Required output fields (all platforms)

A correct run of `process_playground.sh` prints, in order:

1. `=== Process Playground ===`
2. `Generated on: YYYY-MM-DD`
3. `Processes running right now: <positive integer>`
4. `This script's PID: <positive integer>`
5. A parent chain of one or more `pid ppid command` rows
6. `Started background worker: sleep 300 (PID <positive integer>)`
7. The worker in `jobs` output (`... Running ... sleep 300 &`)
8. The worker in `ps` output (a row with PID, PPID, STAT, and `sleep 300`;
   the worker's PPID equals the script's PID from line 4)
9. `Sent SIGTERM to <PID>; wait reported exit status 143 (143 = 128 + 15, death by SIGTERM)`
10. `Verified: PID <PID> is gone`
11. `=== End of playground ===`

`sample-run-macos.txt` in this directory is a real captured run (macOS,
Apple Silicon, 2026-07-12). **Every PID differs on every run** — PIDs are
assigned by the kernel at spawn time, so your numbers (and your process
count) will not match the sample, and that is expected. What must match is
the *shape*: the worker's PPID equals the script's PID, the exit status is
exactly 143, and the final verification line names the same PID that was
spawned. The parent chain also differs by how you launch the script: from
an interactive terminal it climbs through your shell and terminal app; the
sample was captured from a detached run, so its chain is short and ends at
the system's ancestral process (`/sbin/launchd` on macOS; on Linux you
would see `systemd` or `init` as PID 1). The `STAT` column may read `S` or
`SN` on macOS and `S` on Linux — both are the sleeping/waiting state.

sample-run-macos.txt

=== Process Playground ===
Generated on: 2026-07-12
Processes running right now: 912
This script's PID: 25751
Parent chain (this script and up to 5 ancestors):
  25751     1 bash examples/process_playground.sh
      1     0 /sbin/launchd
Started background worker: sleep 300 (PID 25776)
The worker as seen by jobs:
  [1]+ 25776 Running                 sleep 300 &
The worker as seen by ps:
    PID  PPID STAT COMMAND
  25776 25751 SN   sleep 300
Sent SIGTERM to 25776; wait reported exit status 143 (143 = 128 + 15, death by SIGTERM)
Verified: PID 25776 is gone
=== End of playground ===

Source files

examples/process_playground.sh (2203 bytes)
#!/usr/bin/env bash
# Day 007 lab — completed reference implementation.
# Spawns a background process, observes it with jobs and ps, terminates it
# politely with SIGTERM, and verifies it is gone. Also counts the machine's
# processes and prints this script's own PID and parent chain.
#
# Safety rule (see security.md): this script only ever signals the one
# process it started itself, whose PID it captured from $!.
set -u

echo "=== Process Playground ==="
echo "Generated on: $(date '+%Y-%m-%d')"

# --- 1. How many processes exist right now? ---------------------------------
process_count="$(ps -e | tail -n +2 | wc -l | tr -d ' ')"
echo "Processes running right now: ${process_count}"

# --- 2. Who am I, and who spawned me? ----------------------------------------
echo "This script's PID: $$"
echo "Parent chain (this script and up to 5 ancestors):"
pid=$$
depth=0
while [ -n "${pid}" ] && [ "${pid}" -gt 0 ] && [ "${depth}" -lt 6 ]; do
  ps -o pid=,ppid=,command= -p "${pid}" 2>/dev/null | sed 's/^/  /'
  next="$(ps -o ppid= -p "${pid}" 2>/dev/null | tr -d ' ')"
  if [ -z "${next}" ] || [ "${next}" = "${pid}" ] || [ "${next}" -eq 0 ]; then
    break
  fi
  pid="${next}"
  depth=$((depth + 1))
done

# --- 3. Spawn a background worker and capture its PID from $! ----------------
sleep 300 &
worker_pid=$!
# Safety net: if this script dies early for any reason, take the worker along.
trap 'kill "${worker_pid}" 2>/dev/null' EXIT
echo "Started background worker: sleep 300 (PID ${worker_pid})"

echo "The worker as seen by jobs:"
jobs -l | sed 's/^/  /'
echo "The worker as seen by ps:"
ps -o pid,ppid,stat,command -p "${worker_pid}" | sed 's/^/  /'

# --- 4. Terminate it politely (SIGTERM, the kill default) --------------------
kill "${worker_pid}"
wait "${worker_pid}" 2>/dev/null
exit_status=$?
echo "Sent SIGTERM to ${worker_pid}; wait reported exit status ${exit_status} (143 = 128 + 15, death by SIGTERM)"

# --- 5. Verify the worker is really gone -------------------------------------
if ps -p "${worker_pid}" > /dev/null 2>&1; then
  echo "ERROR: worker ${worker_pid} is still alive" >&2
  exit 1
fi
echo "Verified: PID ${worker_pid} is gone"
echo "=== End of playground ==="
metadata.yml (673 bytes)
lesson_id: D007
day: 7
kind: process-tracing
languages: [bash]
setup_commands:
  - cd labs/sections/computing-foundations/day-007-processes-threads-and-scheduling
run_commands:
  - bash examples/process_playground.sh
  - bash starter/process_playground.sh
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'pgrep -l sleep  # check for orphaned sleeps YOU started, then kill only those PIDs'
  - 'git checkout -- starter/process_playground.sh  # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), bash tests/run_tests.sh → 17 checks, 0 failures'
requirements/README.md (704 bytes)
# Dependencies — Day 007 lab

**None beyond a POSIX shell.** This lab intentionally has zero installable
dependencies:

- `bash` ≥ 3.2 (preinstalled on macOS and every mainstream Linux distribution)
- Standard OS utilities only: `ps`, `kill`, `sleep`, `wc`, `tail`, `tr`,
  `sed`, `date` — all part of the base system on macOS and Linux, and the
  shell built-ins `jobs`, `wait`, `trap`, and the `$!`/`$$` variables.

Windows users: run everything inside WSL (any distribution) — the scripts
use portable `ps -o` column syntax that works identically on macOS and
Linux. There is deliberately no `requirements.txt`/`package.json` here;
process control is an operating-system skill, not a library.
starter/process_playground.sh (2656 bytes)
#!/usr/bin/env bash
# Day 007 lab — spawn, watch, and signal a process of your own.
#
# This starter script runs as-is, but five steps are left for you. Each
# exercise comment names the exact command to use — replace the `unknown`
# assignment (or follow the instruction) and run the script again. The
# completed reference version is in examples/process_playground.sh — try
# it yourself first.
#
# Safety rule (see security.md): only ever signal the process YOU started,
# whose PID you captured from $!. Never type PIDs you found elsewhere.
set -u

echo "=== Process Playground ==="
echo "Generated on: $(date '+%Y-%m-%d')"

# Exercise 1: count the processes on this machine using:
#   ps -e | tail -n +2 | wc -l | tr -d ' '
# (ps -e lists every process; tail -n +2 drops the header; wc -l counts lines)
process_count="unknown"
echo "Processes running right now: ${process_count}"

# Exercise 2: print this script's own PID (the special variable $$), then
# show its process-table row and its parent's row using:
#   ps -o pid,ppid,command -p $$
echo "This script's PID: $$"
echo "Parent chain (this script and its parent):"
# --- add your ps command below this line ---

# Exercise 3: start a background worker with `sleep 300 &` and IMMEDIATELY
# capture its PID from $! into worker_pid (worker_pid=$!). The skeleton
# below only runs the remaining steps once you have done so.
worker_pid="unknown"

if [ "${worker_pid}" != "unknown" ]; then
  # Safety net: if this script dies early, take the worker along.
  trap 'kill "${worker_pid}" 2>/dev/null' EXIT
  echo "Started background worker: sleep 300 (PID ${worker_pid})"

  echo "The worker as seen by jobs:"
  jobs -l | sed 's/^/  /'
  echo "The worker as seen by ps:"
  # Exercise 4a: find your worker in the process table using:
  #   ps -o pid,ppid,stat,command -p "${worker_pid}"
  # --- add your ps command below this line ---

  # Exercise 4b: terminate the worker POLITELY (SIGTERM, the default) using:
  #   kill "${worker_pid}"
  # then collect its exit status with:
  #   wait "${worker_pid}" 2>/dev/null; exit_status=$?
  # --- add your kill and wait commands below this line ---
  exit_status="unknown"
  echo "Sent SIGTERM to ${worker_pid}; wait reported exit status ${exit_status}"

  # Exercise 5: verify the worker is gone using:
  #   ps -p "${worker_pid}"
  # (it should find nothing — test with: if ! ps -p "${worker_pid}" > /dev/null 2>&1; then ...)
  # Print exactly:  Verified: PID ${worker_pid} is gone
  # --- add your verification below this line ---
else
  echo "Exercise 3 not completed yet: no background worker was started."
fi

echo "=== End of playground ==="
starter/process-worksheet.md (2178 bytes)
# Process worksheet — Day 007

Fill this in from a real session in one terminal on your own machine.
Commands to use are shown with each field; record what *your* system printed.

## 1. The process population

- Command used: `ps -e | tail -n +2 | wc -l`
- Total processes right now: `____`
- Roughly how many cores does your machine have (from your Day 1 worksheet)? `____`
- One sentence: how can that many processes share that few cores?

  > _your answer here_

## 2. Your shell and its ancestors

- Your shell's PID (`echo $$`): `____`
- Your shell's parent (`ps -o pid,ppid,command -p $$` — the PPID column): `____`
- The parent's own row (`ps -o pid,ppid,command -p <that PPID>`):

  ```text
  (paste the line here)
  ```

- In words: what program spawned your shell (a terminal app? another shell? a login manager?)

  > _your answer here_

## 3. One background-job lifecycle, observed

Run each step and record the evidence:

| Step | Command | What your system printed |
| --- | --- | --- |
| Start | `sleep 300 &` | job number and PID: `____` |
| Capture | `echo $!` | `____` |
| Shell's view | `jobs -l` | `____` |
| System's view | `ps -o pid,ppid,stat,command -p <PID>` | `____` |
| Signal | `kill <PID>` | (usually silent) |
| Exit status | `wait <PID>; echo $?` | `____` |
| Verify | `ps -p <PID>` | `____` |

- The `stat` column showed: `____` — which lifecycle state is that?
- The PPID of your sleep matched: `____` (compare with your `$$` above)

## 4. Polite versus forced: kill versus kill -9

Start two fresh sleepers (`sleep 300 &` twice, capturing each PID from `$!`
right away). Terminate the first with `kill <PID1>` and the second with
`kill -9 <PID2>`, collecting each exit status with `wait <PID>; echo $?`.

- Exit status after plain `kill` (SIGTERM): `____`
- Exit status after `kill -9` (SIGKILL): `____`
- What the shell printed for each (e.g. `Terminated` vs `Killed`):

  ```text
  (paste both lines here)
  ```

- Two or three sentences: what is the difference between the two signals,
  why does the exit-status arithmetic (128 + signal number) explain your
  two numbers, and when is each signal appropriate?

  > _your answer here_
tests/run_tests.sh (4145 bytes)
#!/usr/bin/env bash
# Tests for the Day 007 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Verifies that the completed reference script spawns its OWN background
# worker, finds it, terminates it with SIGTERM, and verifies its death —
# and, if the learner has finished the starter script, checks their version
# the same way. The test suite never signals any process the scripts did
# not start themselves.
set -u

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
failures=0
checks=0

check() {
  local label="$1" ok="$2"
  checks=$((checks + 1))
  if [ "${ok}" = "yes" ]; then
    echo "  ok: ${label}"
  else
    echo "  FAIL: ${label}"
    failures=$((failures + 1))
  fi
}

run_playground_checks() {
  local script="$1" strict="$2" output
  echo "Testing ${script} ..."
  if ! output="$(bash "${script}" 2>&1)"; then
    check "script exits successfully" "no"
    echo "${output}" | sed 's/^/    /'
    return
  fi
  check "script exits successfully" "yes"
  echo "${output}" | grep -q '^=== Process Playground ===$' && check "prints playground header" "yes" || check "prints playground header" "no"
  echo "${output}" | grep -q '^=== End of playground ===$' && check "prints playground footer" "yes" || check "prints playground footer" "no"
  echo "${output}" | grep -q '^Processes running right now:' && check "prints a process count line" "yes" || check "prints a process count line" "no"
  echo "${output}" | grep -q '^This script'"'"'s PID:' && check "prints its own PID" "yes" || check "prints its own PID" "no"

  if [ "${strict}" = "strict" ]; then
    # The process count must be a real positive integer, not 'unknown'.
    count="$(echo "${output}" | sed -n 's/^Processes running right now: //p' | head -n 1)"
    case "${count}" in
      '' | *[!0-9]*) check "process count is a positive integer" "no" ;;
      *) check "process count is a positive integer" "yes" ;;
    esac
    if echo "${output}" | grep -q "unknown"; then
      check "no value is left 'unknown'" "no"
    else
      check "no value is left 'unknown'" "yes"
    fi

    # The script must have spawned its own worker and captured a numeric PID.
    worker_pid="$(echo "${output}" | sed -n 's/^Started background worker: sleep 300 (PID \([0-9][0-9]*\)).*/\1/p' | head -n 1)"
    if [ -n "${worker_pid}" ]; then
      check "spawned its own background worker and captured its PID" "yes"
    else
      check "spawned its own background worker and captured its PID" "no"
    fi
    echo "${output}" | grep -q 'sleep 300' && check "found the worker with ps/jobs (sleep 300 visible)" "yes" || check "found the worker with ps/jobs (sleep 300 visible)" "no"

    # Polite termination: exit status 143 = 128 + 15 (SIGTERM).
    echo "${output}" | grep -q 'wait reported exit status 143' && check "worker died to SIGTERM (exit status 143)" "yes" || check "worker died to SIGTERM (exit status 143)" "no"
    echo "${output}" | grep -Eq '^Verified: PID [0-9]+ is gone$' && check "script verified its worker is gone" "yes" || check "script verified its worker is gone" "no"

    # Independent verification: the worker PID must no longer exist. (We only
    # *read* the process table here — the test signals nothing itself.)
    if [ -n "${worker_pid}" ]; then
      if ps -p "${worker_pid}" > /dev/null 2>&1; then
        check "worker PID ${worker_pid} really is gone (no orphaned sleep)" "no"
      else
        check "worker PID ${worker_pid} really is gone (no orphaned sleep)" "yes"
      fi
    fi
  fi
}

run_playground_checks "${lab_dir}/examples/process_playground.sh" strict

# The starter ships with 'unknown' values on purpose; once the learner has
# replaced them all, hold their script to the same strict standard.
if grep -q '"unknown"' "${lab_dir}/starter/process_playground.sh"; then
  echo "Note: starter/process_playground.sh still has unfilled exercises — testing structure only."
  run_playground_checks "${lab_dir}/starter/process_playground.sh" lenient
else
  run_playground_checks "${lab_dir}/starter/process_playground.sh" strict
fi

echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 007 lab

kill: (12345) - No such process

The PID is stale or mistyped. sleep 300 exits on its own after five minutes, and every run gets fresh PIDs — never reuse a PID from an earlier run or from the sample output. Start a new sleeper and capture $! immediately: sleep 300 & worker_pid=$!.

wait: pid 12345 is not a child of this shell

wait only collects children of the shell you call it from. If you started the sleeper in one terminal and typed wait in another (or inside a script, which is its own shell), it is not your child there. Run the whole lifecycle — spawn, observe, kill, wait — in one shell, start to finish.

jobs prints nothing even though my sleep is running

jobs is per-shell: it lists only the background jobs of the shell you type it in. Your sleeper still exists — find it system-wide with ps -p <PID>. Same rule as above: one terminal for the whole exercise.

Orphaned sleeps: I started sleepers and lost track of them

It happens — a closed terminal or a script that died before its cleanup. First, look: pgrep -l sleep lists every sleep process you can see, with PIDs. Identify the ones you started (on a shared machine, other users and system tools may legitimately run sleeps — check ownership with ps -o pid,user,command -p <PID>), then terminate only those, politely: kill <PID>. An orphaned sleep 300 is harmless — it uses no CPU and exits by itself within five minutes — so when in doubt, just wait it out. Both lab scripts set a trap ... EXIT so their own worker is taken down even if the script fails partway; that pattern is worth stealing.

The starter script says "Exercise 3 not completed yet"

Working as designed: the spawn/kill/verify steps only run after you replace worker_pid="unknown" with a real spawn (sleep 300 & then worker_pid=$! on the next line, inside the script).

Exit status is 0, not 143

Your wait collected the wrong thing, or the sleeper finished naturally before you killed it (five minutes pass quickly when reading). Rerun and kill promptly. If you see 137 instead of 143, you sent SIGKILL (kill -9) rather than SIGTERM — reread the lesson's signals table.

ps: illegal option or missing columns

Use the exact portable form ps -o pid,ppid,stat,command -p <PID>. Other spellings (ps aux variants, --forest) differ between macOS and Linux.

My parent chain looks different from the sample

Expected. The sample was captured from a detached run, so its chain ends at /sbin/launchd. Run from a normal terminal and you will see your shell, your terminal app or editor, and eventually PID 1. That difference is the lesson: every process has a parent, and the chain depends on who spawned whom.

Windows: bash is not recognized

Use WSL (wsl --install, then open Ubuntu). Native PowerShell has its own process cmdlets (Get-Process, Stop-Process), but this lab's scripts and tests assume a Unix-style shell.

Security notes

Security notes — Day 007 lab

  • The iron rule: never signal a process you did not start. Everything in this lab signals only the one sleep the script itself spawned, whose PID it captured from $! at the moment of creation. Typing kill with a PID you found lying around — in a tutorial, in old output, or by guessing — can terminate someone else's work or a system service. Before any manual kill, confirm with ps -o pid,user,command -p <PID> that the process is yours and is the one you think it is.
  • What the scripts do: read the process table (ps), spawn one sleep 300, send it SIGTERM, and verify it exited. They make no network connections, write no files, and change no settings. The test suite only reads the process table; it never signals anything itself.
  • Privileges: everything runs as your normal user, and that is itself a safety net — the kernel refuses to let you signal processes owned by other users. Nothing here needs sudo, and running kill with sudo removes that safety net; don't.
  • kill -9 is a last resort, not a habit. SIGKILL gives the target no chance to clean up — half-written files and stale locks are the typical wreckage. The lab teaches the professional order: SIGTERM, wait, then escalate only if truly necessary — and here the only target is ever a sleep you started seconds earlier.
  • PID reuse: PIDs are recycled by the kernel. A PID you wrote down minutes ago may now belong to a different process — one more reason to capture $! immediately and act on it promptly, never on stale numbers.
  • Reading before running: both scripts are short and commented — read them before executing, as with every lab in this course. A script that sends signals deserves an especially careful read.