OS questions reveal whether you think like a systems engineer or just a syntax user. When your web server slows under load, the answer might be in thread scheduling. When a process hangs, deadlock theory applies. When a database is slow, virtual memory and page faults may be the cause. OS knowledge is what makes code debuggable in production.
⚙ Why OS Knowledge Separates Systems Thinkers
OS questions test whether you understand the invisible infrastructure your code runs on. Infosys interviewers use OS questions to gauge: can you reason about what happens when two threads access the same variable? Do you know why context switching is expensive? Can you diagnose a deadlock? Do you understand why 32-bit systems were limited to 4GB RAM? These reveal whether you think at the system level or only at the application level.
"An operating system has no purpose other than to run application programs. The whole reason it exists is to be convenient for programs to use and to be efficient in using the hardware."— Abraham Silberschatz · Co-author, "Operating System Concepts" (the "dinosaur book") · Yale University
🔄 Processes, Threads & Context Switching
Process and thread concepts are the foundation of every OS question. Interviewers start here to establish baseline systems knowledge. "Process vs thread" is the single most commonly asked OS question at Infosys — fumble this and everything else is uphill. Know it cold, with memory layout, trade-offs, and a real-world example.
A process is a program in execution — the active, running instance of a program. A program is a passive set of instructions on disk; a process is that program loaded into memory with its own execution context.
Process Control Block (PCB): The OS's data structure representing a process. The OS maintains one PCB per process, used to save/restore state during context switches.
| PCB Field | Description |
|---|---|
| Process ID (PID) | Unique numeric identifier for the process |
| Process state | Current state: New, Ready, Running, Waiting, Terminated |
| Program Counter (PC) | Address of the next instruction to execute |
| CPU registers | All register values at last context switch (accumulator, stack pointer, etc.) |
| Memory management info | Page table base address, segment limits, memory bounds |
| I/O status | List of open files, pending I/O operations |
| Accounting info | CPU time used, wall time, priority, resource limits |
| Parent PID | PID of the parent process that created this one |
admitted
[New] ─────────────────────────▶ [Ready]
│ ▲
scheduled │ │ preempted /
by OS │ │ time quantum expires
▼ │
[Running]
│ │
I/O request / event wait│ │ exit
▼ ▼
[Waiting] ──────▶ [Terminated]
│
I/O done / │
event occurs│
▼
[Ready]
| State | Meaning | What triggers transition OUT |
|---|---|---|
| New | Process created but not yet admitted to ready queue | OS admits it (allocates memory, creates PCB) |
| Ready | Waiting for CPU — has everything else it needs | Scheduler selects it (dispatches to CPU) |
| Running | Currently executing on CPU | I/O request, time quantum expires, or process exits |
| Waiting (Blocked) | Waiting for I/O completion or event | I/O completes / event occurs → moves to Ready |
| Terminated | Process finished execution. PCB held briefly for parent's wait() | Parent calls wait() → PCB released |
| Property | Process | Thread |
|---|---|---|
| Definition | Independent program in execution; its own address space | Lightweight unit of execution within a process; shares the process's address space |
| Memory | Own code, data, heap, stack segments | Shares code, data, heap with siblings; own stack + registers only |
| Creation cost | Heavy — full address space duplication (fork) | Light — just allocate a new stack + TCB |
| Communication | IPC needed (pipes, sockets, shared memory, signals) | Direct via shared memory — fast but needs synchronization |
| Crash impact | Isolated — one process crash doesn't affect others | One thread crash can bring down the entire process |
| Context switch | Expensive — must switch address space (TLB flush) | Cheaper — same address space, only switch registers + stack |
| Typical use | Separate applications (browser + word processor) | Concurrent work within one app (web server handling many requests) |
-- Memory layout comparison: Process A: Process B: ┌──────────────┐ ┌──────────────┐ │ Stack (A) │ │ Stack (B) │ ├──────────────┤ ├──────────────┤ │ Heap (A) │ │ Heap (B) │ ├──────────────┤ ├──────────────┤ │ Data (A) │ │ Data (B) │ ├──────────────┤ ├──────────────┤ │ Code (A) │ │ Code (B) │ └──────────────┘ └──────────────┘ Process with 3 threads: ┌──────────────────────────────────────────┐ │ Stack T1 │ Stack T2 │ Stack T3 │ (separate per thread) ├──────────────────────────────────────── │ │ Heap (shared) │ (all threads share) ├──────────────────────────────────────────┤ │ Data (shared) │ ├──────────────────────────────────────────┤ │ Code (shared) │ └──────────────────────────────────────────┘
A context switch is the OS mechanism of saving the complete execution state (context) of the currently running process/thread and loading the saved state of the next one — allowing the CPU to switch between processes/threads.
Steps during a process context switch:
- Timer interrupt fires (or process blocks on I/O)
- OS saves current process state into its PCB: program counter, all CPU registers, memory limits
- OS updates process state (Running → Ready or Waiting)
- OS scheduler selects next process to run
- OS loads next process's PCB: restores program counter, registers
- OS switches memory address space — updates page table pointer (CR3 register on x86)
- TLB (Translation Lookaside Buffer) flush — all cached virtual-to-physical address mappings invalidated
- CPU resumes execution of new process
Why it's expensive:
- No useful work done during the switch itself — pure overhead
- TLB flush means all subsequent memory accesses must re-walk the page table until the TLB warms up — can cause hundreds of extra memory accesses
- Cache pollution: the new process's working set evicts the previous process's cache lines
- Typical cost: 1–10 microseconds + cache warming time
fork(): Creates a new process (child) by duplicating the calling process (parent). The child is an almost-exact copy of the parent — same code, data, heap, open files. fork() returns 0 in the child and the child's PID in the parent. Both continue execution from the same point after the fork() call.
exec(): Replaces the current process's memory image with a new program. Does NOT create a new process — just loads and starts a different program within the existing process. The calling process's code, data, stack are replaced entirely.
// Classic shell fork+exec pattern:
pid_t pid = fork();
if (pid == 0) {
// Child process: replace myself with "ls -la"
execvp("ls", args); // child's memory replaced with ls
// Code here never runs if exec succeeds
} else {
// Parent process: wait for child to complete
wait(NULL); // reap the zombie
// Continue shell loop
}
Zombie process: A process that has finished executing but still has an entry in the process table because the parent has not yet called wait() to collect its exit status. It holds no resources (memory freed) but occupies a PID and a process table slot.
// Creating a zombie:
pid_t pid = fork();
if (pid == 0) {
exit(0); // Child exits immediately
}
sleep(10); // Parent sleeps without calling wait()
// During this 10 seconds: child is a ZOMBIE
// It appears in ps output as "<defunct>" or "Z" state
// Fix: parent calls wait()
waitpid(pid, &status, 0); // zombie cleaned up, PID released
Orphan process: A process whose parent has terminated before it. The OS re-parents it to the init process (PID 1 on Linux, launchd on macOS), which periodically calls wait() to clean up orphaned processes.
// Creating an orphan:
pid_t pid = fork();
if (pid > 0) {
exit(0); // Parent exits immediately
}
// Child continues running: it is now an orphan
// Init (PID 1) automatically becomes its new parent
// Init will wait() for it when it eventually exits
Concurrency: Multiple tasks making progress over time — not necessarily at the same physical instant. Interleaved execution on a single CPU. The OS creates the illusion of simultaneity through rapid context switching.
Parallelism: Multiple tasks executing at literally the same physical instant — requires multiple CPU cores. True simultaneous execution.
-- Concurrency on 1 CPU (interleaved): Time: ──────────────────────────────────────▶ CPU: [T1][T1][T2][T2][T1][T3][T3][T2][T1] (T1, T2, T3 all progress, but only one runs at a time) -- Parallelism on 4 CPUs (simultaneous): Time: ──────────────────────────────────────▶ CPU 1: [T1][T1][T1][T1][T1][T1][T1][T1][T1] CPU 2: [T2][T2][T2][T2][T2][T2][T2][T2][T2] CPU 3: [T3][T3][T3][T3][T3][T3][T3][T3][T3] CPU 4: [T4][T4][T4][T4][T4][T4][T4][T4][T4] (All truly run simultaneously)
| Dimension | Concurrency | Parallelism |
|---|---|---|
| Hardware needed | Single CPU is sufficient | Requires multiple cores |
| Execution model | Interleaved — time-sliced | Simultaneous — literally same moment |
| Solves | Responsiveness — keep CPU busy, handle many tasks | Throughput — compute faster by dividing work |
| Risk | Race conditions, deadlocks from interleaving | Same + data sharing across cores (cache coherence) |
🕐 CPU Scheduling Algorithms
CPU scheduling algorithms are asked because they require you to reason about trade-offs: fairness vs efficiency, response time vs throughput, starvation vs performance. They also reveal whether you can analyze algorithms — not just name them. Always know: how does each work, what does it optimize, what is its weakness, and what is the time complexity of the scheduling decision itself.
First Come First Served. Simplest. Convoy effect problem.
Shortest Job First. Optimal average wait. Requires knowing burst time.
Shortest Remaining Time First. Preemptive SJF. Optimal but starvation risk.
Time quantum. Fair. Quantum size is critical design choice.
By priority value. Flexible. Can starve low-priority processes.
Multiple queues per class. Different policy per queue.
Different scheduling algorithms optimize for different goals. Understanding these metrics is essential for evaluating which algorithm is "best" for a given workload.
| Metric | Definition | Formula | Who cares |
|---|---|---|---|
| CPU Utilization | % of time CPU is doing useful work | Busy time / Total time | Data centers, batch systems |
| Throughput | Number of processes completed per unit time | Processes / Time | Batch processing |
| Turnaround Time | Total time from submission to completion | Completion − Arrival time | Batch jobs |
| Waiting Time | Total time spent in the Ready queue | Turnaround − Burst time | All systems |
| Response Time | Time from submission to FIRST response | First run − Arrival time | Interactive systems, UIs |
FCFS (First Come First Served): Non-preemptive. Processes executed in arrival order. Simple but suffers the convoy effect — one long job blocks all short jobs behind it.
-- Processes: P1(burst=24ms), P2(burst=3ms), P3(burst=3ms) all arrive at t=0 -- FCFS order: P1 → P2 → P3 -- Gantt: [P1: 0-24][P2: 24-27][P3: 27-30] -- Waiting: P1=0, P2=24, P3=27 → Average = 17ms (BAD for P2, P3!)
SJF (Shortest Job First): Non-preemptive. Always run the process with the shortest CPU burst time next. Provably optimal for minimizing average waiting time. Problem: requires knowing burst time in advance (usually estimated from history).
-- Same processes with SJF: P2(3) → P3(3) → P1(24) -- Gantt: [P2: 0-3][P3: 3-6][P1: 6-30] -- Waiting: P1=6, P2=0, P3=3 → Average = 3ms (MUCH better!)
SRTF (Shortest Remaining Time First): Preemptive version of SJF. If a new process arrives with a shorter remaining burst than the current process, preempt and run the new one. Minimizes average waiting time even more than SJF but risks starvation of long jobs.
-- P1 arrives t=0 (burst=8), P2 arrives t=1 (burst=4), P3 arrives t=2 (burst=9), P4 arrives t=3 (burst=5) -- t=0: P1 starts (remaining=8) -- t=1: P2 arrives (remaining=4 < P1's remaining=7) → preempt P1, run P2 -- t=2: P3 arrives (remaining=9 > P2's remaining=3) → continue P2 -- t=3: P4 arrives (remaining=5 > P2's remaining=2) → continue P2 -- t=5: P2 done, compare P1(7), P3(9), P4(5) → run P4...
Round Robin: Each process gets a fixed time quantum (time slice). When the quantum expires, the process is preempted and moved to the back of the ready queue. Fair by design — no starvation possible.
-- P1(burst=53ms), P2(burst=17ms), P3(burst=68ms), P4(burst=24ms), quantum=20ms -- Gantt: -- [P1:0-20][P2:20-37][P3:37-57][P4:57-77][P1:77-97][P3:97-117][P4:117-121][P1:121-134][P3:134-154] -- All processes get CPU turns in rotation. No one starves.
How quantum size affects performance:
| Quantum Size | Behavior | Result |
|---|---|---|
| Very small (1ms) | Context switch every 1ms | Nearly all CPU time wasted on context switching! Terrible throughput. |
| Very large (∞) | Never preempts — runs to completion | Degenerates to FCFS. Poor response time for short jobs. |
| Sweet spot (10–100ms) | Mostly completes jobs in one quantum | Good balance: <10% time in context switching, responsive |
Each process is assigned a priority number. The scheduler always runs the highest-priority ready process. Two variants:
- Non-preemptive: Current process runs to completion (or I/O block) before checking priority
- Preemptive: If a higher-priority process becomes ready, immediately preempt the current process
-- Example (lower number = higher priority): -- P1(priority=3, burst=10), P2(priority=1, burst=1), P3(priority=4, burst=2), P4(priority=5, burst=1), P5(priority=2, burst=5) -- Non-preemptive order (arrival at t=0): P2(1) → P5(2) → P1(3) → P3(4) → P4(5) -- P4 always runs last regardless of how long it waits — starvation if P1-P3 keep arriving!
Starvation: Low-priority processes may wait indefinitely if high-priority processes keep arriving. A process submitted at priority 10 may never run if priority 1–9 processes keep arriving.
Fix — Aging: Gradually increase the priority of processes that have been waiting for a long time.
-- Aging example: increase priority by 1 every 15 minutes of waiting -- P4 starts at priority 5 -- After 15 min waiting: priority becomes 4 -- After 30 min waiting: priority becomes 3 -- Eventually P4 reaches priority 1 and MUST run -- Aging guarantees eventual execution for all processes
| Property | Preemptive | Non-Preemptive |
|---|---|---|
| CPU control | OS can forcibly remove CPU from running process | Process keeps CPU until it voluntarily yields |
| Trigger for switch | Timer interrupt, higher-priority arrival, I/O block | Only when process blocks (I/O) or terminates |
| Response time | Better — urgent tasks can preempt immediately | Worse — must wait for current process to finish |
| Context switch overhead | Higher — more frequent switches | Lower — fewer switches |
| Risk | Race conditions if shared data modified mid-operation | One CPU-bound process can monopolize CPU indefinitely |
| Examples | Round Robin, Priority Preemptive, SRTF | FCFS, SJF, Non-preemptive Priority |
| Best for | Interactive systems, real-time systems | Batch processing, embedded systems |
🔒 Synchronization, Deadlocks & Semaphores
Synchronization problems are the root cause of some of the most catastrophic and hardest-to-reproduce bugs in production systems. Race conditions, deadlocks, and starvation have caused real financial losses, system outages, and safety failures. Interviewers test these because they reveal whether you can reason about concurrent execution — the fundamental skill of systems programming.
A race condition occurs when the correctness of a program depends on the relative timing of events, particularly when multiple threads access shared data concurrently and at least one access is a write.
// Classic race condition: two threads incrementing a shared counter
int counter = 0;
// Thread 1 and Thread 2 both execute:
void increment() {
counter++; // NOT atomic! This is 3 operations:
// 1. LOAD: read counter from memory into register (counter=5)
// 2. ADD: increment register (register=6)
// 3. STORE: write register back to memory (counter=6)
}
// Timeline showing the race:
// T1: LOAD counter=5
// T2: LOAD counter=5 (T2 reads BEFORE T1 stores)
// T1: ADD register=6
// T2: ADD register=6
// T1: STORE counter=6
// T2: STORE counter=6 (T2 OVERWRITES T1's result!)
// Expected: counter=7. Actual: counter=6. Lost update!
Critical Section: The portion of code that accesses shared resources. Must be executed atomically — only one thread at a time.
Conditions for a correct critical section solution:
- Mutual Exclusion: Only one process in the critical section at any time
- Progress: If no process is in the critical section and some want to enter, the decision cannot be postponed indefinitely
- Bounded Waiting: There is a limit on how many times other processes can enter the critical section before a waiting process is allowed in
A semaphore is a synchronization primitive: an integer variable with two atomic operations. Proposed by Edsger Dijkstra. Used to control access to shared resources and coordinate between processes/threads.
Two atomic operations:
- wait() / P() / down(): Decrement value. If value becomes negative, block the calling process (add to semaphore's waiting queue).
- signal() / V() / up(): Increment value. If value is still ≤ 0, wake one blocked process from the waiting queue.
// wait(S):
if (S.value > 0) {
S.value--; // can proceed immediately
} else {
block(current_process); // add to S.waiting_queue, block
}
// signal(S):
S.value++;
if (S.value <= 0) {
wake_up(one_from_waiting_queue); // unblock one waiter
}
Types:
| Type | Initial Value | Used For | Example |
|---|---|---|---|
| Binary Semaphore (Mutex) | 1 | Mutual exclusion — only 1 in critical section | Protecting a shared counter |
| Counting Semaphore | N (>1) | Control access to N instances of a resource | Connection pool with 10 connections (N=10) |
| Property | Mutex | Semaphore |
|---|---|---|
| Full name | Mutual Exclusion Lock | Semaphore (general synchronization) |
| Ownership | The thread that locked it MUST unlock it | No ownership — one thread can signal what another waited on |
| Value range | Binary: locked (0) or unlocked (1) | Any non-negative integer (counting semaphore) |
| Primary use | Mutual exclusion: protect a critical section | Signaling between threads; counting resources |
| Can be released by non-owner? | No — undefined behavior | Yes — any thread can signal |
| Priority inversion handling | Yes — priority inheritance in most OS | No built-in mechanism |
// MUTEX: protect a shared counter (ownership matters) mutex.lock(); // only ONE thread can be here counter++; // critical section mutex.unlock(); // MUST be the same thread that locked // SEMAPHORE: signal between threads (no ownership) sem_t sem; sem_init(&sem, 0, 0); // initial value 0 // Producer thread: produce_item(); sem_post(&sem); // signal: item is ready (value now 1) // Consumer thread: sem_wait(&sem); // wait: blocks until value > 0, then decrements consume_item(); // (Different threads do wait and signal — OK for semaphore, wrong for mutex)
Classic synchronization problem: producer adds to a bounded buffer; consumer removes. Constraints: producer must not write when buffer full; consumer must not read when buffer empty; mutual exclusion when accessing buffer.
// Solution using 3 semaphores:
semaphore mutex = 1; // binary: mutual exclusion for buffer access
semaphore empty = N; // counting: # empty slots (starts at N = buffer capacity)
semaphore full = 0; // counting: # filled slots (starts at 0)
// PRODUCER:
while(true) {
item = produce_item();
wait(empty); // wait for an empty slot
wait(mutex); // lock the buffer
add_item(buffer, item);
signal(mutex); // unlock the buffer
signal(full); // notify: one more item available
}
// CONSUMER:
while(true) {
wait(full); // wait for an item to be available
wait(mutex); // lock the buffer
item = remove_item(buffer);
signal(mutex); // unlock the buffer
signal(empty); // notify: one more empty slot
consume_item(item);
}
Deadlock: Two or more processes permanently blocked, each waiting for a resource held by another in the group.
Four Coffman Conditions (ALL must hold simultaneously for deadlock):
- Mutual Exclusion: At least one resource is non-sharable (only one process can use it at a time)
- Hold and Wait: A process holds at least one resource while waiting for additional resources held by others
- No Preemption: Resources cannot be forcibly taken from a process; must be voluntarily released
- Circular Wait: A circular chain of processes exists, each waiting for a resource held by the next in the chain
Breaking any ONE condition prevents deadlock. Four handling strategies:
| Strategy | Approach | Cost | Real Usage |
|---|---|---|---|
| Prevention | Eliminate one Coffman condition permanently (e.g., require all resources upfront = no Hold and Wait) | Very restrictive, low utilization | Embedded systems, safety-critical |
| Avoidance | Dynamically check before granting a request — Banker's Algorithm checks if granting leads to a "safe state" | Requires knowing max needs upfront, overhead per request | Rarely practical — theoretical |
| Detection + Recovery | Allow deadlocks, detect via wait-for graph cycles, kill a victim process (or rollback a transaction) | Overhead of periodic detection; victim loses work | Database systems (transaction deadlock detection) |
| Ignorance (Ostrich) | Pretend deadlocks don't happen. Reboot if one occurs. | Occasional crash, but low overhead | Most desktop OS (Windows, macOS) for process deadlocks |
Problem: 5 philosophers sit around a circular table. Between each pair is one chopstick (5 total). To eat, a philosopher needs BOTH the left and right chopstick. If all philosophers simultaneously pick up their left chopstick and then wait for the right one — circular wait — deadlock.
// Naive approach (DEADLOCK):
while(true) {
think();
pick_up(left_chopstick); // all philosophers do this simultaneously
pick_up(right_chopstick); // all wait here - DEADLOCK!
eat();
put_down(right_chopstick);
put_down(left_chopstick);
}
Solutions:
- Asymmetric: Odd philosophers pick left first, even pick right first. Breaks circular wait.
- At most N-1 philosophers sit simultaneously: At least one philosopher always gets both chopsticks — guarantees progress.
- Atomic pickup: A philosopher picks up both chopsticks in one atomic action or neither. If both not available, wait without holding any.
- Resource hierarchy: Number chopsticks 1–5. Always pick up lower-numbered first. Breaks circular wait.
- Monitor/Waiter: A central coordinator (monitor) decides who gets to eat — never lets circular wait form.
🧠 Memory Management
Memory management questions test whether you understand the gap between what a programmer sees (a flat address space, unlimited memory) and what actually exists (physical RAM, page tables, TLBs, swap). These concepts explain why 32-bit systems were limited to 4GB, why reading from RAM is 100x faster than disk, and why a misconfigured JVM heap causes GC pauses. These are daily production concerns.
Virtual memory is an abstraction that gives each process the illusion of its own large, private, contiguous address space — regardless of the actual physical RAM size or fragmentation.
Why it exists — 4 key benefits:
- Programs larger than RAM: The OS can run a 16GB program on a machine with 8GB RAM by swapping less-used pages to disk. Not all pages need to be in RAM simultaneously.
- Process isolation: Each process has its own virtual address space. Process A at virtual address 0x1000 accesses completely different physical memory than Process B at virtual address 0x1000. No process can access another's memory (without explicit sharing).
- Simplified programming model: Programs can be written assuming they start at address 0 and have the entire address space available. The OS handles the physical layout transparently.
- Efficient memory sharing: Multiple processes can share the same physical pages (e.g., shared libraries like libc — loaded once in RAM, mapped into every process's address space).
-- Virtual address space per process (64-bit): -- 0x0000000000000000 ... 0x00007FFFFFFFFFFF (128 TB user space) -- The SAME virtual addresses in DIFFERENT processes map to DIFFERENT physical pages -- Process A: virtual 0x1000 → physical 0x4A3000 -- Process B: virtual 0x1000 → physical 0x7B1000 (completely different!)
Paging divides both virtual address space and physical memory into fixed-size blocks: pages (virtual) and frames (physical). A page table maps virtual page numbers to physical frame numbers.
-- Address translation: -- Virtual Address = [Virtual Page Number (VPN) | Offset] -- VPN is used to look up the page table → gives Physical Frame Number (PFN) -- Physical Address = [PFN | Offset] (offset is unchanged) -- Example (page size = 4KB = 2^12 bytes): -- Virtual address: 0x12345678 -- Offset (lower 12 bits): 0x678 -- VPN (upper bits): 0x12345 -- Page table lookup: VPN 0x12345 → PFN 0x00ABC -- Physical address: 0x00ABC678 -- Page Table Entry (PTE) contains: -- Physical Frame Number (PFN) -- Present bit: 1 = page in RAM, 0 = page on disk (triggers page fault) -- Dirty bit: 1 = page modified, must write back to disk on eviction -- Referenced bit: 1 = page accessed recently (used by replacement algorithms) -- Protection bits: Read / Write / Execute permissions
TLB (Translation Lookaside Buffer): Hardware cache of recent page table entries. Avoids slow page table walk on every memory access. TLB hit: single cycle translation. TLB miss: full page table walk (10–100 cycles), then cache in TLB.
| Property | Segmentation | Paging |
|---|---|---|
| Division unit | Variable-size logical segments (code, data, stack, heap) | Fixed-size pages (typically 4KB) |
| Address format | [Segment number, Offset] | [Page number, Offset] |
| External fragmentation | Yes — variable-size gaps between segments | No — all frames are the same size |
| Internal fragmentation | No | Yes — last page may be partially empty |
| Programmer visibility | Visible — programmer manages segments | Transparent — automatic, programmer unaware |
| Protection | Per-segment (code = execute-only, data = read-write) | Per-page (with NX bit, read/write bits in PTE) |
Why modern systems use paging (not segmentation):
- External fragmentation is hard to manage — memory becomes Swiss cheese over time
- Fixed-size pages make allocation trivial (any free frame fits any page)
- Paging enables clean virtual memory: any subset of virtual pages can be in RAM
Modern systems (x86-64) use paged segmentation: hardware segments reduced to ring 0/3 privilege (kernel vs user mode). Actual memory management is entirely paging-based.
A page fault occurs when a process accesses a virtual page that is not currently in physical RAM (the present bit in the page table entry is 0).
-- Step-by-step page fault handling: 1. CPU accesses virtual address → looks up TLB → TLB miss 2. CPU walks page table → PTE found, but present bit = 0 (page not in RAM) 3. CPU raises a page fault exception → control transfers to OS page fault handler 4. OS checks: is this a valid virtual address for this process? If NO: segmentation fault → SIGSEGV, process killed If YES: continue 5. OS finds a free physical frame (or evicts one — see page replacement) 6. OS initiates disk I/O to read the required page from swap/file into the frame 7. Process is blocked during disk I/O 8. I/O completes: OS updates PTE (set present=1, PFN=new frame) 9. OS updates TLB 10. OS restarts the faulting instruction → process continues transparently -- Cost: ~10ms for a disk page fault (vs ~100ns for RAM access) -- 100,000x slower than a RAM access!
Types of page faults:
- Minor (soft) fault: Page is in RAM but not in this process's page table (e.g., shared library, copy-on-write). Cheap — just update page table. No disk I/O.
- Major (hard) fault: Page must be read from disk. Expensive — 10ms+ disk I/O.
- Invalid fault: Access to unmapped virtual address → Segmentation Fault (SIGSEGV).
When a page fault occurs and no free frames are available, the OS must evict (replace) an existing page. Which one?
| Algorithm | Policy | Performance | Implementable? |
|---|---|---|---|
| FIFO | Evict the oldest page (first loaded) | Simple but poor — may evict heavily used pages. Suffers Belady's Anomaly: MORE frames can cause MORE page faults! | Yes — just a queue |
| Optimal (OPT) | Evict the page that won't be used for the longest time in the future | Theoretically best — minimum page faults. Used as a benchmark. | No — requires future knowledge |
| LRU | Evict the page Least Recently Used | Near-optimal in practice. Uses past as proxy for future (locality). | Expensive — must track access time for every page |
| Clock (Second Chance) | Pages arranged in a circle with a reference bit. Clock hand sweeps; if reference bit=1, clear it (give second chance); if 0, evict. On access, set reference bit=1. | Good approximation of LRU with much lower overhead | Yes — used in Linux (approx. LRU) |
-- LRU example: page reference string = 7,0,1,2,0,3,0,4,2,3,0,3,2 (3 frames) -- Track which page was used LEAST recently at each step -- Evict the LRU page when a fault occurs -- Clock algorithm (simplified): -- Pages in circular buffer: [2*, 7*, 1, 5] (* = reference bit set) -- Need to evict one. Clock hand at position 0 (page 2): -- Page 2: ref bit=1 → clear bit, advance (page 2 gets second chance) -- Page 7: ref bit=1 → clear bit, advance -- Page 1: ref bit=0 → EVICT page 1, load new page here
Thrashing occurs when the system spends more time handling page faults than executing actual program instructions — the OS is constantly swapping pages in and out with no useful progress.
-- Thrashing scenario: -- 5 processes each need 4 frames to run efficiently -- System has only 10 frames (not enough for even 3 processes properly) -- Each process constantly needs pages not in RAM -- One process faults → OS swaps in its page → evicts another process's page -- That process faults → evicts the first process's page → cycle repeats -- CPU utilization drops to near 0 (CPU always waiting for disk I/O)
Causes:
- Too many processes competing for too few physical frames
- Degree of multiprogramming too high for available RAM
- Process's working set does not fit in allocated frames
Solutions:
- Working Set Model: Track the set of pages each process actively uses (working set). Only run a process if there are enough free frames to hold its full working set. Suspend processes if necessary.
- Reduce degree of multiprogramming: Swap out entire processes to disk — free up frames for remaining processes.
- Local replacement: Each process has a fixed frame allocation; can only evict its own pages — prevents one process thrashing from polluting others.
- Add more RAM: The physical fix for a RAM-starved system.
| Type | Definition | Caused By | Fix |
|---|---|---|---|
| Internal Fragmentation | Allocated block is larger than what is needed. Unused bytes INSIDE the allocated block. | Fixed-size allocation: requesting 100 bytes but getting a 128-byte block (the 28 extra bytes are wasted inside) | Use variable-size allocation or smaller block sizes |
| External Fragmentation | Enough total free memory exists but it is scattered in non-contiguous chunks. A large request cannot be satisfied even though total free > request size. | Variable-size allocation over time: allocate/free blocks of different sizes leaves holes | Compaction (defragmentation), paging, buddy system |
-- External fragmentation example: -- Memory: [FREE:10KB][USED:20KB][FREE:10KB][USED:30KB][FREE:10KB] -- Total free = 30KB. Request for 25KB CONTIGUOUS block → FAILS -- (No single free block is 25KB, even though 30KB is free in total) -- Paging ELIMINATES external fragmentation: -- The 25KB request needs 7 pages (7x4KB = 28KB, 3KB internal fragmentation) -- The 7 frames can come from ANY 7 free frames in physical RAM -- They do NOT need to be contiguous in physical memory!
🔌 I/O, System Calls, IPC & Kernel Architecture
Every interaction between application code and the hardware goes through system calls. Every process communicating with another uses IPC. Every OS design decision about monolithic vs microkernel involves fundamental trade-offs. These concepts explain why switching from HTTP polling to WebSockets is fast, why pipes are faster than sockets for local IPC, and why Linux is a monolithic kernel but is still modular.
Modern CPUs operate in at least two privilege levels (rings):
| Mode | Privilege | What runs here | Can access |
|---|---|---|---|
| User mode (Ring 3) | Restricted | Application programs (your Java/Python code, browsers, etc.) | Own process memory only; cannot execute privileged instructions; cannot directly access hardware |
| Kernel mode (Ring 0) | Unrestricted | OS kernel, device drivers | All physical memory; all hardware; all privileged CPU instructions |
Why this boundary exists:
- Stability: A buggy application cannot crash the OS or corrupt other processes' memory
- Security: Applications cannot directly access each other's memory or hardware (keyboard, disk, network)
- Control: The OS mediates all hardware access — can enforce policies (quotas, permissions, sandboxing)
-- How applications cross the boundary: system calls
-- Application code (user mode):
char buf[100];
read(fd, buf, 100); // application cannot directly access disk!
// this triggers a system call:
-- Kernel mode execution (transparent to application):
// 1. CPU switches to kernel mode (privilege ring 0)
// 2. Kernel validates fd, permissions, buffer address
// 3. Kernel issues I/O command to disk controller
// 4. Kernel copies data from kernel buffer to user buffer
// 5. CPU switches back to user mode
// 6. read() returns to application with byte count
A system call is the mechanism through which user-space programs request services from the OS kernel. It is the only legal way for a user-mode program to perform privileged operations.
-- System call mechanism: // 1. Application calls wrapper function (e.g., C library's read()) // 2. Wrapper loads system call number into CPU register (e.g., EAX on x86) // 3. Wrapper executes SYSCALL instruction (or INT 0x80 on older x86) // 4. CPU switches to kernel mode, jumps to syscall handler // 5. Kernel identifies call by number, validates arguments // 6. Kernel performs the operation, places return value in register // 7. CPU switches back to user mode // 8. Wrapper returns to application
| Category | System Call | What it does |
|---|---|---|
| File I/O | open(), read(), write(), close() | Open, read, write, and close files |
| Process | fork(), exec(), wait(), exit() | Create, replace, wait for, and terminate processes |
| Memory | mmap(), brk() | Map files into memory, extend heap (malloc uses these) |
| Networking | socket(), connect(), send(), recv() | Create and use network sockets |
| Signals | kill(), signal() | Send signals between processes |
| IPC Method | How it works | Speed | Best for |
|---|---|---|---|
| Pipes (anonymous) | Unidirectional byte stream. Created with pipe(). Parent-child only. | Fast — kernel buffer | ls | grep txt — shell pipelines between related processes |
| Named Pipes (FIFOs) | Like pipes but with a filesystem path. Unrelated processes can use it. | Fast | Simple unidirectional communication between unrelated processes |
| Message Queues | Kernel-managed queue of typed messages. Both sender and receiver can be asynchronous. | Medium | Asynchronous work queues, buffering bursts between producer and consumer |
| Shared Memory | Multiple processes map the same physical pages. Communicate by reading/writing the shared region directly. | Fastest — no copying, direct memory access | High-throughput data transfer (video frames, sensor streams); requires external synchronization (semaphore/mutex) |
| Sockets | Bidirectional communication endpoint. Can be local (Unix domain socket) or network (TCP/UDP). | Medium — Unix fast, TCP slowest | Network communication; local RPC (gRPC uses TCP sockets) |
| Signals | Software interrupts: one process sends a signal (integer) to another. Very limited data — just the signal number. | Fast | Notifications and control: SIGTERM (graceful shutdown), SIGKILL (force kill), SIGUSR1/SIGUSR2 (custom) |
DMA (Direct Memory Access): A hardware feature that allows I/O devices (disk controllers, network cards) to transfer data directly to/from RAM without involving the CPU for each byte transferred.
-- WITHOUT DMA (Programmed I/O — CPU does everything):
for each byte to transfer:
CPU waits for device to be ready
CPU reads byte from device I/O port
CPU writes byte to memory address
-- CPU is 100% occupied during transfer — cannot do anything else!
-- WITH DMA:
CPU: "DMA controller, transfer 64KB from disk to memory address 0x5000. Tell me when done."
DMA controller: *handles the entire transfer*
CPU: *free to do other work*
DMA completes: *raises interrupt* "Done!"
CPU: *handles interrupt, processes data*
Why it matters:
- Without DMA: a 1GB file read would consume the CPU for the entire transfer duration
- With DMA: CPU initiates the transfer and is immediately free to schedule other processes
- Modern NVMe SSDs and 10Gbps NICs transfer gigabytes per second — DMA is essential for this throughput without starving the CPU
| Kernel Type | Architecture | Pros | Cons | Examples |
|---|---|---|---|---|
| Monolithic | All OS services (scheduler, file system, device drivers, networking) run in kernel space (ring 0) | Fast — no IPC needed between kernel components; direct function calls | Large codebase in privileged mode; one buggy driver can crash the whole OS | Linux, traditional Unix |
| Microkernel | Minimal kernel (scheduling, basic IPC, memory management). All other services (file system, drivers) run as user-space servers | More reliable — a crashed driver doesn't crash the kernel; easier to verify formally | Slower — every service interaction requires IPC (user→kernel→user); higher overhead | Mach, Minix, QNX, seL4 |
| Hybrid | Monolithic base + some microkernel principles. Performance-critical code in kernel; some services user-space | Balance of performance and modularity | Complexity of both approaches | Windows NT, macOS XNU (Mach + BSD) |
A file system is the OS component that organizes and provides access to files on storage devices — managing directory hierarchies, file metadata (name, size, permissions, timestamps), and the mapping of file data to physical storage blocks.
| File System | OS | Key Feature | Max File Size |
|---|---|---|---|
| ext4 | Linux (default) | Journaling, backwards compatible with ext2/3 | 16TB |
| NTFS | Windows | Journaling, ACLs, compression, encryption | 16EB |
| APFS | macOS | Copy-on-write, snapshots, strong encryption | 8EB |
| FAT32 | Universal | Maximum compatibility (USB drives) | 4GB (huge limitation) |
| ZFS | FreeBSD, Linux | Built-in checksumming (detects silent corruption), RAID, snapshots | 16EB |
| XFS | Linux (RHEL default) | High performance, excellent for large files and parallel I/O | 8EB |
Journaling: Before modifying the file system, write the intended changes to a log (journal). If a crash occurs mid-operation, replay the journal on next boot to restore consistency. Without journaling: a crash mid-write can corrupt the file system structure (fsck needed, potentially data loss).
| Property | CPU-bound | I/O-bound |
|---|---|---|
| Bottleneck | CPU computation speed | I/O device speed (disk, network, DB) |
| CPU bursts | Long — runs CPU for a while before blocking | Short — runs briefly then waits for I/O |
| Time state | Mostly Running or Ready | Mostly Waiting (blocked on I/O) |
| Examples | ML training, video encoding, scientific computing, cryptography | Web servers, database queries, file copying, anything network-dependent |
| Scales with | More CPU cores, faster CPU | Faster disk/network, async I/O, connection pools |
| Typical multiplexing | Process-per-core, parallel computation | Many threads/coroutines, async/await, event loops |
How schedulers handle them: I/O-bound processes voluntarily release the CPU frequently (they block on I/O). Schedulers often give I/O-bound processes higher priority or shorter quantum — they do not use their full quantum anyway, and responding to their I/O completion quickly improves user-perceived responsiveness. CPU-bound processes get longer quantum to reduce context switch overhead.
Buffering: Storing data in a temporary area (buffer) in memory while it is being transferred between two devices or between a device and an application. Solves speed mismatches — producer produces faster than consumer can consume.
-- Buffering example: playing a video from a slow network -- Network data arrives at variable rate (5 Mbps avg but bursty) -- Video player needs smooth 4 Mbps stream -- Buffer: accumulate data ahead of playback position -- When network is fast: buffer fills up (building cushion) -- When network is slow: buffer drains (player still plays smoothly) -- "Buffering..." on screen = buffer completely drained
Spooling (Simultaneous Peripheral Operations On-Line): A special form of buffering where jobs are stored in a disk queue (spool) and processed by a slower device (like a printer) at its own pace, allowing the CPU to continue processing other jobs.
-- Print spooling example: -- App 1: "print document" → instantly returns (job sent to spool on disk) -- App 2: "print photo" → instantly returns (job sent to spool on disk) -- App 3: "print report" → instantly returns (job sent to spool on disk) -- Print spooler daemon: processes jobs one by one at printer's pace -- All apps continue without waiting for printer to finish!
🗀 OS Quick-Review Cheatsheet
Up Next: Phase 6 — Networks + System Design + SDLC
The final phase: Computer Networks (OSI model, TCP/UDP, HTTP/HTTPS, DNS), System Design fresher problems, and SDLC/Agile questions — all in one comprehensive phase.