Operating Systems Essentials
- 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
| Spaced-rep warm-up: due cards (indexes, transactions, joins) | 10 min |
| ELI5 + tech read: rooms, roommates, and numbered phone lines | 18 min |
| Guided: signals & process tree, memory & descriptors | 42 min |
| Practice: the mystery hog incident drill | 18 min |
| Project: graceful-shutdown harness | 22 min |
| Quiz + flashcards | 10 min |
Builds on: Day 6 β The terminal & Linux Β· Day 16 β Logging & exit codes Β· Day 5 β Files & errors
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.
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
Meet your processes
20 min- Create
os_lab.pywith the starter'sburnerscript section β it prints its PID and burns CPU. Run it in one terminal; in another, find it withps aux | grep burnerand watch it intop(press P to sort by CPU). Confirm the PID matches what it printed. - Kill it politely:
kill <PID>(SIGTERM). Note the exit. Restart it, now with the SIGTERM handler enabled (flip HANDLE_TERM to True): this timekilltriggers the cleanup message and a tidy exit 0 β the behavior Docker will expect of your services. - Make it ignore SIGTERM (flip IGNORE_TERM):
killnow does nothing. Escalate:kill -9ends it instantly β no cleanup line printed. Explain why no handler ran: SIGKILL never reaches the process; the kernel simply stops scheduling it. - Spawn children with the starter's
subprocesssection: parent prints its PID, launches twosleep 30children, andps --forest(orpstree -p) shows the family tree. Kill the parent and check what happened to the children. - Write in your notes: the three signals you now know, who can handle them, and which one Docker sends first.
Memory, descriptors & the evidence trail
22 min- 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
topwhile it builds, then free it withdeland note that RES does not always shrink (allocators keep pages). - 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 withresource.getrlimit. - 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).
- 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.
On your own
Incident drill: the mystery hog
18 minSimulate 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.
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.
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.
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
- OSTEP β the scheduling chapters β β How the kernel decides who runs next: MLFQ and fairness. Optional but it makes top's numbers (load average, %CPU) meaningful.
- 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
β 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