Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Operating System Interfaces - Complete Study Guide
Learn operating system fundamentals by understanding the xv6 teaching operating system - a simple, Unix-like OS that demonstrates core OS concepts in ~9,000 lines of readable code.Core Concept: What is an Operating System?
An operating system has three primary jobs:Abstract Hardware
Programs don’t need to know specific hardware details (e.g., which disk type, GPU model)
Share Resources
Multiple programs run simultaneously (or appear to) by multiplexing CPU, memory, and I/O
Enable Interaction
Programs can communicate and share data safely through controlled mechanisms
Key Design Tension
[!IMPORTANT] The Interface Dilemma
- Simple/narrow interface = easier to implement correctly, but limited features
- Complex interface = more features but harder to maintain and secure
- Solution: Few powerful mechanisms that combine for generality
The xv6 Operating System
What is xv6?
xv6 is a teaching OS based on Unix design by Ken Thompson & Dennis Ritchie. It provides basic Unix interfaces and helps you understand modern operating systems like BSD, Linux, macOS, Solaris, and even Windows. Key Features:- Small enough to understand completely (~9,000 lines)
- Real enough to be useful (actual working OS)
- Provides essential Unix interfaces
- Runs on RISC-V architecture
Architecture Overview
Key Terms:
- Process: Running program with memory (instructions, data, stack)
- Kernel: Special program providing services to processes
- System Call: Process requests kernel service (e.g.,
fork(),open(),read()) - User Space: Normal programs run here (limited privileges)
- Kernel Space: Kernel runs here (full hardware access)
System Calls Reference
Process Management
| System Call | Description | Return Value |
|---|---|---|
fork() | Create new process | Child PID to parent, 0 to child |
exit(status) | Terminate process | No return (0=success, 1=failure) |
wait(*status) | Wait for child exit | Child PID or -1 |
kill(pid) | Terminate process | 0 or -1 |
getpid() | Get current process PID | PID |
sleep(n) | Pause for n clock ticks | 0 |
exec(file, argv[]) | Replace process with new program | No return on success, -1 on error |
sbrk(n) | Grow memory by n bytes | Address of new memory |
File Operations
| System Call | Description | Return Value |
|---|---|---|
open(file, flags) | Open file | File descriptor (fd) or -1 |
read(fd, buf, n) | Read n bytes | Bytes read or 0 at EOF |
write(fd, buf, n) | Write n bytes | Bytes written |
close(fd) | Release file descriptor | 0 or -1 |
dup(fd) | Duplicate fd | New fd to same file |
pipe(p[]) | Create pipe | 0 (p[0]=read, p[1]=write) |
File System
| System Call | Description | Return Value |
|---|---|---|
chdir(dir) | Change current directory | 0 or -1 |
mkdir(dir) | Create directory | 0 or -1 |
mknod(file, major, minor) | Create device file | 0 or -1 |
fstat(fd, *st) | Get file info from fd | 0 or -1 |
stat(file, *st) | Get file info from path | 0 or -1 |
link(file1, file2) | Create new name for file | 0 or -1 |
unlink(file) | Remove file name | 0 or -1 |
[!NOTE] Convention: Unless stated otherwise, system calls return 0 for success and -1 for error.
Processes and Memory
Process Structure
Each process has:- User-space memory: instructions + data + stack
- Kernel-private state: CPU registers, PID, etc.
- PID: Process identifier (unique number)
fork() - Creating Processes
Thefork() system call creates an exact copy of the parent process.
Behavior:
- Creates exact copy of parent process memory
- Both processes continue execution after
fork() - Only difference: return value
- Parent gets: child’s PID (positive number)
- Child gets: 0
- Error: -1
Example:
[!IMPORTANT] Critical Points:
- Parent and child have SEPARATE memory
- Changing variable in one doesn’t affect other
- Both start with identical memory contents
- Each has separate registers
wait() - Synchronizing Processes
Purpose: Parent waits for child to finish Behavior:- Returns PID of exited child
- Copies child’s exit status to provided address
- If no children exited yet, blocks until one does
- If no children exist, returns -1 immediately
exec() - Running Programs
Purpose: Replace current process with new program[!WARNING] Critical characteristic: Does NOT create new process, replaces current one!Arguments:
- Path to executable file
- Array of string arguments (NULL-terminated)
- Loads new program from file
- Replaces process memory completely
- Preserves: PID, file descriptors, open files
- If successful: NEVER returns (new program runs)
- If error: returns to caller
[!NOTE] argv[0] convention: First argument is program name (mostly ignored by program)
How Shell Uses These Calls
Shell main loop:echo hello:
- Shell forks
- Parent waits
- Child calls
exec("/bin/echo", ["echo", "hello", 0]) - echo runs and calls
exit() - Parent’s
wait()returns - Shell ready for next command
- Allows I/O redirection between
fork()andexec(). The child can close, open, and dup file descriptors in the gap between fork and exec, customizing its I/O environment without affecting the parent. - Shell can modify child’s file descriptors before
exec() - Parent’s I/O remains unchanged
cat does not need any code for handling redirection, pipes, or output capture. All of that is the shell’s job, using fork+exec with fd manipulation in between.
Alternative (worse) designs:
- Combined
forkexec()call — awkward for I/O redirection because you would need to pass the full fd configuration as parameters - Shell modifies own I/O, then undoes — error-prone and not thread-safe
- Every program handles own redirection — duplicated work across every utility
Memory Management
Implicit allocation:fork()- Allocates memory for child’s copyexec()- Allocates memory for new executable
sbrk(n)- Grow data memory by n bytes- Returns location of new memory
- Used by
malloc()implementation
fork()uses copy-on-write (COW)- Doesn’t actually copy memory until modified
- Avoids waste when
exec()immediately followsfork()
I/O and File Descriptors
File Descriptor Concept
Definition: Small integer representing kernel-managed I/O object Can refer to:- Regular files
- Directories
- Devices (keyboard, screen, etc.)
- Pipes
- Open file/directory/device
- Create pipe
- Duplicate existing descriptor
File Descriptor Table
Per-process table:
- Each process has private fd space
- FDs start at 0
- Kernel uses fd as index into table
- FD 0 = standard input (stdin)
- FD 1 = standard output (stdout)
- FD 2 = standard error (stderr)
read() and write()
read(fd, buf, n):- Reads UP TO n bytes from fd
- Copies into buf
- Returns number of bytes actually read
- Returns 0 at end of file
- Each fd has file offset that advances automatically
- Writes n bytes from buf to fd
- Returns number of bytes written
- Less than n only on error
- Offset advances automatically
cat Example - The Power of Abstraction
catdoes not know if it is reading from a file, the console, or a pipecatdoes not know if it is writing to a file, the console, or a pipe- Same code works for all cases — this is the Unix “everything is a file” philosophy in action
- Shell controls actual I/O sources/destinations via fd manipulation
cat binary works in all of these contexts without modification:
File Descriptor Allocation
close(fd):- Releases file descriptor
- Makes fd available for reuse
- Always uses LOWEST unused number
- This is critical for I/O redirection
I/O Redirection Mechanism
Example:cat < input.txt
- Child closes stdin (fd 0)
open()gets fd 0 (lowest available)exec()preserves file descriptor tablecatreads from fd 0, which now refers toinput.txt
open() Flags
Defined in fcntl.h:O_RDONLY- Read onlyO_WRONLY- Write onlyO_RDWR- Read and writeO_CREATE- Create if doesn’t existO_TRUNC- Truncate to zero length
fork() and File Descriptors
Behavior:fork() copies file descriptor table
Shared file offset example:
"hello world"
Why: Parent and child share same underlying file offset
- Child writes
"hello "at position 0 - Offset advances to 6
- Parent writes
"world\n"at position 6
dup() - Duplicating Descriptors
Purpose: Create new fd referring to same file Behavior:- Returns new fd (lowest available)
- Both fds share offset (like
fork())
"hello world" (sequential writes)
When offsets are shared:
- Derived from same original fd by
fork()/dup()
- Separate
open()calls, even for same file
Error Redirection
Command:ls existing non-existing > tmp1 2>&1
Meaning:
> tmp1- Redirect stdout (fd 1) to tmp12>&1- Redirect stderr (fd 2) to duplicate of fd 1
Pipes
Pipe Concept
Definition: Kernel buffer with two file descriptors- One fd for reading
- One fd for writing
Creation:
Pipe Example - Running wc
- Parent creates pipe
- Fork creates child with copy of pipe fds
- Child redirects stdin to pipe read end
- Child closes both pipe fds (already has copy at fd 0)
- Parent closes read end (won’t read)
- Parent writes data to pipe
- Parent closes write end (signals EOF)
- Child reads from stdin (pipe), processes, exits
[!IMPORTANT] Critical: Why close pipe fds?
- Child must close write end before
exec()- Otherwise
wcwould never see EOF (it has write end open)- If ANY process has write end open, read won’t return EOF
Pipe Blocking Behavior
When reading from pipe:- If data available: returns data
- If no data AND write end open: blocks (waits for data)
- If no data AND all write ends closed: returns 0 (EOF)
Shell Pipeline Implementation
Command:grep fork sh.c | wc -l
Process tree:
- Create pipe
- Fork for grep
- Redirect stdout to pipe write end
- exec grep
- Fork for wc
- Redirect stdin to pipe read end
- exec wc
- Wait for both children
a | b | c
- Leaves = commands
- Interior nodes = processes managing pipes
Pipes vs Temporary Files
Command comparison:- Auto cleanup — No temp files to delete; the pipe buffer is reclaimed when both ends close
- Streaming — The writer does not need to finish before the reader starts. Data flows through the pipe in a FIFO stream, bounded by a kernel buffer (typically 64 KB on Linux). If the buffer is full, the writer blocks until the reader drains some data.
- Parallel execution — Both programs run simultaneously, overlapping CPU and I/O work.
grepcan start processing output fromfindwhilefindis still traversing directories. - Backpressure — If the consumer is slow, the pipe buffer fills up and the producer naturally pauses. This prevents unbounded memory growth without any explicit coordination.
- Must manually clean up (and handle cleanup on errors/crashes)
- Need disk space for all data — a pipeline processing 10 TB would require 10 TB of temp storage
- Sequential execution (first program must finish before second starts)
- Security risk: temp files can be read by other processes unless permissions are carefully managed
File System
Structure
Hierarchical organization:- Tree of directories
- Root directory:
/ - Data files: uninterpreted byte arrays
- Directories: named references to files/directories
/a/b/c- Absolute path from roota/b/c- Relative to current directory
Directory Navigation
Both open same file:- Changes process current directory to
/a, then/a/b - Opens
crelative to/a/b
- No directory change
- Direct absolute path
Creating Files and Directories
- First number = major device number
- Second number = minor device number
- Together uniquely identify kernel device
read()/write()calls diverted to device driver
Inodes and Links
Inode = actual file data structure
Contains:
- File type (file, directory, device)
- File length
- Location of content on disk
- Number of links to file
[!IMPORTANT] Key insight: File name ≠ file itselfExample:
- One inode can have multiple names (links)
- Each link is directory entry: name + inode reference
- One inode
- Two names:
"a"and"b" - Both refer to same content
- Reading/writing
"a"= reading/writing"b"
stat Structure
- Both
"a"and"b"have sameino nlink= 2
unlink() - Removing Names
Behavior:- Removes name from file system
- Decrements link count
- Inode and disk space freed ONLY when:
- Link count = 0 AND
- No file descriptors refer to it
- Name
"a"removed - File still accessible as
"b" - Inode unchanged
Built-in vs External Commands
External (user-level programs):mkdirlnrmlscat- Most commands
- Anyone can add new commands
- No need to modify kernel/shell
- Easier to extend system
cd
[!WARNING]
Problem: cd must change SHELL’s directory, not child’s
System Call Flow
Understanding how system calls work is crucial for OS internals.
Tracing fork() from User to Kernel
Step 1: User Program Calls fork()Real World Context
Unix Philosophy
“Software tools” culture:- Small programs doing one thing well
- Combine via pipes
- Shell as “scripting language”
- Standard file descriptors (0, 1, 2)
- Pipes for composition
- Simple but powerful shell syntax
POSIX Standard
Purpose: Standardize Unix system call interface xv6 vs POSIX:- xv6 NOT POSIX compliant
- Missing:
lseek(), many other calls - Different implementations of existing calls
- Goal: Simplicity for teaching
- Many more system calls
- Networking, windowing, threads
- Many device drivers
- Continuous evolution
Alternative Designs
Plan 9:- Extended “everything is a file” concept
- Applied to networks, graphics
- Most Unix systems didn’t follow
- Predecessor of Unix
- Files looked like memory
- Very different interface
- Complexity influenced Unix designers to simplify
- Different abstractions possible
- File descriptors not only solution
- Unix model proven very successful
xv6 Limitations
No user protection:- All processes run as root
- No isolation between users
- Teaching-focused tradeoff
- Minimal system calls
- No networking
- No windowing
- Basic functionality only
Key Takeaways
Design Principles
Simple Interfaces
Few powerful mechanisms that combine for generality
Hardware Abstraction
Hide hardware details behind clean interfaces
Process Isolation
Processes isolated from each other for safety
Controlled Communication
Explicit mechanisms for safe interaction
Core Abstractions
- Processes - Unit of execution with isolated memory
- File descriptors - Unified I/O interface for files, pipes, devices
- Pipes - Communication channels between processes
- File system - Persistent storage hierarchy with inodes and links
Why These Abstractions Work
File descriptors:- Hide differences (files, pipes, devices)
- Enable I/O redirection
- Simple but powerful
- Enables I/O redirection
- Shell controls child environment
- Parent unaffected
- Clean inter-process communication
- Better than temp files
- Enable parallel execution
- Separate names from content
- Multiple names for same file
- Automatic cleanup when unused
Getting Started with xv6
Installation (Ubuntu/Debian)
First Commands to Try
Caveats and Common Pitfalls
xv6 is one of the best ways to learn how an operating system actually works. It is also one of the easiest ways to develop misleading mental models of how real operating systems work. The simplifications that make xv6 readable in 9,000 lines also make it dangerously different from production code. Treat xv6 as a teaching diagram, not a blueprint.Interview Deep-Dive
Compare xv6's scheduler to Linux CFS. What is missing from xv6, and what does Linux gain by being more complex?
Compare xv6's scheduler to Linux CFS. What is missing from xv6, and what does Linux gain by being more complex?
Strong Answer Framework:Common Wrong Answers:
- xv6’s scheduler is per-CPU round robin. Each CPU independently runs
scheduler()inproc.c. It walks the process table looking for a RUNNABLE process and runs it. There is no priority, no fairness accounting, no virtual runtime, no load balancing across CPUs, no I/O priority boost, no sleep-time accounting. A long-running CPU-bound process gets exactly the same share as an interactive shell. - Linux CFS tracks vruntime (virtual runtime). Each task has a vruntime that increments based on actual CPU time consumed, weighted by nice value. The scheduler picks the task with the lowest vruntime, stored in a red-black tree for O(log n) selection. Tasks that sleep accumulate vruntime slowly while sleeping (capped to prevent infinite catch-up), so they get a “wakeup boost” that makes them latency-friendly.
- What is missing in xv6. Fairness across nice values, load balancing across CPUs (xv6 actively wastes CPU time when one CPU is idle and another has multiple runnable tasks), CPU affinity hints, group scheduling (cgroups), real-time scheduling classes (SCHED_FIFO / SCHED_RR / SCHED_DEADLINE), and any I/O latency awareness. The scheduler is also not preemptive in the kernel — once a task is running in kernel mode in xv6, it runs until it voluntarily yields.
- What Linux gains. Linux CFS handles workloads from a 4-thread embedded device to a 256-CPU server with thousands of tasks. The complexity buys: predictable interactive latency under load, fairness guarantees that hold over time (not just instantaneously), the ability to express priority via cgroups for containers, and the ability to mix latency-sensitive and batch workloads on the same machine.
- What Linux trades. Complexity (CFS is several thousand lines vs xv6’s hundred), debugging difficulty (vruntime quirks like the “wakeup buddy” heuristic took years to tune), and edge cases (CFS bandwidth control’s period-boundary throttle problem in containers). Simpler is sometimes a feature — which is why ULE (FreeBSD), CFS, and EEVDF have all evolved over decades.
Senior follow-up 1: “What is the ‘thundering herd’ problem in scheduling, and does xv6 have it?” When many tasks block on one event and all wake at once, they all become RUNNABLE simultaneously. xv6 has it (sleep/wakeup wakes all waiters); Linux mitigates it via wait queue exclusivity flags and EPOLLEXCLUSIVE.
Senior follow-up 2: “Why does Linux have separate scheduling classes (CFS, RT, deadline, idle) instead of one unified scheduler?” Different workloads have fundamentally incompatible requirements. Real-time tasks need bounded worst-case latency; CFS provides fairness; idle tasks should never run when anything else is runnable. A unified scheduler tuned for one class compromises the others. The class hierarchy lets each class implement what it needs.
Senior follow-up 3: “What is preemption disabling in the kernel and why does it matter?” When kernel code holds a spinlock or runs in interrupt context, it cannot be preempted — the scheduler will not switch tasks. Long-held preemption-disabled sections cause latency spikes. CONFIG_PREEMPT_RT (recently merged) replaces most spinlocks with sleeping mutexes to allow preemption almost everywhere, dramatically reducing worst-case latency at some throughput cost.
- “xv6’s scheduler is the same as Linux, just smaller.” Wrong — the algorithm is fundamentally different (round-robin vs vruntime-based). Saying “same but smaller” misses the point that fairness is an algorithmic property, not a code-size property.
- “Linux CFS is fair to the millisecond.” Misleading — CFS targets long-run fairness, not instantaneous fairness. Over a few milliseconds, one task can dominate due to wakeup heuristics; over seconds, weights are honored.
- “Operating Systems: Three Easy Pieces”, chapter on scheduling — best free scheduling textbook.
- Documentation/scheduler/sched-design-CFS.rst in the Linux kernel source.
- LWN article “An EEVDF CPU scheduler for Linux” (2023) — explains the CFS-to-EEVDF transition.
Explain xv6's filesystem layout from disk to file. How does opening /a/b.txt actually work?
Explain xv6's filesystem layout from disk to file. How does opening /a/b.txt actually work?
Strong Answer Framework:Common Wrong Answers:
- The disk layout. xv6 divides the disk into named regions: boot block (block 0), super block (block 1, contains FS metadata), log blocks (for crash recovery), inode blocks (each block holds several inodes), bitmap blocks (track which data blocks are free), and data blocks (the actual file contents). The super block tells you where each region starts.
- Inodes. Every file or directory is an inode — a fixed-size structure containing type (file/dir/device), size, and a list of block pointers. xv6 has 12 direct pointers and 1 indirect pointer (which points to a block of more pointers), giving max file size ~70KB. Real Linux ext4 uses extents and triple-indirect blocks for files in the terabyte range, but the inode concept is identical.
- Directory entries. A directory in xv6 is just a file whose contents are an array of
struct dirent { ushort inum; char name[14]; }. Looking up a name means scanning the directory’s data blocks for a matching name and returning the inode number. - Resolving /a/b.txt. Start at inode 1 (root directory, hardcoded). Read its data, scan for “a”, find inum N. Read inode N (must be a directory), read its data, scan for “b.txt”, find inum M. Read inode M — this is the file. Each inode read may require a disk I/O (cached in the inode cache after first read).
- The block cache and log. Every disk block goes through a buffer cache (
buf.c) for performance. Writes go through a write-ahead log: changes are written to the log first, then applied to actual locations after commit. On crash, replay the log to recover. This gives crash consistency similar to ext4 with journaling. - The path lookup function. In xv6,
namei()andnameiparent()walk paths. Their pseudocode: split path on/, start at root (or cwd), repeat: get current inode, lock it, look up next component, follow to next inode. Locking matters because two threads renaming files concurrently could deadlock without ordered acquisition.
ext4_dx_find_entry(). The lesson: path resolution is one of the most performance-critical and security-critical paths in any kernel, and the simple xv6 implementation glosses over many edge cases (Unicode normalization, race conditions, symlinks, mount points). The MIT 6.S081 fs lab walks through exactly these issues at a teaching scale.Senior follow-up 1: “What happens if the system crashes in the middle of writing a directory entry?” Without journaling, you could see a half-written entry — corrupt filesystem. xv6’s log handles this by writing all changes to the log first, then replaying on boot. ext4 does the same with its journal. ZFS/btrfs use copy-on-write so old data is never overwritten until new data is fully durable.
Senior follow-up 2: “How does xv6 handle the inode cache and reference counting?” Each in-memory inode has a reference count.
iget increments it; iput decrements and may free if zero. Locking is separate from reference counting — you can hold a reference without holding the lock. Real Linux dcache/icache works similarly but is much more complex due to RCU.Senior follow-up 3: “What is missing from xv6’s filesystem compared to a real production FS?” No journaling at the metadata level (xv6’s log is per-syscall, not full FS journaling), no extents (extents pack large contiguous ranges into one descriptor), no soft updates or delayed allocation, no copy-on-write snapshots, no compression, no encryption, no extended attributes, no quotas. Each of these features represents a decade of FS research.
- “xv6 uses ext2-style inodes.” Sort of — ext2 inspired the design, but xv6’s inode is much simpler (12 direct + 1 indirect, no triple indirect, no extended attributes). Saying “ext2-style” without acknowledging the simplifications shows shallow understanding.
- “Path resolution is just a series of lookups.” Strictly true but misses the locking, caching, and crash-recovery layers that make real path resolution work. A senior engineer should mention all three.
- The xv6 book, chapter 8 on the file system — one of the best filesystem tutorials in print.
- “Operating Systems: Three Easy Pieces”, chapters on file systems and FFS/LFS.
- ext4 Wikipedia article and
Documentation/filesystems/ext4.rstin Linux source for comparison.
Implementing copy-on-write fork in xv6 -- which files would you change, and what are the gotchas?
Implementing copy-on-write fork in xv6 -- which files would you change, and what are the gotchas?
Strong Answer Framework:Common Wrong Answers:
- What COW fork does. Instead of copying every page on fork (xv6 default), mark all pages read-only in both parent and child page tables. When either writes, take a page fault, copy that page, mark it writable, restore the writer. Most pages are never written before exec(), so most pages are never copied — huge speedup for fork+exec.
- Files to modify.
kernel/vm.c(page table walk and protection bits),kernel/proc.c(thefork()syscall implementation),kernel/trap.c(the page fault handler — need to detect write-to-RO and handle it), andkernel/kalloc.c(need a reference count per physical page so we know when to free). - The reference counting problem. When parent and child share a page, we cannot free it until both are done. Add an array indexed by physical frame number, with a reference count. Increment on share, decrement on unshare. Free when count hits zero. The reference count must be lock-protected (xv6 is multi-CPU).
- The page-fault handler. On a write fault to a RO page that is shared (refcount > 1), allocate a new page, copy contents, decrement old page’s refcount, install new page in the faulting process’s page table as writable. If refcount is exactly 1 (we are the only user), just flip RO to RW — no copy needed. This is the optimization that makes COW efficient.
- Gotchas. (a) The kernel itself sometimes touches user pages (
copyin,copyout) — these need COW handling too, or they will silently write to a shared page. (b)fork()of a fork()ed child means a page might have refcount 3 — the chain of COW must work correctly. (c) Stacks grow via faults — the COW path must not confuse stack growth faults with COW faults. (d) Race condition: two threads in the parent could both write to the same shared page simultaneously and both take a fault — locking is essential.
mm/gup.c. Every modern kernel has been fixed, but the lesson is that COW logic is subtle even after 30 years of refinement. If you implement COW in xv6, expect your first version to have at least one race.Senior follow-up 1: “How does the MIT 6.S081 ‘cowgc’ (COW garbage collection) lab differ from a real Linux implementation?” The lab uses a per-physical-page refcount array, similar to Linux. The real Linux uses
struct page (one per physical page) with a refcount field, plus a separate mapcount for “mapped into how many user page tables.” The two-counter approach handles file-mapped pages (where many processes map the same page-cache page) correctly.Senior follow-up 2: “Can COW pages cause OOM in unexpected ways?” Yes — if many children fork from a parent that allocates a large array, then each child writes to different parts of it, every write triggers a copy. The system can run out of memory even though logically nothing was allocated. The fix is
madvise(MADV_DONTFORK) to mark large mappings as not inherited, or vfork() for the case where the child immediately execs.Senior follow-up 3: “How does COW interact with the TLB?” When you flip a page from RW to RO, you must invalidate that TLB entry on every CPU that might have it cached. This is a TLB shootdown — expensive on multi-CPU systems. xv6’s
sfence.vma handles this for RISC-V; Linux uses inter-processor interrupts for x86 TLB shootdowns. High fork rates can become TLB-shootdown bound.- “Just mark pages read-only and copy on fault — done.” Skips reference counting, which means you cannot tell when to free. A naive implementation leaks every COW-shared page.
- “COW always saves memory.” Not always — if the child writes to most pages (which is common in some long-lived child processes), you pay the fault overhead and end up copying anyway. The savings are biggest when the child immediately execs.
- MIT 6.S081 “cowgc” lab handout (free at pdos.csail.mit.edu) — step-by-step COW implementation.
- Linux kernel source
mm/memory.c, functiondo_wp_page()— the production COW handler. - “Dirty COW” CVE write-ups — excellent for understanding the subtlety of COW races.
Interview Relevance
System Design Questions
System Design Questions
Understanding OS interfaces helps you:
- Design better APIs and abstractions
- Understand performance implications
- Make informed technology choices
- Debug production issues
Common Interview Topics
Common Interview Topics
- How does
fork()work? What’s copy-on-write? - Explain file descriptors and I/O redirection
- How do pipes work? When to use them?
- What’s the difference between hard links and symbolic links?
- How does the shell implement pipelines?
- Explain the system call mechanism
Senior-Level Expectations
Senior-Level Expectations
- Understand trade-offs in OS design
- Explain why Unix chose these abstractions
- Compare with alternative designs
- Discuss performance implications
- Debug issues at the system call level
Resources
Official Materials
- xv6 Book - Complete guide
- xv6 Source Code - GitHub repository
- MIT 6.S081 - OS Engineering course
Next Steps
Process Management
Deep dive into process lifecycle, scheduling, and context switching
Virtual Memory
Understand paging, page tables, and memory management
File Systems
Learn about inodes, directories, and file system implementation
Linux Internals
Explore how these concepts scale in production systems
[!TIP] Learning Path: Start by reading the xv6 book alongside the source code. Try modifying xv6 to add features or change behavior. This hands-on experience is invaluable for truly understanding operating systems.