💻

Top 53 Operating Systems Interview Questions & Answers (2026)

53 Questions 31 Beginner 13 Intermediate 9 Advanced

About Operating Systems

Top 100 Operating Systems interview questions covering processes, threads, scheduling, memory management, file systems, deadlocks, synchronization, and virtualization. Companies hiring for Operating Systems roles test this knowledge at every stage — from first-round screens that check the basics to final-round conversations about real-world trade-offs.

What to Expect in a Operating Systems Interview

Expect a mix of conceptual and practical Operating Systems questions: clear definitions and core-concept checks for junior roles, hands-on scenario questions for mid-level roles, and architecture or trade-off discussions for senior roles. Interviewers usually move from foundational topics toward the kind of problems you'd actually face on the job.

How to Use This Guide

Work through the Operating Systems questions in order — Beginner, then Intermediate, then Advanced — so each concept builds on the last. Every question has its own page; bookmark the ones that trip you up and revisit them the day before your interview.

Curated by Tech Baithak Editorial Team  ·  Last updated: May 2026

Beginner 31 questions

Core concepts every Operating Systems developer must know.

01

What is an operating system?

An Operating System (OS) is a system software that manages computer hardware and software resources and provides common services for computer programs. It acts as an intermediary between the user and the computer hardware. Primary functions: (1) Process management: creating, scheduling, terminating processes and threads; (2) Memory management: allocating and deallocating memory to processes; (3) File system management: organizing, storing, retrieving data on storage devices; (4) Device management (I/O): managing device drivers and I/O operations; (5) Security and protection: controlling access to resources, user authentication; (6) Network management: networking stack, communication protocols; (7) User interface: CLI (command line) or GUI (graphical). Types of OS: Batch OS (early — no user interaction), Time-sharing/Multitasking OS (multiple users/tasks concurrently), Real-time OS (RTOS — strict timing guarantees, embedded systems), Distributed OS (multiple networked computers appear as one), Embedded OS (resource-constrained devices), Mobile OS (Android, iOS). Examples: Windows, macOS, Linux, Android, iOS, FreeBSD, Solaris. OS as resource manager: hardware (CPU, memory, I/O) is scarce and shared — OS arbitrates access, enforces policies, and ensures fair/efficient use while maintaining security boundaries between processes.

Open this question on its own page
02

What is a process?

A process is a program in execution — an active entity with its own memory space, system resources, and execution context. A program is a passive, stored set of instructions (binary on disk); a process is that program being actively run. Process components: (1) Code segment (text): the executable instructions; (2) Data segment: global and static variables; (3) Heap: dynamically allocated memory (malloc/new); (4) Stack: local variables, function call frames, return addresses; (5) Process Control Block (PCB): OS data structure storing process state — PID, program counter, CPU registers, memory maps, open files, process state. Process states: New (being created) → Ready (in queue, waiting for CPU) → Running (executing on CPU) → Waiting/Blocked (waiting for I/O, event) → Terminated (finished or killed). Process vs program: one program can spawn multiple processes (browser tabs). Multiple processes can run the same program simultaneously (multiple users running ls). Multiprogramming vs multitasking: multiprogramming = multiple programs loaded in memory at once; multitasking = rapid CPU switching among them giving illusion of simultaneous execution. Process creation: fork() (Unix) creates a child process as a copy of parent; exec() replaces process image with a new program; CreateProcess() (Windows). Process overhead: processes have high overhead — separate memory space, creation is expensive. Threads within a process share memory, are cheaper to create.

Open this question on its own page
03

What is a thread?

A thread is the smallest unit of execution within a process — a lightweight process. Multiple threads within the same process share the same memory space (code, data, heap) but each has its own stack, program counter, and CPU registers. Thread vs process: threads share memory (no context-switch overhead for memory mapping), faster to create/destroy (no memory allocation), communicate easily (shared memory), but share risks (one thread crash can kill the process, data races). Processes are isolated (crash in one doesn't affect others, separate memory), safer but heavier. Thread components (own): stack (local variables, function calls), program counter (where it is in execution), CPU registers (current state). Thread components (shared with process): code segment, data segment (global vars), heap (dynamic memory), file descriptors, signals. Multithreading benefits: responsiveness (UI stays active while I/O happens), resource sharing (share data without IPC), economy (cheaper than processes), scalability (run on multiple CPUs simultaneously). Java threads: Thread t = new Thread(() -> { System.out.println("Thread: " + Thread.currentThread().getName()); }); t.start();. User-level threads vs kernel-level threads: user-level — managed by runtime library (fast, OS doesn't know about them, can't utilize multiple CPUs); kernel-level — managed by OS (slower, true parallelism on multicore). Modern OSes: M:N hybrid mapping.

Open this question on its own page
04

What is a process control block (PCB)?

A Process Control Block (PCB) is the data structure maintained by the OS kernel to represent and track every process. When a process is created, the OS creates a PCB; when the process terminates, the PCB is freed. PCB contains: (1) Process state: ready, running, waiting, etc.; (2) Program counter (PC): address of next instruction to execute; (3) CPU registers: all register values (accumulator, index registers, stack pointer) — saved/restored on context switches; (4) CPU scheduling information: process priority, scheduling queue pointers; (5) Memory management information: page tables, segment tables, base and limit registers; (6) Accounting information: CPU time used, wall-clock time, time limits, account numbers; (7) I/O status information: list of I/O devices allocated, open file tables; (8) Process ID (PID): unique identifier; (9) Parent PID (PPID); (10) Process group, session; (11) Signal masks; (12) Open file descriptors. Context switching: when CPU switches from Process A to Process B, the OS saves A's CPU state into A's PCB, loads B's state from B's PCB, then resumes B. This takes time (overhead) — hundreds of nanoseconds to microseconds. Frequent context switching wastes CPU time. PCB in Linux: the task_struct structure (~600 fields in modern Linux kernel). Every process and thread has a task_struct.

Open this question on its own page
05

What is CPU scheduling?

CPU scheduling is the process of determining which ready process or thread should run on the CPU next. Since CPU is a scarce resource and multiple processes may be ready simultaneously, the OS scheduler decides which gets the CPU and for how long. Scheduling objectives: maximize CPU utilization (keep CPU busy), maximize throughput (jobs completed per unit time), minimize turnaround time (time from submission to completion), minimize waiting time (time in ready queue), minimize response time (time from request to first response), fairness. Types of scheduling: Non-preemptive (cooperative): once a process has the CPU, it keeps it until it finishes or blocks (I/O). Simple but poor for interactive systems. Preemptive: scheduler can interrupt a running process and give CPU to another. Most modern OSes use this. Required for fair sharing and real-time constraints. When scheduling decisions occur: process switches from running to waiting (always); process terminates (always); process switches from running to ready (preemptive); new process enters ready queue (preemptive). Scheduling algorithms: FCFS (First Come First Served), SJF (Shortest Job First), Priority Scheduling, Round Robin, Multilevel Queue, CFS (Completely Fair Scheduler — Linux default). Dispatcher: the module that gives CPU control to the selected process — involves context switching, switching to user mode, jumping to correct location in program. Dispatch latency = time to stop one process and start another.

Open this question on its own page
06

What is FCFS scheduling?

First Come First Served (FCFS) is the simplest CPU scheduling algorithm. Processes are scheduled in the order they arrive in the ready queue — a FIFO queue. The process at the front gets the CPU; new arrivals go to the back. Example: Processes: P1 (burst: 24ms), P2 (burst: 3ms), P3 (burst: 3ms) arriving at t=0: P1 → P2 → P3 (in arrival order). Gantt chart: |P1(0-24)|P2(24-27)|P3(27-30)|. Waiting times: P1=0ms, P2=24ms, P3=27ms. Average waiting time = (0+24+27)/3 = 17ms. Convoy effect: if P3 and P2 arrived first: |P3(0-3)|P2(3-6)|P1(6-30)|. Waiting: P3=0, P2=3, P1=6. Average = 3ms — much better! If a CPU-intensive process arrives before many short I/O-bound processes, all short processes wait for the long one. This is the Convoy Effect — a major problem with FCFS. Characteristics: non-preemptive; simple implementation (just a queue); poor average waiting time for mixed workloads; no starvation (every process eventually runs); poor for interactive systems (long waits for short processes); fair in the "first-come" sense. Performance: depends heavily on arrival order. Optimal if processes arrive with shortest first. Worst if longest arrives first. Used in: batch systems where fairness by arrival order is acceptable, real-time disk scheduling variation (SCAN algorithms build on FCFS).

Open this question on its own page
07

What is Round Robin scheduling?

Round Robin (RR) is a preemptive CPU scheduling algorithm designed for time-sharing systems. Each process gets a fixed time slice (quantum/time slot) of CPU time; when it expires, the process is preempted and moved to the back of the ready queue. Algorithm: maintain a circular FIFO queue of processes. Give each process Q time units (quantum). If the process finishes within Q, release CPU voluntarily. If not, preempt and add to rear of queue. Repeat. Example (Q=4ms): P1(24), P2(3), P3(3). |P1(0-4)|P2(4-7)|P3(7-10)|P1(10-14)|P1(14-18)|P1(18-22)|P1(22-26)|P1(26-28)| Wait: P1=6, P2=4, P3=7. Avg=5.67ms. Turnaround: P1=28, P2=7, P3=10. Choice of quantum: if Q is very large → degenerates to FCFS; if Q is very small → high context-switch overhead; Q should be large relative to context-switch time (typical: 10-100ms). Rule: 80% of CPU bursts should be shorter than Q. Characteristics: preemptive; fair — every process gets CPU regularly; good response time for short processes; higher average turnaround than SJF; starvation-free; context switches add overhead; more complex than FCFS. Used in: virtually all modern OSes use variants. Linux's CFS uses a virtual time-based variant. Java green threads used Round Robin. Advantage: ideal for time-sharing — users get responses quickly even with many processes.

Open this question on its own page
08

What is Shortest Job First (SJF) scheduling?

Shortest Job First (SJF) selects the process with the smallest next CPU burst for scheduling. It is provably optimal for minimizing average waiting time when all processes are available simultaneously. Non-preemptive SJF: once CPU is given to a process, it runs until it completes or blocks — can't preempt even if a shorter job arrives. Example (non-preemptive): P1(6ms), P2(8ms), P3(7ms), P4(3ms) all arrive at t=0. Order: P4(3), P1(6), P3(7), P2(8). Gantt: |P4(0-3)|P1(3-9)|P3(9-16)|P2(16-24)|. Avg wait = (0+16+9+3)/4 = 7ms. Preemptive SJF (Shortest Remaining Time First — SRTF): if a new process arrives with a shorter remaining burst than the current running process, preempt and switch. Optimal average waiting time for all cases: Example: P1(7) arrives t=0, P2(4) arrives t=2. At t=2, P2 has 4ms remaining, P1 has 5ms remaining → switch to P2. |P1(0-2)|P2(2-6)|P1(6-11)|. Problems: (1) Starvation: if short jobs keep arriving, long jobs may never run; (2) Predicting burst time: impossible to know the exact future CPU burst. In practice, exponential averaging of past bursts: estimated_next = α × actual_prev + (1-α) × estimated_prev. Starvation solution: aging — gradually increase priority of waiting processes over time.

Open this question on its own page
09

What is a context switch?

A context switch is the process of saving the state (context) of the currently running process/thread and restoring the state of another process/thread so it can resume execution. It's how the OS implements multitasking — switching the CPU among multiple processes rapidly. Steps in a context switch: (1) Save CPU registers (PC, stack pointer, general-purpose registers, flags) into current process's PCB; (2) Update current process's state (Running → Ready or Waiting); (3) Select next process to run (scheduler); (4) Update memory management structures (switch page tables, flush TLB for different address space); (5) Load CPU registers from next process's PCB; (6) Resume execution of next process from saved program counter. Context switch overhead: purely overhead — the CPU is doing no useful work while switching. Takes hundreds of nanoseconds to microseconds. Modern CPUs have features to speed this up (PCID — Process Context Identifier — lets TLB retain entries across context switches). What triggers a context switch: timer interrupt (time quantum expired — Round Robin), I/O request (process blocks), system call, interrupt, higher priority process becomes ready. Thread context switch vs process context switch: thread switches within the same process are cheaper — no need to switch page tables or flush TLB (same address space). Only registers and stack need switching. Measuring: perf stat -e context-switches ./program. High context switches can indicate thrashing or too many threads competing for CPU.

Open this question on its own page
10

What is an interrupt?

An interrupt is a signal to the processor from hardware or software indicating an event that requires immediate attention. Interrupts allow the OS to respond to events asynchronously without polling. How interrupts work: a device or condition raises an interrupt signal on the CPU's interrupt pin → CPU finishes the current instruction → saves current state (registers, PC) → looks up the Interrupt Vector Table (IVT) to find the Interrupt Service Routine (ISR) address → transfers control to the ISR → ISR handles the event → restores state → resumes original execution. Types of interrupts: (1) Hardware interrupts (external): keyboard keypress, mouse click, timer tick, disk I/O completion, network packet arrival — signals from I/O devices; (2) Software interrupts (traps/exceptions): triggered by executing special instructions — system calls (int 0x80, syscall instruction), division by zero, page fault, invalid memory access, overflow; (3) Timer interrupts: generated periodically by the timer chip — enables preemptive scheduling (time quantum expiration). Interrupt priority: multiple interrupts can occur simultaneously — handled by priority levels. Higher-priority interrupts can preempt lower-priority ISRs. Maskable vs non-maskable: maskable interrupts can be temporarily disabled (during critical sections); non-maskable interrupts (NMI) always handled (hardware failure, power failure). DMA (Direct Memory Access): devices transfer data to/from memory without CPU involvement, interrupt CPU only on completion — reduces CPU overhead for I/O.

Open this question on its own page
11

What is virtual memory?

Virtual memory is a memory management technique that gives each process the illusion of having its own large, contiguous address space — even if physical RAM is smaller. The OS and hardware (MMU) transparently map virtual addresses to physical addresses. Key concepts: (1) Virtual address space: each process has its own (e.g., 0 to 2^64-1 on 64-bit systems). Addresses in the program are virtual; (2) Physical memory (RAM): actual hardware memory installed in the system; (3) Paging: both virtual and physical memory divided into fixed-size blocks. Virtual blocks = pages (typically 4KB). Physical blocks = frames. Page table maps virtual page numbers to physical frame numbers; (4) Page table: per-process data structure. Maps virtual page → physical frame (or "not in RAM — in disk"); (5) Demand paging: pages loaded from disk only when first accessed (not all upfront). Reduces memory usage; (6) Page fault: when CPU accesses a virtual page not currently in RAM — OS loads it from disk (swap space), updating page table; (7) Swap space: disk area used to store evicted pages. Much slower than RAM (milliseconds vs nanoseconds). Benefits: processes can use more memory than physically installed; memory isolation between processes (different page tables); efficient memory sharing (shared libraries map to same physical frames); simplified memory allocation. Address Translation Hardware: MMU (Memory Management Unit) + TLB (Translation Lookaside Buffer — cache of recent page table entries for fast translation).

Open this question on its own page
12

What is paging?

Paging is a memory management scheme that eliminates the need for contiguous physical memory allocation by dividing virtual memory into fixed-size blocks called pages and physical memory into same-size blocks called frames. How it works: Virtual address is split into: [Page Number | Offset]. Page number indexes into the page table to find the frame number. Physical address = [Frame Number | Offset]. Example (page size = 4KB = 2^12 bytes): virtual address 12290 (0x3002) → page 3, offset 2. If page 3 maps to frame 7 → physical address = 7 × 4096 + 2 = 28674. Page table: OS maintains one per process. Entry contains: valid/invalid bit (is page in RAM?), frame number, access rights (read/write/execute), dirty bit (has page been modified?), reference bit (has page been accessed recently?). TLB (Translation Lookaside Buffer): small, fast hardware cache of recent address translations. TLB hit (page found in TLB): 1 memory access. TLB miss (page not in TLB): access page table in RAM (extra memory access), update TLB. TLB hit rate typically 99%+. Multi-level page tables: a single-level page table for 64-bit addresses would be enormous. Solution: hierarchical page tables (2, 3, or 4 levels). x86-64 uses 4-level paging (PML4, PDPT, PD, PT). Page size trade-off: larger pages = smaller page table, but more internal fragmentation. Advantages over segmentation: no external fragmentation; simpler physical memory allocation (any free frame).

Open this question on its own page
13

What is a page fault?

A page fault occurs when a process tries to access a virtual memory page that is not currently loaded in physical RAM. The CPU generates a page fault exception (trap), which transfers control to the OS's page fault handler. Page fault handling steps: (1) CPU detects invalid bit in page table entry (page not in RAM); (2) CPU generates page fault trap, saves current state; (3) OS fault handler runs; (4) Determine if the access is valid: if address is completely invalid (outside process's virtual space) → segmentation fault, kill process; if valid page but not loaded → proceed; (5) Find a free frame in physical RAM; (6) If no free frame → select a victim frame to evict using a page replacement algorithm; (7) If victim frame is dirty (modified), write to swap space; (8) Load the needed page from disk/swap into the free frame; (9) Update page table entry (set valid bit, set frame number); (10) Restart the instruction that caused the fault. Performance impact: page faults are extremely expensive. Memory access: ~100ns. Page fault with disk I/O: ~10ms = 100,000× slower! Minor vs major page faults: minor fault — page is in memory but page table not updated yet (e.g., shared page, first access to demand-zero page); no I/O needed. Major fault — page must be loaded from disk; I/O required. Thrashing: if a process has too many page faults (insufficient frames), it spends more time doing page I/O than actual computation. Working set model: ensure a process has enough frames for its "working set" (recently used pages) to avoid thrashing.

Open this question on its own page
14

What is swapping?

Swapping is the technique of temporarily moving a process or portions of a process from main memory (RAM) to secondary storage (disk/SSD — swap space) to free up RAM for other processes, then bringing it back when needed. Process-level swapping (traditional): entire processes are moved to and from swap. When memory is scarce, the OS selects a blocked or low-priority process and "swaps it out" — copies its entire address space to swap space, freeing its RAM frames. When the process is needed again, it's "swapped in" back to RAM. Page-level swapping (modern — demand paging): rather than swapping entire processes, individual pages are evicted to swap when frames are needed. This is what modern OSes do. The OS swaps out pages (not whole processes) using page replacement algorithms. Swap space: dedicated disk partition or file used for temporary storage of evicted pages/processes. Linux: /proc/swaps; Windows: pagefile.sys; macOS: compresses memory first, uses swap as last resort. Performance impact: disk I/O is orders of magnitude slower than RAM (ms vs ns). Heavy swapping = thrashing → severe performance degradation. Symptom: high disk activity, CPU mostly waiting, poor responsiveness. Modern trends: SSDs made swap less painful but still slow. Mobile OSes (iOS) don't use traditional swap — they kill background processes. Linux now uses zswap (compress pages before swapping) and zram (RAM as compressed swap). Best practice: have enough RAM to avoid swapping for production systems.

Open this question on its own page
15

What is a file system?

A file system is the method and data structure used by an operating system to control how data is stored and retrieved on storage devices. Without a file system, data would be one large blob with no way to organize or identify individual pieces. File system components: (1) Files: named collection of related data — the basic storage unit; (2) Directories (folders): hierarchical organization of files; (3) Metadata: attributes associated with files — name, size, creation time, modification time, permissions, owner; (4) Inodes: (Unix) data structures storing file metadata (not the name — that's in the directory); (5) Blocks/clusters: smallest addressable unit on disk (typically 4KB); (6) Free space management: track which blocks are available. Common file systems: ext4 (Linux — journaling, default); NTFS (Windows — journaling, permissions, encryption); FAT32 (universal, no journaling, 4GB file limit); exFAT (USB drives, no file size limit); APFS (Apple — copy-on-write, encryption, snapshots); ZFS (enterprise — integrity checks, RAID, snapshots); Btrfs (Linux — copy-on-write, snapshots). File system operations: create, open, read, write, seek, close, delete, rename, get attributes, set attributes, link, symlink. Virtual File System (VFS): abstraction layer in Linux that presents a uniform interface regardless of underlying file system type.

Open this question on its own page
16

What is deadlock?

A deadlock is a situation where two or more processes are permanently blocked, each waiting for a resource held by another — a circular waiting dependency from which no process can escape. Classic example: Process A holds Lock 1, wants Lock 2. Process B holds Lock 2, wants Lock 1. Neither can proceed — deadlock! Four necessary conditions (Coffman, 1971): ALL four must hold simultaneously for deadlock to occur: (1) Mutual exclusion: at least one resource is non-shareable — only one process can use it at a time; (2) Hold and wait: a process holds at least one resource while waiting to acquire additional resources held by others; (3) No preemption: resources can only be released voluntarily by the holding process — cannot be forcibly taken; (4) Circular wait: a set {P0, P1,...,Pn} where P0 waits for P1, P1 waits for P2,..., Pn waits for P0. Deadlock handling strategies: Prevention (eliminate one of the four conditions); Avoidance (dynamically check if granting a resource leads to deadlock — Banker's Algorithm); Detection and Recovery (allow deadlock, detect it, recover by killing a process or preempting resources); Ignorance (Ostrich algorithm — ignore deadlock; restart system when needed — used by Windows and many Unix systems in practice). Resource allocation graph: represents processes and resources as nodes, requests and holdings as directed edges. A cycle in this graph indicates deadlock (when resources have single instances).

Open this question on its own page
17

What is mutual exclusion?

Mutual exclusion (mutex) ensures that only ONE thread/process can access a critical section (shared resource) at a time. When one process is executing in the critical section, no other process can enter its critical section — this prevents race conditions. Critical section problem: code that accesses shared data and must be executed atomically — concurrent access leads to data corruption. Requirements for solution: (1) Mutual exclusion — only one process in critical section; (2) Progress — if no process is in critical section and some want to enter, selection can't be postponed indefinitely; (3) Bounded waiting — a bound on how many times others can enter before a waiting process gets its turn. Mutex lock (implementation): // Acquire mutex before entering critical section: acquire(mutex); // CRITICAL SECTION -- only one thread here! shared_counter++; // Release mutex after exiting: release(mutex);. Spinlock: busy-waits (spins in a loop checking the lock) until available. Good for short critical sections on multiprocessors — no context switch overhead. Wastes CPU if held long. Blocking mutex: puts the thread to sleep if lock unavailable, woken up when released. Better for long critical sections. Issues with mutexes: deadlock (two threads each holding one lock, waiting for the other); priority inversion (high-priority thread waits for low-priority thread holding a mutex); starvation; performance bottleneck if heavily contended.

Open this question on its own page
18

What is a semaphore?

A semaphore is a synchronization primitive invented by Dijkstra that uses an integer variable to control access to shared resources among multiple threads. It supports two atomic operations: wait() / P() / down(): while (semaphore <= 0); // Wait if no resources semaphore--; // Acquire resource. signal() / V() / up(): semaphore++; // Release resource, wake waiter. Both operations must be atomic (no interruption between check and modify). Types: (1) Binary semaphore (counting = 0 or 1): essentially a mutex lock. Initialized to 1. wait() decrements to 0 (locks). signal() increments to 1 (unlocks): // Thread A: wait(sem); // Critical section signal(sem);; (2) Counting semaphore: value can be any non-negative integer. Initialized to N (N permits). Used to limit concurrent access to N instances of a resource: Semaphore parkingSpots = new Semaphore(10); // 10 spots // Thread (car): parkingSpots.acquire(); // Wait for a spot useParking(); parkingSpots.release(); // Free the spot. Semaphore vs mutex: mutex has ownership (only owner can release); semaphore has no ownership concept (any thread can signal). Semaphore is more general. Producer-consumer with semaphores: empty = Semaphore(N); // Buffer slots available full = Semaphore(0); // Filled slots mutex = Semaphore(1); // Mutual exclusion Producer: wait(empty); wait(mutex); produce(); signal(mutex); signal(full); Consumer: wait(full); wait(mutex); consume(); signal(mutex); signal(empty);.

Open this question on its own page
19

What is a race condition?

A race condition occurs when the outcome of a program depends on the relative timing or interleaving of operations by multiple threads/processes accessing shared data — and that timing leads to incorrect behavior. The "race" is between threads to access shared state first, and the wrong order causes bugs. Classic example: // Two threads both incrementing a shared counter: Thread A: Thread B: LOAD counter (0) LOAD counter (0) ADD 1 (1) ADD 1 (1) STORE counter (1) STORE counter (1) // Result: counter = 1, but should be 2! // Thread A's write was lost. The counter++ operation is NOT atomic — it's three separate assembly instructions (LOAD, ADD, STORE). If a context switch happens between them, data is corrupted. Race condition vs data race: data race = concurrent access to the same memory location with at least one write and no synchronization. All data races are race conditions, but not all race conditions are data races (logic races don't involve memory). Detecting: ThreadSanitizer (TSan) compiler flag (-fsanitize=thread) instruments code to detect data races at runtime. Helgrind (Valgrind tool). Preventing: mutex locks around critical sections; atomic operations (std::atomic, AtomicInteger); lock-free data structures; message passing (avoid shared state); immutable data. Heisenbugs: race conditions often disappear when adding debugging output (changes timing) — notoriously hard to reproduce and fix. Always reproduce with TSan/Helgrind before fixing.

Open this question on its own page
20

What is the producer-consumer problem?

The Producer-Consumer problem (Bounded-Buffer problem) is a classic synchronization problem where producers generate data and put it in a shared buffer, and consumers take data from the buffer. Challenges: buffer has a fixed capacity; producers must wait if buffer is full; consumers must wait if buffer is empty; concurrent access must be synchronized. Solution using semaphores: // Shared: Buffer buffer[N]; Semaphore empty = N; // Available slots Semaphore full = 0; // Filled slots Semaphore mutex = 1; // Buffer access lock int in = 0, out = 0; // Buffer pointers // Producer: while (true) { item = produce(); wait(empty); // Wait for empty slot wait(mutex); // Lock buffer buffer[in] = item; in = (in + 1) % N; signal(mutex); // Unlock signal(full); // Signal consumer } // Consumer: while (true) { wait(full); // Wait for item wait(mutex); // Lock buffer item = buffer[out]; out = (out + 1) % N; signal(mutex); // Unlock signal(empty); // Signal producer consume(item); }. Java BlockingQueue solution: BlockingQueue<Item> queue = new LinkedBlockingQueue<>(10); // Producer: queue.put(item); // Blocks if full // Consumer: Item item = queue.take(); // Blocks if empty. Variations: multiple producers and consumers; different produce/consume rates; different buffer policies (priority, LIFO). Real-world: printer spooler (producers = apps sending print jobs, consumer = printer), web server (producers = incoming requests, consumers = worker threads), pipeline stages.

Open this question on its own page
21

What is the readers-writers problem?

The Readers-Writers problem synchronizes access to a shared resource where multiple readers can read simultaneously (safe — no modification) but writers need exclusive access (modifying means no readers or writers can access concurrently). Requirements: multiple readers can read simultaneously; only one writer at a time; readers and writers are mutually exclusive; no starvation. First readers-writers solution (readers priority): Semaphore mutex = 1; // Protect readCount Semaphore wrt = 1; // Writer lock int readCount = 0; // Reader: wait(mutex); readCount++; if (readCount == 1) wait(wrt); // First reader locks signal(mutex); READ_DATA(); wait(mutex); readCount--; if (readCount == 0) signal(wrt); // Last reader unlocks signal(mutex); // Writer: wait(wrt); // Exclusive access WRITE_DATA(); signal(wrt);. Problem: writers can starve if readers keep arriving (there's always a reader, so wrt is never released for the writer). Second readers-writers (writers priority): writers are given preference when waiting — complex implementation preventing new readers when a writer is waiting. Real-world implementations: Java ReadWriteLock: ReadWriteLock lock = new ReentrantReadWriteLock(); lock.readLock().lock(); try { readData(); } finally { lock.readLock().unlock(); } lock.writeLock().lock(); try { writeData(); } finally { lock.writeLock().unlock(); }. Used in: database systems (many readers, few writers), cache systems, configuration management.

Open this question on its own page
22

What is the dining philosophers problem?

The Dining Philosophers problem (Dijkstra, 1965) illustrates deadlock and resource contention. Five philosophers sit at a circular table with five forks, one between each pair. A philosopher needs BOTH adjacent forks to eat. Philosophers cycle between thinking and eating. Naive deadlock scenario: all philosophers simultaneously pick up their LEFT fork → all waiting for the RIGHT fork → circular wait → deadlock! Solutions: (1) Allow only 4 philosophers to eat at once: use a semaphore initialized to 4 — at most 4 can grab forks simultaneously, guaranteeing at least one can get both: Semaphore table = new Semaphore(4); philosopher i: wait(table); wait(fork[i]); wait(fork[(i+1)%5]); EAT(); signal(fork[i]); signal(fork[(i+1)%5]); signal(table);; (2) Asymmetric solution — odd/even pick-up order: odd-numbered philosophers pick up left then right; even-numbered pick up right then left. Breaks circular wait: if (i % 2 == 0) { pick left, then right } else { pick right, then left }; (3) Resource hierarchy solution: number all forks; always pick lower-numbered fork first — breaks circular wait; (4) Chandy-Misra solution (message passing): philosophers ask neighbors to pass forks — no shared memory needed. Why it matters: illustrates how seemingly correct solutions can deadlock; teaches resource ordering, hold-and-wait elimination, and preemption as deadlock prevention strategies.

Open this question on its own page
23

What is memory fragmentation?

Memory fragmentation is the inability to use available memory because it's in small, non-contiguous pieces. Two types: External fragmentation: total free memory is enough to satisfy a request, but it's scattered in small non-contiguous chunks — no single large chunk available: Memory: [Used 10K][Free 5K][Used 8K][Free 3K][Used 15K][Free 7K] Total free: 15K, but largest contiguous: 7K. A 10K request FAILS even though 15K is free!. Cause: variable-size allocation and deallocation creating "holes." Solutions: compaction (move processes to fill holes — expensive, requires relocation), paging (no external fragmentation — any page/frame pair works). Internal fragmentation: allocated memory is slightly more than requested — wasted space within an allocated region: Request: 18KB allocation system allocates in 4KB chunks: allocates 20KB (5 × 4KB) Internal waste: 2KB (unusable space inside the allocated block). Cause: fixed-size allocation units (pages, blocks). Larger allocation units → more internal fragmentation. Smaller units → more overhead. Solutions: external: compaction (move allocations to consolidate free space — requires process relocation), paging (non-contiguous allocation in equal-size frames). Internal: smaller allocation unit size, buddy system (power-of-2 sizes, merge buddies on free). Best Fit / First Fit / Worst Fit allocation policies: affect external fragmentation patterns. Best Fit minimizes waste per allocation but creates many small unusable holes. First Fit is fastest. Worst Fit leaves larger free chunks — empirically often worse.

Open this question on its own page
24

What is the difference between multiprogramming, multitasking, and multiprocessing?

These terms describe different aspects of concurrent execution: Multiprogramming: multiple programs are kept in memory simultaneously. When one process waits for I/O, the CPU switches to another instead of idling. Goal: maximize CPU utilization. CPU runs ONE program at a time, but multiple programs are loaded. Single-CPU system. Without: CPU → Run P1 → Wait I/O (idle) → Run P1 → ... With: CPU → Run P1 → Wait I/O → Run P2 → Wait I/O → Run P3 → ... Higher CPU utilization!. Multitasking (time-sharing): extension of multiprogramming where the OS rapidly switches among multiple jobs — giving each a small time slice (quantum). Users feel all programs are running simultaneously. Interactive response time is important. Same single CPU — context switching creates the illusion of parallelism. Focus: responsiveness, user experience. Multiprocessing: system has MULTIPLE physical CPUs (or cores). Processes genuinely run simultaneously on different CPUs — true parallelism. Types: Symmetric Multiprocessing (SMP — all CPUs equal, share memory, run OS and user processes); Asymmetric (one master CPU runs OS, others run user processes); NUMA (Non-Uniform Memory Access — CPUs have local fast memory and slower remote memory). Summary: multiprogramming = multiple programs in memory (CPU efficiency); multitasking = rapid switching between programs (responsiveness); multiprocessing = multiple physical CPUs (true parallelism). Modern systems have all three: multiple CPUs, each rapidly switching among many programs all loaded in memory.

Open this question on its own page
25

What is process synchronization?

Process synchronization is the coordination of concurrent processes/threads that share data to prevent race conditions and ensure data consistency. It manages the order of access and execution of concurrent operations. Why synchronization is needed: multiple processes share data (memory, files, I/O). Without coordination, concurrent access leads to inconsistent results (race conditions). Example: bank account balance updated by two concurrent transactions — both read 1000, both add 100, both write 1100 → result should be 1200 but is 1100. Synchronization mechanisms: (1) Mutex locks: ensure only one thread in critical section; (2) Semaphores: integer-based signaling, counting access; (3) Monitors: high-level synchronization construct — encapsulates data and operations, ensures only one process active at a time within; (4) Condition variables: allow threads to wait within a monitor until a condition becomes true — wait(), notify(), notifyAll() in Java; (5) Atomic operations: indivisible operations that complete without interruption (CAS — Compare and Swap, atomic fetch-and-add); (6) Lock-free data structures: use atomic operations to avoid locks altogether; (7) Message passing: avoid shared state entirely — communicate via messages. Synchronization in Java: synchronized keyword (method/block), wait()/notify(), java.util.concurrent (ReentrantLock, CountDownLatch, CyclicBarrier, Semaphore, BlockingQueue). Goal: ensure safe access to shared resources while maximizing concurrency — too much synchronization causes bottlenecks; too little causes races.

Open this question on its own page
26

What is a monitor in operating systems?

A monitor is a high-level synchronization construct that bundles shared data with the procedures that access it, automatically ensuring mutual exclusion. It's like a class (OOP) with built-in locking — only ONE process/thread can be active inside the monitor at any time. Concept: the monitor handles locking automatically. You simply call monitor procedures; the monitor ensures mutual exclusion. Condition variables: within a monitor, threads can WAIT for conditions using condition variables. If a thread can't proceed (e.g., buffer is full), it calls wait() — releases the monitor lock and blocks. Another thread calls signal() to wake a waiter. Bounded buffer as monitor (Java synchronized): public synchronized void put(Object item) throws InterruptedException { while (count == N) wait(); // Buffer full -- wait buffer[in] = item; in = (in + 1) % N; count++; notifyAll(); // Wake consumers } public synchronized Object take() throws InterruptedException { while (count == 0) wait(); // Buffer empty -- wait Object item = buffer[out]; out = (out + 1) % N; count--; notifyAll(); // Wake producers return item; }. Condition types: (1) Hoare semantics: signaling thread immediately yields to waiting thread — signal guarantees the condition holds for the resumed thread; (2) Mesa semantics (Java uses this): signal just moves waiter to ready queue — waiter must re-check condition (hence while loop, not if). Monitors vs semaphores: monitors are higher-level, easier to use correctly, less error-prone. Semaphores are lower-level, more flexible. Java's synchronized keyword implements a monitor per object.

Open this question on its own page
27

What is inter-process communication (IPC)?

Inter-Process Communication (IPC) is the set of mechanisms that allow processes to exchange data and synchronize with each other. Processes have separate memory spaces — they need IPC to communicate. IPC mechanisms: (1) Pipes (anonymous pipes): unidirectional byte stream between related processes (parent-child). int fd[2]; pipe(fd); // fd[0]=read end, fd[1]=write end. Shell: cmd1 | cmd2; (2) Named pipes (FIFOs): pipe with a name in the filesystem — works between unrelated processes: mkfifo /tmp/myfifo; cat /tmp/myfifo & echo "hello" > /tmp/myfifo; (3) Message queues: messages sent to a queue, received by any process with access. Supports selective reading (by message type). POSIX and System V variants; (4) Shared memory: fastest IPC — two processes map the same physical memory into their address spaces. Read/write directly. Requires additional synchronization (semaphores/mutexes): shmget(), shmat(); (5) Sockets: network IPC — works on same machine (Unix domain sockets) or across network (TCP/UDP). Most flexible; (6) Signals: asynchronous notification — one process sends a signal to another: kill(pid, SIGTERM). Limited data (just signal type); (7) Memory-mapped files (mmap): file mapped into memory, shared between processes; (8) D-Bus (Linux): high-level IPC for desktop services. Choosing IPC: related processes + simple → pipes; high throughput → shared memory; across network → sockets; complex messaging → message queues.

Open this question on its own page
28

What is the difference between user mode and kernel mode?

Modern CPUs support (at minimum) two protection levels: User mode (ring 3 in x86): restricted execution environment for user applications. Cannot directly access hardware, cannot execute privileged instructions. If a user-mode program tries to access kernel memory or execute privileged instructions → CPU generates a protection fault → OS kills the process. Kernel mode (ring 0 in x86): privileged execution for the OS kernel. Full access to hardware, all memory, all instructions. No restrictions — can crash the entire system if buggy. Why the distinction? Security and stability — user programs can't interfere with the kernel or each other's protected data. One buggy app doesn't bring down the system. Mode switching: user programs request OS services via system calls (syscalls). When a process calls read(), write(), malloc() (which calls brk/mmap), etc.: (1) User mode: execute system call instruction (SYSCALL on x86-64, int 0x80 older); (2) CPU switches to kernel mode; (3) Kernel validates parameters, performs the operation; (4) Returns result; (5) CPU switches back to user mode. Context switch cost: mode switch (user↔kernel) is cheap (~50 cycles). Process context switch is expensive (100s of ns). x86 protection rings: Ring 0 (kernel), Ring 1 (device drivers — rarely used), Ring 2 (device drivers — rarely used), Ring 3 (user applications). Linux and Windows only use Ring 0 and Ring 3. Hypervisor (Ring -1): virtualization introduces Ring -1 for the hypervisor — runs below even the guest OS kernel.

Open this question on its own page
29

What is a system call?

A system call is the mechanism by which a user-mode program requests a service from the operating system kernel — the interface between user applications and the OS. When a program needs to: read/write files, allocate memory, create processes, communicate via network, access devices — it uses system calls because these operations require kernel privileges. Common system calls: Process: fork(), exec(), exit(), wait(), getpid(). File I/O: open(), read(), write(), close(), lseek(), stat(). Memory: brk(), mmap(), munmap(). Network: socket(), bind(), listen(), accept(), connect(), send(), recv(). Device: ioctl(). IPC: pipe(), shmget(), msgget(). System call execution: (1) User program calls library function (printf, fread, malloc); (2) Library function sets up system call number and arguments; (3) Executes SYSCALL instruction → CPU traps to kernel; (4) Kernel handler looks up system call table by number; (5) Validates parameters, executes privileged operation; (6) Returns result via return register; (7) CPU returns to user mode. Overhead: system calls are expensive relative to function calls — mode switch, parameter validation, kernel execution, mode switch back. Minimize unnecessary syscalls. Buffered I/O (fread vs read) batches many small reads into one large syscall. strace command (Linux): traces all system calls made by a process — essential debugging tool: strace ls -la. VDSO (Virtual Dynamic Shared Object): kernel optimization — some syscalls (gettimeofday) executed in user mode by reading kernel-mapped memory — eliminates mode switch overhead.

Open this question on its own page
30

What is thrashing?

Thrashing is a condition where a system spends more time swapping pages in and out of memory than executing actual process instructions — the CPU is mostly waiting for disk I/O, and throughput collapses. How thrashing occurs: as the OS adds more processes to increase CPU utilization → each process gets fewer frames → more page faults → processes spend more time waiting for pages → CPU becomes idle → OS adds even more processes → even more page faults → positive feedback loop → thrashing! Symptoms: CPU utilization drops to near 0%; paging device (disk/SSD) is 100% busy; system appears to freeze or be extremely slow; all processes are waiting; adding more processes makes it worse. Detection: monitor page fault rate. If rate suddenly spikes, thrashing may be occurring. Solutions: (1) Working set model: identify each process's "working set" (pages actively used) and ensure it has enough frames. If frames available < sum of all working sets, suspend a process; (2) Page fault frequency control: if PFF rate too high → give process more frames; if rate too low → take frames away. Suspend a process if no more frames available; (3) Reduce degree of multiprogramming: suspend and swap out some processes to give remaining ones enough frames; (4) Add more physical RAM: most effective long-term solution. Working set: W(t, Δ) = set of pages referenced in the last Δ time units. OS tracks this and allocates frames accordingly. If process doesn't have all its working set in memory → thrashing for that process.

Open this question on its own page
31

What is the difference between preemptive and non-preemptive scheduling?

Preemptive scheduling allows the OS to forcibly remove the CPU from a running process and give it to another — without the running process's cooperation. Non-preemptive (cooperative) scheduling relies on processes voluntarily giving up the CPU — by blocking for I/O, calling yield(), or completing. Non-preemptive: Once a process has the CPU, it runs until: it blocks (I/O, semaphore wait), it terminates, it explicitly yields. No timer-based interruptions. Advantages: simple; no context switch overhead from interrupts; predictable behavior; good for batch processing. Disadvantages: poor responsiveness for interactive users; one process can monopolize CPU; not suitable for real-time systems; convoy effect. Examples: original Mac OS (cooperative multitasking), early Windows (pre-95), MS-DOS. Preemptive: Timer interrupts the OS every quantum. OS can preempt any process at any time. Context switch overhead but fair CPU sharing. Advantages: better responsiveness; fairer CPU sharing; supports real-time constraints; prevents monopolization. Disadvantages: more complex; race conditions if shared data not protected; context switch overhead; harder to reason about timing. Examples: Linux, Windows NT+, macOS, Android, iOS — all modern OSes. Preemptive algorithms: Round Robin, Shortest Remaining Time First, Priority Scheduling with preemption, Multilevel Feedback Queue. Real-time systems: need preemptive scheduling with guaranteed response times (safety-critical: avionics, ABS, pacemakers).

Open this question on its own page
Intermediate 13 questions

Practical knowledge for developers with hands-on experience.

01

What are page replacement algorithms?

When a page fault occurs and there are no free frames, the OS must evict (replace) a page to make room. The page replacement algorithm determines which page to evict. Optimal (OPT/Belady): replace the page that won't be used for the longest time in the future. Theoretically optimal (fewest page faults) but impossible to implement (requires knowing the future). Used as benchmark. FIFO (First In First Out): replace the oldest page in memory. Simple (just a queue). Problem: Belady's Anomaly — adding more frames can INCREASE page faults! Counter-intuitive. Example: with 3 frames, access pattern 1,2,3,4,1,2,5,1,2,3,4,5: 9 faults. With 4 frames: 10 faults! LRU (Least Recently Used): replace the page not used for the longest time. Uses past as predictor of future. No Belady's anomaly. Good approximation of OPT. Implementation: timestamp each page access, choose minimum timestamp for eviction. Expensive: hardware counter or stack per page access. Approximations used in practice. LRU approximations: Clock algorithm (second chance): FIFO but give each page a second chance using a reference bit. If bit=1 when page would be evicted, clear bit and skip to next page. Clock hand sweeps through frames. LFU (Least Frequently Used): replace page with lowest access count. Recent behavior not captured well. Aging can help. NFU (Not Frequently Used) / NRU (Not Recently Used): use reference and dirty bits. Prefer clean over dirty pages (saving dirty requires disk write). Linux uses: variant of clock algorithm with multiple lists (active, inactive).

Open this question on its own page
02

What is the Banker's Algorithm?

The Banker's Algorithm (Dijkstra) is a deadlock avoidance algorithm that dynamically examines resource allocation to ensure the system never enters an unsafe state — a state from which deadlock is possible. It works like a banker who grants loans only if the bank can still satisfy all clients' maximum future needs. Data structures: Available[m] — available resources of each type; Max[n][m] — maximum demand of each process; Allocation[n][m] — currently allocated; Need[n][m] = Max - Allocation. Safety algorithm: Work = Available; Finish[i] = false for all i; while (some process not Finish): find i: Finish[i]==false AND Need[i] <= Work; if found: Work = Work + Allocation[i]; Finish[i] = true; if Finish[i] = true for all i: SAFE STATE. Resource request algorithm: when process Pi requests resources Request[i]: (1) If Request[i] ≤ Need[i], proceed; (2) If Request[i] ≤ Available, proceed; (3) Pretend to allocate: Available -= Request[i]; Allocation[i] += Request[i]; Need[i] -= Request[i]; (4) Run safety algorithm: if safe → grant request; if unsafe → deny and restore. Example: 5 processes, 3 resource types. Determine if a request can be safely granted. Limitations: must know maximum resource needs in advance (usually unknown); doesn't handle new processes arriving; overhead of running safety algorithm per request; doesn't work with resources requested in arbitrary order. Practice: most systems use detection+recovery rather than avoidance because of Banker's impracticality.

Open this question on its own page
03

What are disk scheduling algorithms?

Disk scheduling algorithms determine the order in which disk I/O requests are served. Goal: minimize seek time (time for disk arm to move to correct track) and rotational latency. FCFS (First Come First Served): serve requests in order of arrival. Fair but slow — disk arm may zigzag wildly. Example: head at 53, requests: [98,183,37,122,14,124,65,67] → total seek: 640 cylinders. SSTF (Shortest Seek Time First): serve the request closest to current head position. Reduces seek time but causes starvation of distant requests. Example: head at 53 → 65→67→37→14→98→122→124→183 → total: 236 cylinders. SCAN (Elevator algorithm): disk arm moves in one direction serving all requests, then reverses. Like an elevator. No starvation. Better than SSTF for uniformly distributed requests. Example: head at 53 moving right → 65→67→98→122→124→183→(reverse)→37→14 → total: 208. C-SCAN (Circular SCAN): like SCAN but when reaching the end, immediately jumps to the other end (without serving requests on return trip). More uniform wait time than SCAN. C-LOOK: like C-SCAN but only goes to the last request in that direction, not the disk end. More practical. LOOK: like SCAN but reverses at last request, not disk end. Modern considerations: SSDs have no seek time — disk scheduling algorithms are less relevant. SSDs use different optimization strategies (merge writes, wear leveling). NVMe SSDs have multiple queues — parallel request handling.

Open this question on its own page
04

What is a zombie process?

A zombie process is a process that has terminated (exit() called, or killed) but whose entry still remains in the process table because the parent hasn't yet called wait() to read the child's exit status. Why zombies exist: When a child process terminates, its resources (memory, file descriptors, threads) are released. However, its PCB entry is kept in the process table — it holds the exit status (return code) that the parent must eventually read via wait()/waitpid(). This is necessary: the parent may need to know how the child ended. The child is "dead" but its record persists until acknowledged. Process state Z (zombie): visible in ps aux as Z or defunct. A zombie uses virtually no resources — just a process table slot. Problem: if a parent creates many children without calling wait(), zombie processes accumulate. If process table fills up → cannot create new processes → system instability. Creating a zombie: pid_t pid = fork(); if (pid == 0) { exit(0); // Child exits immediately } else { sleep(100); // Parent doesn't call wait() -- child is zombie for 100 seconds } . Fixing zombie accumulation: parent calls wait() or waitpid(); use SIGCHLD signal handler to call wait() when a child terminates; double-fork trick — fork a child that immediately forks again and exits, orphaning the grandchild which is adopted by init (PID 1) which always calls wait(). Orphan process: a process whose parent has terminated before calling wait(). The orphan is reparented to init (PID 1) which calls wait() for it — no zombie.

Open this question on its own page
05

What is a daemon process?

A daemon is a background process that runs continuously without direct user interaction, providing services to the system or other processes. Daemons typically start at system boot and run until shutdown. Named "daemon" (from Greek mythology — spirits that work in the background). Characteristics of daemons: run in background, not associated with a terminal; typically have no stdin/stdout/stderr (or redirected to /dev/null or log files); usually run as root or a dedicated system user; often have names ending in "d" (httpd, sshd, mysqld, cron, syslogd); started by system startup scripts (systemd, init.d). Common system daemons: sshd (SSH server), httpd/nginx (web server), mysqld/postgres (database), crond (scheduled tasks — cron jobs), syslogd/rsyslogd (system logging), udevd (device management), networkd (networking), journald (systemd logging). Creating a daemon (Unix): (1) fork() and parent exits (orphans the child, shell gets prompt back); (2) child calls setsid() (creates new session, detaches from terminal); (3) fork() again (prevents daemon from opening a terminal); (4) Change working directory to / (avoid preventing unmounts); (5) Close standard file descriptors (0, 1, 2); (6) Open /dev/null for stdin; redirect stdout/stderr to log file or /dev/null; (7) Run daemon logic. systemd unit files (modern): [Service] Type=forking ExecStart=/usr/bin/myapp Restart=always. Systemd handles daemonization automatically.

Open this question on its own page
06

What is memory segmentation?

Segmentation is a memory management scheme that divides a process's address space into variable-sized, logical segments corresponding to the logical structure of the program — code, data, stack, heap, etc. Each segment has a name/number and a length. Segment table: maps (segment number, offset) → (base, limit). Entry contains: base address (where segment starts in physical memory) and limit (segment's length). Address translation: logical address = (segment, offset). If offset ≥ limit → segmentation fault. Physical address = base[segment] + offset. Example segments: Segment 0 = main program code, Segment 1 = heap, Segment 2 = stack, Segment 3 = shared library. Advantages: logical organization matches programmer's view; different protection for different segments (code=read-only, stack=read-write); easy to share (e.g., two processes share the same code segment); no internal fragmentation. Disadvantages: external fragmentation (segments of varying sizes leave unusable holes between them); needs compaction or sophisticated allocation; variable-size allocation is complex. Segmentation vs Paging: segmentation is logical division (programmer's view); paging is physical division (hardware convenience). Segmentation visible to programmer; paging usually transparent. Combined Segmentation + Paging: Intel x86 supported both (GDT/LDT for segments, then page tables for each segment). Linux and Windows set segment base=0 and limit=max, using flat segmentation (effectively only paging). Pure segmentation: less common today. Modern use: segments still present conceptually (code, data, BSS, heap, stack sections in ELF) but implementation is through paging.

Open this question on its own page
07

What is copy-on-write (COW) in operating systems?

Copy-On-Write (COW) is an optimization strategy where creating a "copy" initially shares the original data; the actual copy is made only when one of the parties modifies the data. This defers expensive copying until necessary and often avoids it entirely. fork() optimization (most important use): when fork() creates a child process, naively copying all of the parent's memory would be wasteful — many forks are immediately followed by exec() (which replaces memory). COW fork: both parent and child initially share the same physical pages (read-only); pages are marked copy-on-write in the page tables; when either writes to a page → page fault → OS copies the page → the writing process gets its own private copy; the other process continues using the original. Huge savings if child exec()s immediately. How COW works: (1) Pages shared, marked read-only + COW in page tables; (2) Write to shared page → hardware protection fault; (3) OS allocates new physical frame; (4) Copies content of original page to new frame; (5) Updates the writing process's page table entry to point to new frame (read-write); (6) Resumes execution. Other COW uses: file system snapshots (ZFS, Btrfs — write new data to new block, original preserved for snapshot); VM live migration (copy memory pages on modification); Redis AOF rewrite (fork() + COW); Python copy module and data structures. Refcount: each physical page has a reference count. COW copies when count > 1 and write occurs. After copy, both counts are 1 (exclusive). When count = 1, writes happen in place without copying.

Open this question on its own page
08

What are memory-mapped files?

Memory-mapped files map a file (or device) into the virtual address space of a process. Once mapped, the file can be accessed using regular memory operations (pointer dereference, array indexing) rather than explicit read()/write() system calls. How it works: the OS maps the file's pages into the process's virtual address space. When the process accesses a page that isn't loaded, a page fault occurs → OS reads that portion of the file into memory. Modified pages may be written back to the file ("dirty page writeback"). mmap() system call (Unix): // Open file: int fd = open("data.bin", O_RDWR); // Map 1MB starting at file offset 0: void* ptr = mmap(NULL, 1024*1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); // Access file as memory! int* data = (int*)ptr; printf("%d\n", data[0]); // Read first int from file data[0] = 42; // Write directly to file! msync(ptr, 1024*1024, MS_SYNC); // Flush to disk munmap(ptr, 1024*1024); // Unmap close(fd);. MAP_SHARED vs MAP_PRIVATE: MAP_SHARED — changes written back to file; MAP_PRIVATE — COW, changes not written to file. Benefits: performance — avoid explicit read/write syscalls; large file access without reading all into memory; lazy loading via demand paging; file sharing between processes (MAP_SHARED); simplified programming (file = array). Uses: database files (PostgreSQL, SQLite use mmap), shared memory IPC, executable loading (loader mmaps .text, .data sections), large data files (genomics, scientific), shared libraries (single physical copy, multiple process mappings).

Open this question on its own page
09

What is the difference between hard links and soft links?

Both create references to files in Unix file systems, but they work at different levels: Hard link: a directory entry that points directly to the same inode (file data) as another directory entry. Multiple filenames can reference the same inode (same file data). ln original.txt hardlink.txt # Create hard link ls -i original.txt hardlink.txt # Same inode number! # 12345 original.txt; 12345 hardlink.txt. Characteristics: both names are equal — no "original" and "link"; deleting one name doesn't delete the file (inode deleted only when link count = 0); inode stores the link count; can't cross filesystem boundaries (inode numbers only unique per filesystem); can't link directories (would break the tree structure); file accessible by any of its hard link names even if others deleted. Soft link (symbolic link, symlink): a special file that contains a PATH (text) to another file or directory. Like a shortcut. ln -s original.txt symlink.txt # Create symbolic link ls -la symlink.txt # lrwxr-xr-x symlink.txt -> original.txt. Characteristics: symlink has its own inode; points to path, not inode; if target is deleted → dangling symlink (broken link); can cross filesystem boundaries; can link directories; path can be absolute or relative; can create circular symlinks. When to use which: hard links: when you need multiple equal-access paths to the same file content; symlinks: directories, across filesystems, when "pointing to" something is the intent (shortcuts, versioning: libpython.so → libpython.so.3.11).

Open this question on its own page
10

What is file system journaling?

Journaling is a technique that maintains a log (journal) of pending file system changes before applying them. If the system crashes during a write, the journal can be replayed to complete or undo partial changes — ensuring file system consistency. Problem without journaling: a typical file write involves multiple disk operations (update data blocks, inode, directory entry, bitmap). If power fails between operations, metadata may be inconsistent (e.g., inode says file has data but bitmap says those blocks are free). Recovery requires full file system check (fsck) which can take hours on large disks. How journaling works: (1) Write-Ahead Logging: before applying changes to the file system, write a journal entry describing the intended changes; (2) Mark transaction as committed; (3) Apply actual changes to the file system; (4) Mark transaction as complete (checkpoint); (5) Journal entry can be reclaimed. Recovery: on reboot after crash, scan journal: committed but not checkpointed transactions → replay them; incomplete (not committed) → ignore/discard them. Fast recovery (seconds vs hours with fsck). Journaling modes (ext4): journal — journal data AND metadata (safest, slowest); ordered (default) — journal metadata only, but write data before metadata (good balance); writeback — journal metadata only, data may be written in any order (fastest, less safe — data before crash may be garbage but filesystem consistent). File systems with journaling: ext3, ext4, NTFS (Windows), APFS (Apple), XFS, JFS. Copy-on-Write file systems (ZFS, Btrfs): no journaling needed — always write new data to new location, atomically update pointer. Provides same consistency guarantee differently.

Open this question on its own page
11

What is the difference between concurrency and parallelism?

Often used interchangeably but they mean different things: Concurrency: dealing with multiple tasks that make progress over overlapping time periods — tasks are "in progress" simultaneously even on a single core. The OS interleaves execution. Tasks don't necessarily run at the exact same instant. // Single CPU, two threads: Thread A: ----run----wait--run-- Thread B: --------run----wait----run // They overlap in time but not in exact execution. Concurrency is about structure/design — handling multiple things at once by interleaving. Achieved via multitasking on single core. Requires synchronization for shared data. Parallelism: executing multiple tasks simultaneously — they literally run at the same instant on different hardware (multiple cores or CPUs). True simultaneous execution. // Multi-core, two threads: Core 1: ----run----run----run-- Core 2: ----run----run----run-- // Running at literally the same moment. Parallelism is about execution — doing multiple things at the same time. Requires multiple CPU cores. Data must still be synchronized. Rob Pike (Go creator): "Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once." — "Concurrency is about structure, parallelism is about execution." Can have: concurrency without parallelism (single-core multitasking); parallelism without concurrency (dedicated pipelines); both (multi-core with multitasking — most modern systems). I/O-bound vs CPU-bound: I/O-bound work benefits from concurrency (overlap I/O wait with other work); CPU-bound work needs parallelism (more cores = faster) and concurrency won't help on single core.

Open this question on its own page
12

What is a spinlock?

A spinlock is a synchronization mechanism where a thread repeatedly checks ("spins") in a loop until the lock becomes available, rather than sleeping and being woken up. It's a busy-wait lock. Implementation: // Simple spinlock (hardware atomic instruction): struct spinlock { atomic_flag locked = ATOMIC_FLAG_INIT; }; void spin_lock(spinlock* lock) { while (atomic_flag_test_and_set(&lock->locked)) { // Spin -- keep looping until lock is free! } } void spin_unlock(spinlock* lock) { atomic_flag_clear(&lock->locked); } // Usage: spin_lock(&lock); // Critical section spin_unlock(&lock);. Compare-and-Swap (CAS) based: bool compare_and_swap(int* addr, int expected, int desired); void spin_lock(int* lock) { while (!compare_and_swap(lock, 0, 1)) { /* spin */ } }. Advantages over sleeping mutex: no context switch overhead; no scheduler involvement; very fast when lock held for VERY short time (fewer nanoseconds than a context switch). Disadvantages: wastes CPU cycles (spinning = doing nothing useful); bad when lock held for long time or many threads waiting; on single-core → spinning is useless (the holder can't make progress); can cause priority inversion; battery waste on mobile devices. When to use spinlocks: multiprocessor systems only; lock held for fewer microseconds; interrupt context (can't sleep in interrupt handler). Ticket spinlock / MCS lock: fair spinlocks — threads wait in order, preventing starvation. Used in Linux kernel. Adaptive locks: spin briefly then sleep if still locked — best of both worlds. Java synchronized, Linux futex use adaptive approaches.

Open this question on its own page
13

What is priority inversion?

Priority inversion occurs when a high-priority task is blocked waiting for a resource held by a low-priority task, and medium-priority tasks preempt the low-priority task — effectively running before the high-priority task. Classic scenario: Task L (low priority): holds Resource R Task M (medium priority): no resource, just runnable Task H (high priority): wants Resource R 1. L acquires Resource R 2. H wakes up, preempts L (higher priority) 3. H tries to acquire R -- blocked! (held by L) 4. M wakes up, preempts L (M > L priority, and L is still runnable) 5. M runs to completion while H is waiting! 6. Eventually M finishes, L resumes, completes, releases R 7. H finally runs Priority order achieved: M ran before H -- INVERTED!. Famous example: Mars Pathfinder (1997) — VxWorks RTOS priority inversion caused system resets. High-priority bus management task blocked waiting for resource held by low-priority sensor task, while medium-priority tasks ran. Fix: re-enabled priority inheritance. Solutions: (1) Priority inheritance: when a high-priority task is blocked waiting for a resource, temporarily boost the lock holder's priority to that of the highest-priority waiter. Ensures the holder completes quickly. When it releases the lock, priority returns to normal. Implemented in most RTOS and Linux PTHREAD_PRIO_INHERIT; (2) Priority ceiling protocol: each resource has a "priority ceiling" = priority of highest-priority task that can use it. Any task that acquires the resource is temporarily raised to the ceiling. Prevents inversion entirely. More predictable but requires knowing all resource users in advance; (3) Disable preemption: hold the lock briefly with preemption disabled — only works for very short critical sections.

Open this question on its own page
Advanced 9 questions

Deep expertise questions for senior and lead roles.

01

What is the Linux kernel scheduling (CFS)?

The Completely Fair Scheduler (CFS) is Linux's default CPU scheduler (since kernel 2.6.23), replacing the O(1) scheduler. CFS aims to give each runnable task a fair share of CPU time — as if we had an ideal multi-tasking processor. Core concept — virtual runtime (vruntime): CFS tracks how much CPU time each task has consumed via a "virtual runtime" counter. The task with the smallest vruntime gets to run next — it's the task that has received the least CPU time relative to its weight (priority). vruntime increases faster for tasks with lower priority (nice level). Red-black tree: CFS stores tasks in a red-black tree ordered by vruntime. The leftmost node (smallest vruntime) is always the next task to run — O(log n) to find, O(1) cached in min_vruntime. Weight (nice levels): each task has a weight based on its nice value (-20 to +19). Nice -20 (highest priority) = 9× more weight than nice 0. Weight determines how fast vruntime increases: high-priority tasks accumulate vruntime slowly (run longer before being preempted). Scheduling latency: the target period for all tasks to run at least once. Default: 6ms for ≤8 tasks; scales with task count. Minimum granularity: 0.75ms (prevents too-frequent context switches). Load balancing: CFS balances load across CPUs and NUMA nodes. Periodically migrates tasks to underloaded CPUs. Control groups (cgroups): CFS supports hierarchical scheduling via cgroups — limit CPU percentage per group (containers, user sessions). EEVDF (Earliest Eligible Virtual Deadline First): scheduled to replace CFS in Linux 6.6+ — more responsive, better for latency-sensitive workloads.

Open this question on its own page
02

What is the difference between mutex, semaphore, and condition variable?

These three synchronization primitives serve different purposes and have different semantics: Mutex (Mutual Exclusion lock): binary lock for exclusive access to a resource. Owner-based: only the thread that locked can unlock. Used for protecting critical sections from concurrent access. mutex.lock(); // Acquire exclusive access shared_data++; // Critical section mutex.unlock(); // Release. Properties: ownership (locking thread must unlock), no "available count," optional recursion (reentrant mutex). Semaphore: signaling primitive with a counter. No ownership — any thread can signal. Used for: limiting concurrent access (counting semaphore), signaling between threads (binary semaphore as event). // Producer-consumer signaling: sem_wait(&empty); // Decrement, block if 0 produce(); sem_post(&full); // Increment, wake waiter. Key difference from mutex: semaphore can be signaled by a thread different from the one that waited — used for producer-consumer coordination. Mutex cannot. Condition variable: allows a thread to wait for a specific condition to become true, atomically releasing a mutex while waiting. Always used WITH a mutex. // Consumer: mutex.lock(); while (queue.empty()) { // Check condition cond.wait(mutex); // Atomically release mutex + sleep } item = queue.dequeue(); mutex.unlock(); // Producer: mutex.lock(); queue.enqueue(item); cond.notify_one(); // Wake one waiter mutex.unlock();. Why while loop? Spurious wakeups (condition variable can wake up without notify). POSIX allows spurious wakeups — always re-check the condition. Summary: Mutex = exclusive access; Semaphore = counting/signaling; Condition variable = waiting for a condition within a mutex's protection.

Open this question on its own page
03

What is virtual memory in detail — TLB, page tables, huge pages?

Deep dive into virtual memory implementation: Address translation pipeline (x86-64): 64-bit virtual address → 4-level page table walk → physical address. VA bits: [PML4(9)][PDPT(9)][PD(9)][PT(9)][Offset(12)]. Each level indexes 512 entries. Physical address = PTE.frame_number × 4096 + offset. Translation: 4 memory accesses per translation without TLB! TLB (Translation Lookaside Buffer): small, fully associative hardware cache of recent {virtual page → physical frame} mappings. L1 TLB: 64-128 entries, 4-cycle access. L2 TLB: 1024+ entries, ~12-cycle access. TLB hit: 1 cycle extra. TLB miss: 4 memory accesses (full page table walk). TLB hit rate: 99%+ for most workloads. TLB shootdown: when OS modifies a page table entry, it must invalidate the TLB entry on ALL CPUs. Uses Inter-Processor Interrupts (IPIs) — expensive in multiprocessor systems. PCID (Process Context Identifier): tag each TLB entry with the process ID — no need to flush entire TLB on context switch. Significant performance improvement. Intel introduced in Westmere; Linux uses since 4.14. Huge pages (large pages): instead of 4KB pages, use 2MB or 1GB pages. Fewer TLB entries needed to cover the same memory. Huge page pros: fewer TLB misses, fewer page table levels, less page table memory. Cons: internal fragmentation (wasted space in huge page), harder to allocate physically contiguous memory. Linux huge pages: explicit: mmap(, 2MB, MAP_HUGETLB). Transparent Huge Pages (THP): kernel automatically converts small pages to huge pages when beneficial. Critical for databases (PostgreSQL, MySQL, MongoDB benefit greatly).

Open this question on its own page
04

What is the boot process of a Linux system?

The Linux boot process involves multiple stages from power-on to login prompt: 1. Power On / BIOS/UEFI POST: CPU starts executing from fixed address (0xFFFFFFF0). BIOS/UEFI performs Power-On Self Test (POST) — checks RAM, CPU, hardware. Identifies boot device. 2. Boot loader (GRUB2): BIOS loads the first sector (512 bytes, MBR) or UEFI loads the EFI application from the EFI partition. GRUB2 (or systemd-boot) presents the boot menu. Loads kernel image (vmlinuz-x.x.x) and initial RAM disk (initrd/initramfs) into memory. 3. Kernel initialization: CPU starts in real mode, switches to protected mode (32-bit) then long mode (64-bit). Decompresses itself. Initializes hardware: CPU, memory management (page tables), interrupt subsystem, basic device drivers. Mounts initramfs as temporary root filesystem. 4. initramfs: minimal filesystem in RAM containing: modules to access the real root device (RAID, LVM, encrypted disk drivers), scripts to mount the real root. After mounting real root, initramfs calls pivot_root or switch_root. 5. init system (systemd/PID 1): kernel executes /sbin/init (usually systemd). PID 1 is the first process — all other processes descend from it. systemd reads unit files, starts target units in dependency order. 6. Target units: sysinit.target → basic.target → multi-user.target (or graphical.target for desktop). Parallel service startup. 7. Login prompt: getty process on TTYs, display manager (GDM, LightDM) for graphical login. User authenticates → shell (bash, zsh) started. Key files: /etc/systemd/system/, /boot/grub/grub.cfg, /etc/fstab (filesystems to mount).

Open this question on its own page
05

What is NUMA (Non-Uniform Memory Access)?

NUMA (Non-Uniform Memory Access) is a computer memory design where memory access time depends on the memory location relative to a processor. In systems with multiple CPU sockets, each CPU has "local" memory that it can access faster than "remote" memory on other CPUs' local banks. Why NUMA? All CPUs sharing a single memory bus creates a bottleneck. NUMA adds dedicated memory per CPU. Performance: local memory access: ~100ns; remote memory access (through interconnect like QPI/UPI/HyperTransport): 200-400ns — 2-4× slower. NUMA topology example (4-socket server): CPU 0 ↔ 128GB RAM (local) ↔ CPU 1 ↔ 128GB RAM (local) ↔ CPU 2 ↔ 128GB RAM (local) ↔ CPU 3 ↔ 128GB RAM (local). All CPUs can access all RAM but local is faster. NUMA in Linux: numactl --hardware — show NUMA topology; numastat — NUMA statistics per node; numactl --membind=0 --cpunodebind=0 ./app — pin app to NUMA node 0; /proc/sys/kernel/numa_balancing — automatic NUMA balancing. NUMA-aware allocation: numa_alloc_onnode(size, node) — allocate on specific NUMA node. Impact on software: memory should be allocated on same NUMA node as the CPU that will access it. Cross-NUMA access causes latency spikes. Java: JVM NUMA-aware heap: -XX:+UseNUMA. Databases: PostgreSQL, MySQL have NUMA awareness settings. Redis: numactl --interleave=all redis-server to spread memory across nodes. False sharing across NUMA nodes is very expensive. NUMA awareness in the OS scheduler: Linux scheduler prefers running tasks on CPUs near their memory (NUMA-local), migrates tasks when necessary.

Open this question on its own page
06

What is cgroups (control groups) in Linux?

cgroups (control groups) is a Linux kernel feature that limits, accounts for, and isolates the resource usage of collections of processes. It's the foundation of containers (Docker, Kubernetes) and resource management on Linux. Key capabilities: (1) Resource limiting: set maximum CPU, memory, disk I/O, network bandwidth for a group of processes; (2) Prioritization: allocate resources proportionally among groups; (3) Accounting: measure resource usage per group for billing and monitoring; (4) Control: freeze, checkpoint, restart a group of processes. cgroup subsystems (controllers): cpu — CPU time allocation and throttling; memory — RAM and swap limits; cpuacct — CPU usage accounting; blkio — block device I/O rates and limits; net_cls/net_prio — network traffic classification; pids — limit number of processes; devices — allow/deny device access; freezer — suspend/resume groups. cgroups v1 vs v2: v1 (legacy) — each controller has its own hierarchy, complex; v2 (unified hierarchy, Linux 4.5+) — single unified hierarchy for all controllers, cleaner design. Docker and Kubernetes moving to v2. Docker container limits via cgroups: docker run --memory=512m --cpus=1.5 --blkio-weight=700 myapp. Internally: echo 536870912 > /sys/fs/cgroup/memory/docker/[id]/memory.limit_in_bytes. systemd and cgroups: systemd uses cgroups v2 to manage service resources. Every service is a cgroup. systemctl set-property sshd.service MemoryMax=256M CPUQuota=10%. kubernetes resource limits: translate to cgroup settings on each node: CPU limits → cpu.cfs_period_us + cpu.cfs_quota_us; Memory limits → memory.limit_in_bytes.

Open this question on its own page
07

What are Linux namespaces and how do they enable containers?

Linux namespaces partition kernel resources so that different groups of processes see different views of those resources. A process's namespace determines what parts of the system it can see. Namespaces + cgroups = containers (no hypervisor needed). Namespace types (7 types): (1) PID namespace: processes have a different set of PIDs. Processes in a container see themselves as PID 1 (their container init); (2) Network namespace: isolated network stack — own IP addresses, routing tables, network interfaces, firewall rules. Container gets its own eth0; (3) Mount namespace: isolated filesystem mount points. Container sees its own root filesystem; (4) UTS namespace: isolated hostname and domain name. Container has its own hostname; (5) IPC namespace: isolated inter-process communication — shared memory, semaphores; (6) User namespace: isolated user and group IDs. Container root (UID 0) maps to unprivileged user on host — rootless containers; (7) cgroup namespace (Linux 4.6+): isolated view of cgroup hierarchy. Creating namespaces: unshare(1) or unshare(2) syscall: sudo unshare --pid --fork --mount-proc bash # New PID namespace ps aux # Only sees processes in this namespace. clone() flags: CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER. Docker implementation: each container gets a new set of namespaces. Files in /proc/[pid]/ns/ show which namespaces a process belongs to. Container runtime (runc, containerd) calls clone() with all CLONE_NEW* flags, then exec()s the container process. This is literally all a "container" is at the Linux level — no magic, no VM.

Open this question on its own page
08

What is I/O subsystem and device drivers?

The I/O subsystem manages communication between the OS and all peripheral devices. It provides a uniform interface to diverse hardware through device drivers. I/O Hardware components: I/O port (device registers accessible by CPU), I/O bus (data pathway: PCIe, USB, SATA), controller (interprets high-level commands, drives physical device), DMA controller (transfers data without CPU). I/O techniques: (1) Programmed I/O (polling): CPU continuously checks device status register. Simple but wastes CPU cycles; (2) Interrupt-driven I/O: CPU initiates I/O, continues other work, device interrupts CPU on completion. CPU-efficient for slow devices; (3) DMA (Direct Memory Access): DMA controller transfers data directly between device and memory. CPU only involved to set up transfer and handle completion interrupt. Most efficient for large transfers (disk, network). Device driver role: abstracts specific hardware details; presents a uniform interface to the OS kernel (block device, character device, network interface); translates generic OS requests to device-specific commands; handles interrupts from device; manages device state and buffering. I/O layers (Linux): User Process → POSIX API (read/write) → Virtual File System (VFS) → File System (ext4, NTFS) → Block I/O Layer (I/O scheduler, request queue) → Device Driver → Hardware Controller → Physical Device. Block vs character devices: block devices (disk, SSD) — transfer data in fixed-size blocks, random access, buffered. Character devices (keyboard, serial port, mouse) — transfer one byte at a time, sequential, unbuffered. Network devices: separate category. Buffering: kernel maintains page cache — all file I/O goes through it. Reading: kernel reads block from disk into page cache → copies to user buffer. Writing: user writes to page cache → kernel flushes to disk (sync or async).

Open this question on its own page
09

What is real-time operating systems (RTOS)?

A Real-Time Operating System (RTOS) is an OS designed to serve real-time application requests and process data as it comes in, typically without delays (latency guarantees). The defining characteristic is deterministic timing — the system must respond to events within guaranteed time bounds. Hard real-time: missing a deadline is catastrophic failure. Examples: aircraft flight control (fail = crash), airbag deployment (100ms window), nuclear reactor control, anti-lock braking systems, pacemakers. The system MUST meet every deadline, guaranteed. Soft real-time: missing a deadline is undesirable but not catastrophic. Quality degrades but system continues. Examples: video streaming (dropped frames = quality degradation), online gaming (latency spike = gameplay stutter), audio processing. Firm real-time: missing deadline makes the result useless but not dangerous. Example: weather radar processing (stale data is useless but not dangerous). RTOS characteristics: deterministic interrupt latency (bounded, predictable); priority-based preemptive scheduling with bounded preemption delays; minimal OS jitter; small footprint (embedded systems); often fixed-priority scheduling (Rate Monotonic, Earliest Deadline First); limited or no memory swapping (paging causes unpredictable delays); priority inheritance to prevent inversion. Popular RTOS: FreeRTOS (microcontrollers — Arduino, ESP32), VxWorks (aerospace, defense), QNX (automotive, medical — Blackberry phones), RTEMS, Zephyr (IoT), RT-Linux (Linux with realtime patch). PREEMPT_RT Linux: Linux kernel patches for real-time behavior — all spinlocks converted to sleeping locks, preemptible interrupt handlers. Latency: typically 50-100μs guaranteed. Used in manufacturing, audio, robotics.

Open this question on its own page
Back to All Topics 53 questions total