Skip to content

Module 2: How programs run

Use the task tracker from Module 1 as a process you can observe. The goal is to connect source code to the operating-system and hardware resources it consumes—not to memorize component catalogues.

Learning outcomes

By the end of this module, you should be able to:

  • trace a program from shell command to running process;
  • distinguish a program, process, thread, and system call;
  • explain virtual memory, stack, heap, resident memory, and storage;
  • separate CPU-bound work from waiting for I/O;
  • inspect process state, open files, resource use, and system calls;
  • use measurements to form and test a debugging hypothesis.

Mental model

When you run the task tracker:

shell
  → asks the kernel to create a process
  → runtime loads code and libraries into virtual memory
  → CPU executes instructions from runnable threads
  → program requests file I/O through system calls
  → kernel and filesystem move data to or from storage
  → process exits with a status code

Keep these concepts separate:

Concept Meaning
Program Code and data stored as files
Process A running instance with an identity, address space, resources, and permissions
Thread An execution stream within a process; threads share process resources
System call A controlled request from user-space code to the kernel
Virtual memory Per-process address space mapped by the OS to memory or other backing
Stack Function-call frames and local execution state for a thread
Heap Dynamically allocated objects managed by the runtime or allocator
File descriptor A process-local handle for an open file, pipe, socket, or similar resource

Before measuring

Use a Linux environment when possible because /proc and strace make the exercises visible. On macOS, ps, lsof, and time still cover most outcomes; system-call tracing differs and may require elevated permissions.

Record the operating system, Python version, processor architecture, and exact command with every observation. Measurements without their environment are difficult to reproduce.

Experiment 1: Process identity and lifecycle

Start a process that remains alive long enough to inspect:

python -c 'import os, time; print(os.getpid(), flush=True); time.sleep(60)' &
runtime_pid=$!
ps -o pid,ppid,state,rss,vsz,time,command -p "$runtime_pid"
kill "$runtime_pid"
wait "$runtime_pid"

Explain each observation:

  • PID identifies this process instance; PPID identifies its parent.
  • state describes whether it is running, runnable, sleeping, stopped, or finished.
  • RSS estimates pages currently resident in physical memory.
  • VSZ describes virtual address space and is not the same as RAM consumed.
  • CPU time grows only while the process executes on a CPU.

Run the process twice. The program is the same, but the process identity is different.

Experiment 2: CPU time versus waiting

Compare a computation with a sleep:

time python -c 'sum(i * i for i in range(10_000_000))'
time python -c 'import time; time.sleep(2)'

Interpret real, user, and sys time:

  • real: elapsed wall-clock time;
  • user: time executing user-space instructions;
  • sys: time executing kernel work on behalf of the process.

The sleep has substantial elapsed time but little CPU time. Do not call a program CPU-bound merely because it feels slow; compare elapsed time with CPU time and inspect what it is waiting for.

Experiment 3: Files and system calls

Run ./tasks list, replacing ./tasks with your actual Module 1 entry point. While it is running—or after adding a temporary pause—inspect its open files:

lsof -p PROCESS_ID

On Linux, trace relevant system calls:

strace -f -e trace=openat,read,write,close,rename ./tasks list

Find the operations that:

  1. load the Python runtime and libraries;
  2. open the JSON data file;
  3. read its bytes;
  4. write output to the terminal;
  5. replace the data file after a mutation.

There will be more calls than your source code suggests because the runtime performs work on your behalf. A library function is not necessarily one system call, and many function calls never enter the kernel.

Experiment 4: Virtual and resident memory

Create a temporary probe:

import os
import time
import tracemalloc

tracemalloc.start()
print(f"pid={os.getpid()}", flush=True)
values = list(range(1_000_000))
current, peak = tracemalloc.get_traced_memory()
print(f"python_current={current} python_peak={peak}", flush=True)
time.sleep(60)

Inspect it with ps before and after the allocation, then compare RSS with tracemalloc output. They need not match: tracemalloc tracks Python allocations, while process memory also includes the interpreter, native libraries, stacks, mapped files, allocator overhead, and shared pages.

Delete the list and repeat if you are curious. A language runtime may retain freed memory for reuse instead of immediately returning it to the OS.

Experiment 5: Stack and heap

Use a debugger or traceback to inspect a nested chain of function calls in the task tracker. Each active call has a frame containing execution state. Task dictionaries, strings, lists, and parsed JSON are dynamically allocated objects.

Answer these questions from evidence:

  • Which frames are active when the persistence function reads JSON?
  • Which objects remain reachable after that function returns?
  • What changes when an exception unwinds the call stack?
  • Why can a traceback explain control flow but not total memory use?

The words “stack” and “heap” describe useful regions and allocation behavior, but Python adds runtime-managed frames and objects. Do not assume its implementation matches the simplest diagram exactly.

Failure investigation

Make the task data file readable but not writable, then attempt a mutating command. Restore its permissions immediately afterward.

Investigate in this order:

  1. capture the command, error, and exit status;
  2. verify the process identity and effective user;
  3. inspect the file path, owner, and permissions;
  4. trace the failing file operation where tracing is available;
  5. identify which layer rejected the operation;
  6. verify that the original data remains intact.

The useful explanation is not “Python failed.” The program requested an operation, the kernel checked the process credentials against filesystem permissions, and the operation was denied.

Evidence to keep

Add a short runtime-notes.md to the project containing:

  • one annotated ps observation;
  • the CPU-versus-waiting comparison;
  • the relevant portion of one file-operation trace;
  • the memory probe results and why its two measurements differ;
  • the permission-failure diagnosis;
  • environment and commands needed to reproduce each result.

Keep raw output only when it supports a conclusion. The notes should explain observations, not dump every line a tool produced.

Completion check

You are ready for the next module when you can:

  • draw the path from shell to process, runtime, system call, kernel, and storage;
  • explain why two runs have different PIDs but execute the same program;
  • distinguish elapsed, user, and system time from your measurements;
  • explain why VSZ, RSS, and Python-tracked allocations differ;
  • identify the system calls associated with reading and replacing the task file;
  • diagnose the permission failure without changing code blindly;
  • distinguish a process from its threads and shared resources.

Read only when needed

Continue with Module 3: Data structures and algorithms after the completion check passes.