Operating Systems Interview Preparation
This guide covers the most frequently asked OS interview questions at top tech companies, organized by topic with detailed answers and common follow-ups.
Target : Senior/Staff Engineering Interviews
Companies : FAANG, startups, systems companies
Preparation Time : 20-30 hours across all topics
Interview Question Patterns
┌─────────────────────────────────────────────────────────────────┐
│ OS INTERVIEW CATEGORIES │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. CONCEPTUAL (40%) │
│ "Explain how virtual memory works" │
│ "What happens when you call malloc?" │
│ │
│ 2. DESIGN (30%) │
│ "Design a thread pool" │
│ "Implement a simple scheduler" │
│ │
│ 3. DEBUGGING (20%) │
│ "This program deadlocks. Why?" │
│ "Why is this server slow?" │
│ │
│ 4. LINUX-SPECIFIC (10%) │
│ "Explain cgroups" │
│ "What is /proc?" │
│ │
└─────────────────────────────────────────────────────────────────┘
Learning Tracks and Progression
Use this section as a roadmap for practicing OS questions based on your goals.
Track 1: Generalist / Application Engineer
Focus on being dangerously good at fundamentals :
Read: Processes & Threads, Virtual Memory, Synchronization, Scheduling.
Practice:
Explain process vs thread vs coroutine in your own words.
Walk through malloc → page tables → page faults.
Debug simple deadlock and starvation examples.
Goal: Comfortably answer most “Top 20” OS questions in this file.
Track 2: Systems / Infra / Backend Engineer
You own services in production and need to debug OS-level issues:
Read: Everything in Track 1 plus File Systems, I/O Systems, Networking, Deadlocks, Linux Internals.
Practice:
Trace a slow web request through CPU, memory, and I/O using tools (strace, perf, iostat, ss).
Design a thread pool and explain scheduler interactions.
Reason about cgroups, namespaces, and container isolation.
Goal: Confidently debug “server is slow / high CPU / high IO wait” incidents.
Track 3: Kernel / Low-level Engineer
You want to work on kernels, drivers, or high-performance runtimes:
Read: All OS chapters, especially CPU Architectures, Kernel Memory, Linux Internals, Device Drivers, Storage Stack, Security, eBPF.
Practice:
Read and explain small sections of real kernel code (e.g., do_page_fault, tcp_recvmsg).
Implement simple kernel modules, experiment with scheduling and memory policies.
Design lock hierarchies and reason about RCU and lock-free algorithms.
Goal: Handle deep-dive interviews where you whiteboard OS internals and read code live.
Use these tracks to decide which sections to master first and which to treat as later extensions.
Top 20 OS Interview Questions
Process & Threading
1. What's the difference between a process and a thread?
Answer: Aspect Process Thread Memory Separate address space Shared address space Creation Expensive (fork) Cheap Context switch Expensive (TLB flush) Cheap Communication IPC needed Shared memory Crash impact Isolated Affects all threads
When to use each:
Processes : Isolation needed (security), different languages, crash isolation
Threads : Shared state, low latency communication, same codebase
Follow-up : “How does fork() work?”
Creates copy of parent’s address space (copy-on-write)
Child gets PID, returns 0 from fork()
Parent gets child’s PID, returns child PID from fork()
2. What happens when you run a program?
Answer (step-by-step):
Shell parses command , finds executable in PATH
fork() : Create child process
Copy page tables (COW)
Copy file descriptors
New PID, same code
execve() : Replace child with new program
Load ELF headers
Set up new address space
Map code, data sections
Set up stack with args/env
Jump to _start (entry point)
Dynamic linking : ld.so loads shared libraries
main() : C runtime calls your main()
exit() : Cleanup, return status to parent
wait() : Parent reaps child, gets exit status
Key syscalls : fork, execve, wait4, exit_group
3. Explain context switching
Answer: What is saved:
CPU registers (general purpose, PC, SP)
Floating point/SIMD registers
Kernel stack pointer
Page table pointer (CR3 on x86)
Steps: 1. Save current task's registers to its task_struct
2. Save stack pointer
3. Select next task (scheduler)
4. Restore next task's stack pointer
5. Restore next task's registers
6. If different process: switch page tables (TLB flush)
7. Return to user mode
Cost:
Thread switch: ~1-2 μs
Process switch: ~5-10 μs (TLB flush)
Why it matters : Too many context switches = poor performance
Memory Management
4. Explain virtual memory
Answer: Concept : Each process sees private address space (illusion of having all memory)How it works: Virtual Address → MMU → Page Tables → Physical Address
┌────────────────┐ ┌─────────────────┐ ┌────────────────┐
│ Process A │ │ Page Table A │ │ Physical RAM │
│ addr: 0x1000 │────►│ 0x1→phy 0x8000 │────►│ Frame 0x8000 │
└────────────────┘ └─────────────────┘ └────────────────┘
┌────────────────┐ ┌─────────────────┐
│ Process B │ │ Page Table B │ Same physical
│ addr: 0x1000 │────►│ 0x1→phy 0x9000 │────►frame or different
└────────────────┘ └─────────────────┘
Benefits:
Process isolation
Memory overcommit
Demand paging (don’t load until needed)
Shared libraries (one physical copy, many mappings)
Easy relocation
Components:
Page tables (software)
MMU (hardware)
TLB (cache for page table lookups)
5. What happens during a page fault?
Answer: Types:
Minor fault : Page in memory, just needs mapping
Major fault : Page on disk, I/O required
Invalid : Segmentation fault
Flow: 1. CPU accesses address
2. MMU checks page table → entry invalid/not present
3. CPU raises page fault exception
4. Kernel handler runs:
- Find VMA for address
- Check permissions
- If valid:
- Minor: Update page table
- Major: Read from disk, then update
- If invalid: SIGSEGV
5. Return to faulting instruction (retry)
Common causes:
Demand paging (first access)
Copy-on-write
Stack growth
Swapped out page
Null pointer dereference (→ SIGSEGV)
Answer: Layers: malloc(100) ← User request
│
▼
User allocator ← glibc (ptmalloc/jemalloc)
(maintains free list, caches)
│
▼ brk()/mmap() if needed
Kernel
(VMA management, demand paging)
Small allocations (less than 128KB typically):
Come from pre-allocated heap
Free list management
May use sbrk() to extend heap
Large allocations (>128KB):
Use mmap() directly
Return to OS on free
Allocator strategies:
ptmalloc : Per-thread arenas, reduces contention
jemalloc : Better for multi-threaded, used by Firefox
tcmalloc : Thread-caching, used by Go
Follow-up : “What happens when you free()?”
Small: Returns to free list (not to OS)
Large (mmap’d): munmap() returns to OS
May trigger coalescing of free blocks
Synchronization
7. What is a deadlock? How do you prevent it?
Answer: Four Coffman conditions (all must hold):
Mutual exclusion : Resource can’t be shared
Hold and wait : Holding one, waiting for another
No preemption : Can’t forcibly take resource
Circular wait : A→B→C→A waiting cycle
Prevention strategies: Strategy Method Tradeoff Lock ordering Always acquire in same order Requires discipline Lock timeout Give up after timeout May fail needlessly Try-lock Non-blocking acquire Retry logic needed Single lock One lock for all Poor concurrency Lock-free Atomic operations only Complex to implement
Example fix: // BAD: Thread 1 locks A then B, Thread 2 locks B then A
// GOOD: Always lock in order (A before B)
void transfer (Account * from , Account * to ) {
Account * first = (from < to) ? from : to;
Account * second = (from < to) ? to : from;
lock (first);
lock (second);
// transfer...
unlock (second);
unlock (first);
}
8. Explain mutex vs semaphore vs condition variable
Answer: Primitive Purpose Count Use Case Mutex Mutual exclusion 0/1 Protect critical section Semaphore Resource counting 0-N Limit concurrent access Cond Var Wait for condition N/A Producer-consumer
Mutex: pthread_mutex_lock ( & mutex );
// Critical section - only one thread
pthread_mutex_unlock ( & mutex );
Semaphore: sem_wait ( & sem ); // Decrement, block if 0
// Access resource (up to N concurrent)
sem_post ( & sem ); // Increment
Condition Variable: pthread_mutex_lock ( & mutex );
while ( ! condition) {
pthread_cond_wait ( & cond, & mutex); // Releases mutex while waiting
}
// Condition is now true
pthread_mutex_unlock ( & mutex );
// In another thread:
pthread_mutex_lock ( & mutex );
condition = true ;
pthread_cond_signal ( & cond ); // Wake one waiter
pthread_mutex_unlock ( & mutex );
Key insight : Condition variables are always used with a mutex and a predicate (while loop).
9. What is a spinlock? When to use it?
Answer: Spinlock : Busy-wait for lock instead of sleepingwhile ( atomic_test_and_set ( & lock ) == 1 ) {
// Spin! CPU is busy waiting
}
// Have lock
// ... critical section ...
atomic_clear ( & lock );
When to use:
Very short critical sections (less than 1μs)
Interrupt handlers (can’t sleep)
Lock held time shorter than context switch time
Known low contention
When NOT to use:
Long critical sections
High contention
User space (usually)
Single CPU system
Comparison: Aspect Spinlock Mutex Waiting Busy-wait Sleep CPU use 100% while waiting 0% while waiting Context switch None Yes Best for Very short holds Longer holds Kernel use Very common Less common
Scheduling
10. Explain CPU scheduling algorithms
Answer: Algorithm Description Pros Cons FCFS First come first served Simple Convoy effect SJF Shortest job first Optimal avg wait Need to know times Round Robin Time slices Fair Context switch overhead Priority Based on priority Important tasks first Starvation Multilevel Multiple queues Flexible Complex CFS Fair share of CPU Fair, no starvation Overhead
Linux CFS (Completely Fair Scheduler):
Each task tracks “virtual runtime” (vruntime)
Lower vruntime = hasn’t had fair share = run it next
Red-black tree for O(log n) task selection
Nice values adjust time slices, not priority
Key metrics:
Throughput : Jobs completed per time
Turnaround : Submit to completion
Wait time : Time in ready queue
Response time : Submit to first run
11. What is priority inversion? How do you solve it?
Answer: Problem: Low priority task (L) holds lock
High priority task (H) needs lock → blocks
Medium priority task (M) preempts L
Result: H waits for M, even though H > M priority!
Solutions:
Priority Inheritance :
L temporarily gets H’s priority while holding lock
L runs, releases lock, H continues
Used in Linux (rt_mutex)
Priority Ceiling :
Lock has ceiling = max priority of any user
Acquiring task gets ceiling priority
Prevents other tasks from preempting
Real example : Mars Pathfinder (1997)
Low-priority task held bus mutex
High-priority task blocked
Watchdog timer triggered reset
Fixed by enabling priority inheritance
File Systems & I/O
12. What happens when you read a file?
Answer: System call path: read(fd, buffer, size)
│
▼
VFS (Virtual File System)
- Translates to inode operations
│
▼
Page Cache (Check if cached)
─────► HIT: Copy to user buffer, return
─────► MISS: Continue...
│
▼
Filesystem (ext4, xfs)
- Map file offset → block numbers
│
▼
Block Layer
- I/O scheduling, merging
│
▼
Device Driver
- Issue commands to disk
│
▼
Hardware (SSD/HDD)
- DMA data to memory
│
▼
Complete I/O
- Wake up process
- Copy to user buffer
Optimizations:
Page cache (avoid disk for repeated reads)
Read-ahead (prefetch next blocks)
I/O merging (combine adjacent requests)
13. Explain the difference between sync and async I/O
Answer: Synchronous I/O: // Process blocks until I/O completes
ssize_t n = read (fd, buffer, size);
// Control returns here after disk I/O
Asynchronous I/O: // Submit request, continue immediately
struct aiocb cb = {...};
aio_read ( & cb );
// Do other work...
// Later: check completion or get signal
Modern: io_uring (Linux 5.1+):// Submit multiple I/O requests
io_uring_prep_read (sqe, fd, buffer, size, offset);
io_uring_submit ( & ring );
// Check completions (batched)
io_uring_wait_cqe ( & ring , & cqe );
Comparison: Aspect Sync Async (AIO) io_uring Blocking Yes No No System calls 1 per I/O 2 per I/O Batched Complexity Simple Complex Moderate Performance OK Better Best
Linux Specific
14. What is the difference between select, poll, and epoll?
Answer: Aspect select poll epoll Max FDs 1024 (FD_SETSIZE) Unlimited Unlimited Passing FDs Copy each call Copy each call Register once Checking O(n) scan O(n) scan O(1) ready list Edge trigger No No Yes
select: fd_set readfds;
FD_ZERO ( & readfds );
FD_SET (fd, & readfds );
select (fd + 1 , & readfds , NULL , NULL , NULL );
if ( FD_ISSET (fd, & readfds )) { ... }
poll: struct pollfd fds [ 1 ];
fds [ 0 ].fd = fd;
fds [ 0 ].events = POLLIN;
poll (fds, 1 , - 1 );
if ( fds [ 0 ].revents & POLLIN) { ... }
epoll: int epfd = epoll_create1 ( 0 );
struct epoll_event ev = {.events = EPOLLIN, . data . fd = fd};
epoll_ctl (epfd, EPOLL_CTL_ADD, fd, & ev );
struct epoll_event events [ 10 ];
int n = epoll_wait (epfd, events, 10 , - 1 );
// Only returns ready FDs!
Use epoll for :
High-performance servers
Many connections
Production systems
15. Explain containers (namespaces + cgroups)
Answer: Namespaces (isolation):Namespace Isolates PID Process IDs (PID 1 inside container) NET Network stack, interfaces MNT Filesystem mounts UTS Hostname IPC Shared memory, semaphores USER UID/GID mapping CGROUP Cgroup root
// Create new namespace
clone (..., CLONE_NEWPID | CLONE_NEWNET, ...);
// Or join existing
setns (fd, CLONE_NEWPID);
Cgroups (limits):Controller Limits memory RAM usage cpu CPU time blkio Disk I/O pids Number of processes
# Create cgroup
mkdir /sys/fs/cgroup/memory/mycontainer
echo 100M > /sys/fs/cgroup/memory/mycontainer/memory.max
echo $$ > /sys/fs/cgroup/memory/mycontainer/cgroup.procs
Docker = namespaces + cgroups + union filesystem + OCI runtime
Common Design Questions
Requirements:
Fixed number of worker threads
Task queue
Graceful shutdown
Implementation: typedef struct {
pthread_t * threads;
int num_threads;
// Task queue
task_t * queue;
int head, tail;
int queue_size;
// Synchronization
pthread_mutex_t lock;
pthread_cond_t not_empty;
pthread_cond_t not_full;
bool shutdown;
} thread_pool_t ;
void * worker ( void * arg ) {
thread_pool_t * pool = arg;
while ( 1 ) {
pthread_mutex_lock ( & pool -> lock );
while ( pool -> head == pool -> tail && ! pool -> shutdown ) {
pthread_cond_wait ( & pool -> not_empty , & pool -> lock );
}
if ( pool -> shutdown ) {
pthread_mutex_unlock ( & pool -> lock );
break ;
}
task_t task = pool -> queue [ pool -> head ];
pool -> head = ( pool -> head + 1 ) % pool -> queue_size ;
pthread_cond_signal ( & pool -> not_full );
pthread_mutex_unlock ( & pool -> lock );
// Execute task
task . func ( task . arg );
}
return NULL ;
}
void submit ( thread_pool_t * pool , void ( * func)( void * ), void * arg ) {
pthread_mutex_lock ( & pool -> lock );
while (( pool -> tail + 1 ) % pool -> queue_size == pool -> head ) {
pthread_cond_wait ( & pool -> not_full , & pool -> lock );
}
pool -> queue [ pool -> tail ] = ( task_t ){func, arg};
pool -> tail = ( pool -> tail + 1 ) % pool -> queue_size ;
pthread_cond_signal ( & pool -> not_empty );
pthread_mutex_unlock ( & pool -> lock );
}
Follow-ups:
How to handle task priorities? Use priority queue
How to handle task cancellation? Cancellation tokens
How to tune thread count? CPU cores, task type (I/O vs CPU)
17. Implement a simple memory allocator
Simple bump allocator: typedef struct {
void * base;
size_t size;
size_t offset;
} arena_t ;
void * arena_alloc ( arena_t * arena , size_t size ) {
// Align to 8 bytes
size = (size + 7 ) & ~ 7 ;
if ( arena -> offset + size > arena -> size ) {
return NULL ;
}
void * ptr = ( char * ) arena -> base + arena -> offset ;
arena -> offset += size;
return ptr;
}
void arena_reset ( arena_t * arena ) {
arena -> offset = 0 ;
}
Free list allocator: typedef struct block {
size_t size;
struct block * next;
} block_t ;
block_t * free_list = NULL ;
void * my_malloc ( size_t size ) {
block_t ** prev = & free_list;
block_t * curr = free_list;
// First fit
while (curr) {
if ( curr -> size >= size) {
* prev = curr -> next ;
return ( char * )curr + sizeof ( block_t );
}
prev = & curr -> next ;
curr = curr -> next ;
}
// No fit, get from OS
block_t * new = sbrk (size + sizeof ( block_t ));
new -> size = size;
return ( char * )new + sizeof ( block_t );
}
void my_free ( void * ptr ) {
block_t * block = ( block_t * )(( char * )ptr - sizeof ( block_t ));
block -> next = free_list;
free_list = block;
}
Real allocators add:
Coalescing free blocks
Size classes (slab-like)
Per-thread caches
Best fit or other strategies
18. Design a rate limiter
Token bucket algorithm: typedef struct {
double tokens;
double max_tokens;
double refill_rate; // tokens per second
struct timespec last_refill;
pthread_mutex_t lock;
} rate_limiter_t ;
bool acquire ( rate_limiter_t * rl , double tokens ) {
pthread_mutex_lock ( & rl -> lock );
// Refill tokens based on elapsed time
struct timespec now;
clock_gettime (CLOCK_MONOTONIC, & now);
double elapsed = ( now . tv_sec - rl -> last_refill . tv_sec ) +
( now . tv_nsec - rl -> last_refill . tv_nsec ) / 1 e 9 ;
rl -> tokens += elapsed * rl -> refill_rate ;
if ( rl -> tokens > rl -> max_tokens ) {
rl -> tokens = rl -> max_tokens ;
}
rl -> last_refill = now;
// Try to acquire
if ( rl -> tokens >= tokens) {
rl -> tokens -= tokens;
pthread_mutex_unlock ( & rl -> lock );
return true ;
}
pthread_mutex_unlock ( & rl -> lock );
return false ;
}
Sliding window: // Track requests in time window
// More accurate for bursts but more memory
typedef struct {
int * requests; // Circular buffer
int window_size; // Seconds
int max_requests;
struct timespec window_start;
} sliding_window_t ;
Distributed rate limiting:
Use Redis for shared state
Approximate algorithms (e.g., sliding window log)
Accept some inconsistency for performance
Debugging Scenarios
19. This program hangs. Diagnose it.
Approach:
Attach debugger / get stack traces:
$ gdb -p < pi d >
( gdb ) thread apply all bt
# Or without GDB:
$ pstack < pi d >
Check for deadlock:
# Look for mutex wait in stack traces
# Multiple threads waiting on locks = deadlock candidate
# Check lock order: Does thread 1 hold A, wait for B?
# Does thread 2 hold B, wait for A?
Check for infinite loop:
$ top -H -p < pi d >
# Look for thread at 100% CPU
$ perf top -p < pi d >
# What function is hot?
Check for I/O block:
$ cat /proc/ < pi d > /stack
# Look for I/O wait syscalls
$ strace -p < pi d >
# What syscall is it stuck on?
Check for network:
$ ss -tunapee | grep < pi d >
# Stuck connections?
$ netstat -an | grep CLOSE_WAIT
# Leaked sockets?
Common causes:
Deadlock (lock order violation)
Network timeout too high
Database connection exhausted
Full disk / slow I/O
Signal handling issue
20. This server is slow. Diagnose it.
Systematic approach:
CPU check:
$ top
$ mpstat -P ALL 1
# High CPU? Which process?
# Low CPU but slow? Waiting on I/O?
Memory check:
$ free -h
$ cat /proc/meminfo
$ vmstat 1
# Swapping? OOM pressure?
Disk I/O check:
$ iostat -x 1
$ iotop
# Disk at 100% utilization?
# High await times?
Network check:
$ ss -s
$ netstat -i
$ sar -n DEV 1
# High retransmits? Connection errors?
Application profiling:
$ perf record -g -p < pi d > sleep 30
$ perf report
# Where is time spent?
System calls:
$ strace -c -p < pi d >
# Which syscalls are slow?
Common bottlenecks:
Lock contention
Database queries
Network latency
GC pauses (Java, Go)
Disk I/O (logging, temp files)
Connection exhaustion
Quick Reference Cheat Sheet
Key Numbers to Know
Metric Approximate Value L1 cache access 1 ns L2 cache access 4 ns L3 cache access 12 ns RAM access 100 ns SSD read 100 μs HDD seek 10 ms Context switch 1-10 μs Page fault (minor) 5-10 μs Page fault (major) 1-10 ms System call 100-200 ns
System Call Quick Reference
// Process
fork () // Create child process
execve () // Replace with new program
wait () // Wait for child
exit () // Terminate process
// Files
open () // Open file
read () // Read from fd
write () // Write to fd
close () // Close fd
stat () // Get file metadata
// Memory
mmap () // Map memory/files
munmap () // Unmap
brk () // Extend heap
// Network
socket () // Create socket
bind () // Bind to address
listen () // Listen for connections
accept () // Accept connection
connect () // Connect to remote
Common File Paths
/proc/meminfo # Memory statistics
/proc/cpuinfo # CPU information
/proc/ <pid>/status # Process status
/proc/ <pid>/maps # Memory mappings
/proc/ <pid>/fd/ # Open file descriptors
/sys/class/net/ # Network interfaces
/sys/block/ # Block devices
/etc/passwd # User accounts
/etc/fstab # Mount configuration
Study Plan
Week 1: Fundamentals
Process/threads, memory management, virtual memory
Week 2: Synchronization
Mutexes, deadlocks, condition variables, lock-free
Week 3: Scheduling & I/O
CPU scheduling, file systems, disk I/O
Week 4: Linux Internals
System calls, kernel modules, containers
Week 5: Practice
Implement thread pool, allocator, rate limiter
Key Takeaways
Know the Fundamentals Process vs thread, virtual memory, page faults - these come up constantly.
Practice Debugging “Why is this slow/hanging?” questions are common. Know your tools.
Design Questions Be ready to implement thread pool, allocator, scheduler from scratch.
Know Linux Specifics epoll vs select, namespaces, cgroups - expected for systems roles.
Next: Case Studies →