Course outline · 0% complete

0/28 lessons0%

Course overview →

Capstone: build a mini process inspector

lesson 9-2 · ~14 min · 28/28

Every layer in one program

This capstone touches every layer of the course in a single program, with your code playing the role of the shell.

  1. Create a program by writing Python source code into a file on disk, from unit 7.
  2. Inspect the file by reading its metadata to prove it exists as bytes, from lesson 7-3.
  3. Run it as a child process with subprocess, exactly the way the shell does, from lesson 3-3.
  4. Capture what the child wrote to file descriptor 1, from lesson 8-2.
  5. Read its exit code and give a verdict, from lesson 3-3.
StepLayer exercised
1 and 2filesystem, bytes and metadata
3process creation
4file descriptors and pipes
5exit codes

The working version below does all five steps in order, and each print lines up with one numbered step above.

The inspector

The full inspector: it writes a child program to disk, checks its size in bytes, runs it as a separate process, then reports the child's output and exit code.

sys.executable is used instead of a literal "python3" so the child runs under the exact same interpreter as the parent. capture_output=True is what redirects the child's file descriptor 1 into a pipe the parent can read, rather than letting it print straight to the terminal.

import os
import subprocess
import sys

child_source = 'print("child reporting in")\n'
with open("child.py", "w") as f:
    f.write(child_source)

print("program on disk:", os.stat("child.py").st_size, "bytes")

result = subprocess.run([sys.executable, "child.py"],
                        capture_output=True, text=True)
print("child said:", result.stdout.strip())
print("exit code:", result.returncode)

Output

program on disk: 28 bytes
child said: child reporting in
exit code: 0

Notes

  • Until subprocess.run, child.py is a program (a file, doing nothing). During the run it is a process with its own PID and memory. Afterward only the exit code remains.

Inspecting a program that fails

The same five steps against a child that deliberately fails. bad.py contains only import sys and sys.exit(3), so it produces no output and exits with code 3.

import os
import subprocess
import sys

bad_source = 'import sys\nsys.exit(3)\n'
with open("bad.py", "w") as f:
    f.write(bad_source)

print("program on disk:", os.stat("bad.py").st_size, "bytes")

result = subprocess.run([sys.executable, "bad.py"],
                        capture_output=True, text=True)
print("exit code:", result.returncode)
if result.returncode == 0:
    print("verdict: ok")
else:
    print("verdict: failed")

Output

program on disk: 23 bytes
exit code: 3
verdict: failed

The source is 23 bytes because 'import sys\n' is 11 and 'sys.exit(3)\n' is 12, and result.returncode holds the child's exit code, compared against 0 for the verdict.

This is the case that matters in practice. A non-zero exit code is how a failing process reports trouble to whoever started it, and it is what a shell stores in $?, what && checks before running the next command, and what CI reads to decide whether a build passed.

The parent survives the child's failure untouched and simply reads the number, which is the isolation from unit 3 paying off in the most ordinary possible way.

Where to go from here

You now hold the mental models that make the rest of a software career less mysterious.

  • A debugger pauses a process the same way the scheduler does, and reads its stack frames, from unit 4.
  • Docker containers are ordinary processes wearing OS-enforced isolation, not tiny virtual machines.
  • Databases are careful choreography of buffers, fsync, and locks, from units 5 and 7.
  • async and await are a user-space answer to the same problem the scheduler solves in unit 6, which is never wasting time waiting.

When something weird happens, the course's questions are the tool. Which process is it, whose memory is involved, who is waiting on what, which layer buffered it, and what did the exit code say.

SymptomThe question that cracks it
output missingwhich layer buffered it
service hungwho is waiting on what
memory climbingwhat still holds a reference
build failing silentlywhat did the exit code say

Those questions are the skill, and they transfer to every language and every stack, because the machine underneath does not change.

Why a crashing child cannot hurt the inspector

When the inspector's child process crashes badly, the parent keeps running fine and simply reads a non-zero exit code, because processes have isolated memory.

Process isolation from lessons 3-1 and 4-1 gives the child its own address space, so its death cannot corrupt the parent. The parent collects a non-zero exit code as in lesson 3-3 and carries on.

Unit of concurrencyShares memorySurvives a sibling crash
processnoyes
threadyes, the heapno

Threads would not enjoy this protection, which is exactly the trade from unit 5. That is also the reason supervisors, browsers, and CI runners are built out of child processes rather than threads: the isolation converts a crash into a number.