Day 39 Β· The hotel manager

Operating Systems Essentials

You will be able to
  • Distinguish processes from threads in terms of memory and failure isolation
  • Explain virtual memory, the stack/heap split, and what the OOM killer does
  • Inspect and manage live processes with ps, top, and kill
  • Handle SIGTERM/SIGINT in Python and explain why SIGKILL cannot be handled
  • Trace a file descriptor from open() to the 0/1/2 convention and redirection
Today's ~120 minutes
Spaced-rep warm-up: due cards (indexes, transactions, joins)10 min
ELI5 + tech read: rooms, roommates, and numbered phone lines18 min
Guided: signals & process tree, memory & descriptors42 min
Practice: the mystery hog incident drill18 min
Project: graceful-shutdown harness22 min
Quiz + flashcards10 min

Builds on: Day 6 β€” The terminal & Linux Β· Day 16 β€” Logging & exit codes Β· Day 5 β€” Files & errors

The analogy

A grand hotel runs hundreds of guests on one building's worth of resources, and the manager's genius is ISOLATION plus ILLUSION. Each guest gets a room with the same layout β€” bed here, desk there β€” and no guest can wander into another's room or even knows the others exist. That is a process: your program, convinced it owns the whole machine, actually living in one room whose "addresses" (virtual memory) the manager privately maps onto real physical space. Guest 1's desk and guest 2's desk are both "the desk," in different rooms.

Threads are roommates: several workers sharing ONE room. They coordinate cheaply β€” just talk across the room β€” but they share everything, so one roommate's mess (a corrupted shared structure, a crash) is everyone's mess. Separate rooms are safer; roommates are faster to coordinate. That trade drives tomorrow entirely.

The manager also runs the switchboard. Guests do not grab resources directly; they ring the front desk (system calls). Every room has numbered phone lines β€” line 0 for incoming (stdin), line 1 for outgoing (stdout), line 2 for complaints (stderr) β€” which is why Day 6's redirection worked: you were rerouting numbered lines. And when the hotel truly runs out of space, the manager evicts someone. Abruptly. That is the OOM killer, and your training job on Day 88 should not be the loudest guest.

Why this matters on the job

Every mysterious production symptom bottoms out in OS concepts: "Killed" with exit code 137 is the OOM killer (you WILL meet it loading models on Day 103), "Too many open files" is file-descriptor leakage, a container that ignores docker stop for 10 seconds is a process mishandling SIGTERM, and 100% CPU with no progress is a scheduling story told by top. Day 40's concurrency choices (threads vs processes vs async) are unintelligible without today's process/thread model, and Day 148's containers are exactly this isolation machinery β€” namespaces and cgroups β€” productized. FDEs debug on customer machines where ps, top, and kill are the only tools you can assume exist.

Guided practice

guided 1

Meet your processes

20 min
  1. Create os_lab.py with the starter's burner script section β€” it prints its PID and burns CPU. Run it in one terminal; in another, find it with ps aux | grep burner and watch it in top (press P to sort by CPU). Confirm the PID matches what it printed.
  2. Kill it politely: kill <PID> (SIGTERM). Note the exit. Restart it, now with the SIGTERM handler enabled (flip HANDLE_TERM to True): this time kill triggers the cleanup message and a tidy exit 0 β€” the behavior Docker will expect of your services.
  3. Make it ignore SIGTERM (flip IGNORE_TERM): kill now does nothing. Escalate: kill -9 ends it instantly β€” no cleanup line printed. Explain why no handler ran: SIGKILL never reaches the process; the kernel simply stops scheduling it.
  4. Spawn children with the starter's subprocess section: parent prints its PID, launches two sleep 30 children, and ps --forest (or pstree -p) shows the family tree. Kill the parent and check what happened to the children.
  5. Write in your notes: the three signals you now know, who can handle them, and which one Docker sends first.
🐍 python β€” editable, runs in your browser
Ctrl/⌘+Enter runs · Tab indents · numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)
guided 2

Memory, descriptors & the evidence trail

22 min
  1. Stack vs heap, felt. In a REPL: a deliberately unbounded recursion raises RecursionError near 1000 frames (stack guard); building a list of 100 million ints eats gigabytes of heap β€” watch RES climb for the python process in top while it builds, then free it with del and note that RES does not always shrink (allocators keep pages).
  2. Descriptor census. Run the starter's fd section: it prints your process's open descriptors from /proc/self/fd β€” see 0, 1, 2 sitting there before you open anything. Open 10 files without closing; watch the census grow; close them; watch it shrink. Then read your limit with resource.getrlimit.
  3. The leak, simulated safely. The starter lowers the fd limit to 64 for THIS process, then leaks descriptors in a loop until OSError "Too many open files" β€” catch it, print how many you got, and state the production moral (every leaked socket/file counts against this).
  4. Exit-code forensics. From the shell: run python -c "import sys; sys.exit(3)"; echo $? β†’ 3. Then kill a sleeping python with -9 from another terminal and check $? β†’ 137. Decode: 128 + 9. You can now read the two most common mystery exits (137 OOM/SIGKILL, 139 segfault) from the number alone.
🐍 python β€” editable, runs in your browser
Ctrl/⌘+Enter runs · Tab indents · numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)

On your own

Incident drill: the mystery hog

18 min

Simulate an on-call page. Copy the starter's burner into hog.py, but modify it to ALSO append 10 MB to a growing list every second (a CPU hog with a memory leak). Launch it detached (python hog.py &), close the terminal that started it if you like, and then β€” using only ps, top, and /proc β€” write down: its PID and parent PID, its CPU%, its RES memory trend over 30 seconds, and how many fds it has open. Then terminate it politely, verify it is gone, and write a four-line incident note: symptom, evidence, action, prevention.

Constraints: no killing by name (pkill) until you have identified the PID by evidence; the incident note must cite numbers you actually observed.

Hints: watch RES in top over time for the leak; cat /proc/<PID>/status shows VmRSS; ps -o pid,ppid,%cpu,rss,cmd -p <PID> is a compact one-liner.

Ship before you stop

A graceful-shutdown harness you will reuse

Build graceful.py: a long-running "worker" that processes one queued job per second (simulated with sleep + a log line via Day 16's logging), and shuts down PROPERLY. Requirements: on SIGTERM or SIGINT it finishes the job in flight, logs how many jobs completed, closes its log file handler, and exits 0 β€” never mid-job. A --stubborn flag disables handling (for demonstrating kill -9). Include a README section: the exact kill commands to test each path, expected exit codes ($?), and the decoded meaning of 0, 130, 137. This harness is the shape of every service you will ship: Day 42's API, and the containers Docker stops on Day 148 get exactly 10 seconds of this politeness.

Rubric β€” check what you completed (0/5)

Common mistakes & misconceptions

  • Treating kill as "force kill." Plain kill sends SIGTERM β€” the polite, handleable request. kill -9 (SIGKILL) is the last resort, and cleanup code never runs under it.
  • Registering cleanup on SIGKILL and wondering why it never fires. SIGKILL is delivered by the kernel without the process ever seeing it β€” that is the design.
  • Reading VIRT in top as "memory used." VIRT is address space reserved; RES (resident) is actual RAM occupied β€” the number the OOM killer cares about.
  • Confusing exit code conventions: 137 is not "error 137" β€” it is 128 + signal 9, meaning killed (usually OOM or docker stop timeout). 130 is 128 + SIGINT.
  • Leaking descriptors in long-running services by skipping with-blocks "because the script is short." Services are never short; every socket counts against ulimit.
  • Blaming Python when the OOM killer strikes. dmesg (or journalctl -k) names the victim and the score β€” check the kernel log before theorizing.
Knowledge check

Q1. Your container ignores docker stop for 10 seconds, then dies losing its last batch of work. What is happening?

Q2. A training script prints "Killed" and the shell reports exit code 137. Most likely cause and first place to check?

Q3. Threads vs processes β€” which statement is correct?

Go deeper β€” curated resources

bookOSTEP β€” Processes & Address Spaces chapters (free book) β†—35 mincourseMIT Missing Semester β€” command environment & job control β†—20 mindocssignal β€” official Python docs β†—15 min
If you have a third hour
Done means
  • SIGTERM handled, ignored, and SIGKILLed β€” behaviors observed and explained
  • fd census run; the simulated leak hit the limit and was diagnosed
  • Incident note written with observed PIDs, CPU%, and memory numbers
  • graceful.py committed; exit codes 0/130/137 demonstrated and decoded
  • Quiz β‰₯ 2/3
How this connects

← Back: Day 6's pipes and redirection were file descriptors all along; Day 16's exit codes are what parents collect from dying children; Day 27's RecursionError was the stack guard rail introduced properly today.

Forward β†’: Tomorrow (Day 40) races threads and processes using today's model. Day 41's servers are processes juggling socket descriptors, Day 148's containers are this isolation machinery with namespaces and cgroups, and Day 155's GPU memory pressure replays the OOM story on different silicon.

Unlocks: D40 Concurrency & Async Python Β· D43 Networking β€” Packets to HTTPS Β· D148 Docker Fundamentals