All chapters in this track
- 01Chapter 1: Why OS Fundamentals Matter to Application Engineers
- 02Chapter 2: Processes, Threads, and the Process Lifecycle
- 03Chapter 3: The Memory Hierarchy: Registers to Disk
- 04Chapter 4: CPU Scheduling Algorithms
- 05Chapter 5: Memory Management I: Paging and Segmentation
- 06Chapter 6: Memory Management II: Virtual Memory and Page Replacement
- 07Chapter 7: File Systems: How Data Is Actually Stored and Retrieved
- 08Chapter 8: Interprocess Communication: Pipes, Shared Memory, Message Queues, Sockets
- 09Chapter 9: I/O Management and Interrupts
- 10Chapter 10: Deadlocks at the OS Level: Conditions, Prevention, Detection, Recovery
- 11Chapter 11: Virtualization and Containers: A Conceptual Introduction
- 12Chapter 12: When OS Abstractions Leak: Failure Modes Application Engineers Actually Hit
- 13Chapter 13: Walkthrough: What Happens When You Run a Program
- 14Chapter 14: Common OS Interview Questions: Paging, Page Faults, and Deadlock Scenarios
- 15Chapter 15: OS Concepts in System Design and Production Debugging
Chapter 13 of 15
Chapter 13: Walkthrough: What Happens When You Run a Program
Why this matters
Every concept in this book has been covered individually. This chapter's purpose is to run them all together, in sequence, against one concrete action: typing ./my_program and pressing Enter in a shell. Walking the full path end to end is both a genuinely useful mental model and one of the most common "tie it all together" interview questions in this subject.
Step 1: The shell prepares to run your program
[BEGINNER] The shell itself is just an ordinary process (Chapter 2). When you type a command and press Enter, the shell:
- Parses the command line into the program name and arguments.
- Searches the directories listed in the
PATHenvironment variable for an executable matching that name (unless you gave a full/relative path, like./my_program). - Prepares to create a new process to run it.
Step 2: fork() — cloning the shell
[BEGINNER] The shell calls fork() (Chapter 2 and Chapter 6), creating a child process that is, at this instant, an almost-exact copy of the shell itself — same code, same open file descriptors, same environment variables — made cheap via copy-on-write (Chapter 6), so no actual memory is duplicated yet.
Step 3: exec() — replacing the child's program image
[BEGINNER] The child process immediately calls a member of the exec() family (e.g., execve()), which replaces the child's entire memory image with the new program's code and data — this is the exact fork()+exec() pairing referenced in Chapter 12's fork-safety discussion, and it's precisely why that pairing is the standard, safe way to launch a new program from a running one:
1. The OS reads the executable file's header (e.g., ELF format on Linux) to learn
its structure: which parts are code, which are initialized data, how large the
uninitialized data (BSS) and stack should be, and where execution should begin.
2. The child's virtual address space (Chapters 5-6) is discarded and replaced:
a NEW page table is set up for the new program, with entries initially marked
"not present" — nothing is actually loaded into physical memory yet.
3. The OS sets the new program's entry point as where execution will resume.
4. Control transfers to the new program's code, starting from main() (technically,
from C runtime startup code that then calls main()).
Step 4: Demand paging loads the program, lazily
[BEGINNER] As main() starts executing, the CPU tries to fetch the very first instruction — which triggers a page fault (Chapter 6), because no pages have actually been loaded into physical memory yet. The OS's fault handler loads the needed page(s) from the executable file on disk, and execution proceeds. This repeats, page by page, only for the parts of the program actually touched — exactly Chapter 6's demand paging mechanism, now shown as the literal reason a program's very first instructions require page faults to even begin running.
Step 5: The program requests memory, and it's backed lazily too
[BEGINNER] If the program calls malloc() for heap memory, or declares local variables that grow the stack, the OS (via the C runtime and kernel together) extends the process's virtual address space — but, following the same demand-paging principle, physical frames are only actually assigned when those new pages are first touched, not the moment malloc() returns.
Step 6: The program performs I/O
[BEGINNER] Suppose the program reads a configuration file. It calls open(), then read() (Chapter 7's file system concepts: a directory lookup resolves the filename to an inode, which points to data blocks). If the data isn't already cached in memory, the process moves to the Waiting state (Chapter 2) while the disk performs the read; an interrupt (Chapter 9) signals completion, moving the process back to Ready, and eventually Running again once the scheduler (Chapter 4) picks it.
Step 7: The scheduler shares the CPU with everything else on the system
[BEGINNER] Throughout all of this, the program isn't the only thing running — the scheduler (Chapter 4) is constantly making decisions about which of potentially hundreds of ready processes/threads gets the CPU next, for how long, via context switches (Chapter 2) that save and restore register state (and, for a process switch, invalidate the TLB, per Chapter 5's translation-caching discussion).
Step 8: The program exits, and the OS reclaims everything
[BEGINNER] When the program finishes (returns from main(), or calls exit()), the OS:
- Closes any file descriptors the process still had open (Chapter 12's leaked-descriptor discussion is exactly about what happens when this step is relied upon instead of explicit cleanup — it does happen eventually, at process exit, but often too late if descriptors were being leaked continuously during a long-running process's life).
- Frees the process's physical memory frames back to the free pool (Chapter 6).
- Removes the process's entry from the process table, after the parent has had a chance to retrieve its exit status (until then, briefly, it exists as a "zombie" process — a fully finished process whose exit status hasn't yet been collected via
wait()/waitpid()by its parent). - Moves to the Terminated state (Chapter 2's process lifecycle, completed).
The full sequence, as one diagram
Shell (running process)
|
| fork() [Chapter 2, 6: copy-on-write clone]
v
Child process (identical copy, briefly)
|
| exec() [replaces memory image with new program]
v
New program's page table set up, all pages "not present" [Chapter 5, 6]
|
| first instruction fetch -> page fault -> demand-paged load [Chapter 6]
v
main() begins executing
|
| malloc() / stack growth -> lazily backed by physical frames on first touch [Chapter 6]
| open()/read() a file -> Waiting state -> interrupt on completion -> Ready [Chapter 2, 7, 9]
| (all the while: scheduler context-switches this process in and out) [Chapter 4]
v
Program returns / calls exit()
|
| OS closes descriptors, frees memory, reaps process table entry (via parent's wait())
v
Terminated [Chapter 2]
Common mistakes
- Believing
exec()creates a new process. It doesn't — it replaces the current process's memory image in place; the same process ID persists across theexec()call, which is precisely whyfork()(new process) andexec()(replace this process's program) are two separate, composable steps rather than one. - Assuming a program is fully loaded into memory before
main()starts executing. Demand paging means most of the loading happens lazily, on first access, page by page, throughout execution — not as one upfront step. - Forgetting the zombie-process window. A process that has exited but whose parent hasn't yet called
wait()still occupies a process table entry — a parent that never reaps its children accumulates zombie processes, a real (if usually slow-building) resource leak. - Treating this walkthrough as Linux/Unix trivia disconnected from daily work. Every step maps directly to a real, previously-covered production concern from this book — descriptor leaks (Chapter 12), OOM kills (Chapters 1, 6), scheduling-related latency (Chapter 4), and I/O-wait behavior (Chapter 9) are all visible in this single walkthrough.
Discussion / interview questions
-
"Walk me through what happens, at the OS level, when a shell runs a new program." Strong answer: Shell forks (cheap, copy-on-write clone), the child execs the new program (replacing its memory image entirely, same PID), the new program's pages load lazily via demand paging as they're first touched, and the process cycles through Ready/Running/Waiting under the scheduler until it exits and the OS reclaims its resources.
-
"What's the difference between
fork()andexec(), and why are they typically used together?" Strong answer:fork()creates a new process (a copy of the caller);exec()replaces the current process's memory image with a different program, keeping the same process ID. Used together, they let a program launch a completely different program in a new process —fork()alone would just run more copies of the same program. -
"Does a program's entire binary get loaded into memory before it starts running?" Strong answer: No — demand paging means pages are loaded lazily, only when actually accessed, which is exactly why the very first instruction fetch after
exec()triggers a page fault rather than the whole program being pre-loaded. -
"What is a zombie process, and how does it arise?" Strong answer: A process that has finished executing but whose exit status hasn't yet been collected by its parent via
wait()/waitpid()— it still occupies a process table entry until reaped; a parent that never callswait()on its children leaves zombies accumulating.
Chapter summary / cheat sheet
- Running a program: shell forks (cheap, COW clone) → child execs the new program (replaces memory image, same PID) → pages load lazily via demand paging as touched → the process cycles through the lifecycle (Ready/Running/Waiting) under the scheduler → exits and is reaped, resources reclaimed.
fork()creates a new process;exec()replaces a process's program in place — composed together, they're how a new program gets launched from an existing one.- A finished-but-unreaped process is a zombie, occupying a process table entry until its parent calls
wait(). - This single walkthrough exercises nearly every concept from Chapters 1–12, which is exactly why it's a favorite "tie it together" interview question.