Operating Systems: Three Easy Pieces 1


This is inspired by teachyourselfcs. OSTEP is one of those books that explains complex stuff in a simple manner.

What are these chapters about?

Ch 1–5 are basically the OS “origin story”:

  • why OS exists at all
  • what a process really is
  • how the OS creates/runs programs (fork/exec/wait)
  • how we fake “many CPUs” on one CPU (scheduling / time-sharing)
  • why the OS draws a hard line between safe and powerful (user vs kernel)

The crux

OS is a resource manager. It virtualize the hardware (CPU/mem/disk) without letting programs wreck each other or the system.


Key definitions to rememeber

  • Abstraction: a nicer interface over ugly reality; fundamental idea to CS; makes system convenient and easy to use.
  • Virtualization: takes a physical resource (CPU, memory, and disk etc.) & converts them into a virtual form.
  • Process: a running program + the machine state needed to keep it running.
  • Machine state (what makes a process):
    • Memory / address space: process’s private stuff -> code + data it can access
    • Registers: CPU state the program is using
    • Special registers:
      • Program Counter/IP: where we are in the code (what runs next)
      • Stack Pointer/FP: stack management; where we are on the stack (function calls, locals, returns)
    • I/O state: what it has open / connected (files, stdin/stdout, etc.)
  • File system: OS layer that manages persistent storage (disk/SSD) and controls access.

Key insights

  • CPU, memory, disk are the real physical resources. OS makes them feel virtual so multiple programs can coexist.
  • Each process gets its own “fake” private memory (virtual address space). Two processes can both use the same address and it’s fine- OS maps them to different physical memory behind the scenes.
  • We get “many CPUs” via time-sharing: run process A for a slice, then switch to B, etc. (and it’s fast enough that it looks parallel).
  • The OS has to track a bunch of bookkeeping to pull this off:
    • a process list / task list
    • one record per process: PCB (process control block)
  • PCB matters because it stores enough info to stop/resume a process:
    • saved register context (used for context switches)
    • process state (not just running/ready/blocked)
  • There are extra states that matter:
    • embryo (being created)
    • sleeping (blocked)
    • runnable
    • running
    • zombie (dead but not cleaned up yet)
  • Zombie state exists so the parent can still read the child’s exit code. wait() is basically the parent saying “ok I got it, you can clean up now”.

Mechanisms / APIs (the concrete stuff)

System calls (user mode → kernel mode bridge)

Apps shouldn’t be able to read any bytes from disk whenever they want (privacy/security would collapse). So the file system isn’t just a normal library you link against- access has to be mediated by the OS via system calls.

  • Goal: let programs do privileged things (files, I/O, memory stuff) without giving them god-mode.
  • How it works: A system call is the controlled bridge between the two: it transfers control into the OS and temporarily (trap handler) raises the hardware privilege level only for that OS code, with checks/permissions enforced along the way.
  • Gotcha: normal function calls don’t change privilege. Syscalls do.

Program -> Process (loading + starting)

  • OS loads the program from disk into memory (code + static data).
  • sets up the stack (and fills main(argc, argv)).
  • sets up a small heap (grows later when you malloc()).
  • sets up basic I/O (stdin/stdout/stderr).
  • then jumps to main() and you’re “running”.

Eager vs lazy loading:

  • old/simple OS: load everything upfront
  • modern OS: load on demand (paging/swapping magic later)

fork() (make a child)

  • child is basically a copy (its own memory, registers, PC).
  • but the return value differs:
    • parent gets child PID
    • child gets 0 (so both can tell who they are)

exec() (run a new program)

  • replaces the current process image with a new executable.
  • after successful exec(), the old code doesn’t keep going.

wait() (cleanup + exit code)

  • parent waits for child to finish
  • grabs exit status
  • tells OS it can clean up (prevents zombies piling up)

Why fork() + exec() is split (shell reason)

This is the main motivation:

  • shell does fork()
  • child does some setup (redirection/pipes/env)
  • then exec() to actually run the command
  • parent wait()s

Without that gap between fork and exec, shells would be way harder.


Common pitfalls

  • thinking “process = program” (it’s program + state)
  • assuming virtual addresses are globally real (they’re per-process)
  • assuming the parent/child run order after fork() (nope)
  • forgetting wait() and leaving zombies around
  • forgetting that syscalls are special (privilege boundary)

TL;DR

  1. OS virtualizes hardware resources so multiple programs can run safely.
  2. A process = program + machine state (memory, regs, I/O).
  3. Syscalls are the controlled way to do privileged operations.
  4. fork/exec/wait explains how UNIX process management (and shells) work.
  5. Scheduling/time-sharing is how we fake “many CPUs”.