In June 1969, three IBM researchers, Bélády, Nelson, and Shedler, published a paper describing something that isn't supposed to happen: give a running program more memory, and under one specific page-replacement policy, it runs slower, not faster (Bélády, Nelson, Shedler, Communications of the ACM, 1969). Fifty-seven years later, that result, now called Bélády's anomaly, still comes up in operating system interview questions because it's genuinely counterintuitive on first hearing. Most of the rest of an OS interview isn't counterintuitive at all. It's just detailed, and most candidates studied it once in college and never opened the textbook again.
Here's an opinion that might be wrong: most prep spends too long on process versus thread, a question with a clean textbook answer that takes ninety seconds to memorize, and not nearly enough on deadlock and page replacement, the two topics where a candidate's answer actually reveals whether they understand what a kernel does all day instead of what they read off a diagram once. A candidate who can recite the four conditions for deadlock but can't explain why the Banker's algorithm never made it into a real general-purpose kernel hasn't understood the topic, just memorized the flashcard.
This page runs through operating system interview questions across ten areas: process versus thread, process states and scheduling (FCFS, SJF, Round Robin, and priority), context switching, deadlock and the Banker's algorithm, paging and virtual memory, page replacement, synchronization primitives, inter-process communication, file systems, and thrashing. Most of it is conceptual, the way these interviews actually run, with a couple of short C snippets where an example genuinely clarifies more than another paragraph of prose would.
Process vs thread: the question every OS round opens with
Every loop opens here, junior or senior. It's a warm-up, but a shaky answer sets the tone for the forty minutes that follow.
Easy questions
15A process is an independent unit of execution with its own address space, its own file descriptors, and its own memory, isolated from every other process by the OS. A thread is a unit of execution inside a process, and every thread in that process shares the same address space, open files, and heap, but keeps its own stack, program counter, and register set.
Two threads in the same process can read and write the same variable directly. Two processes can't, not without an OS-provided mechanism like shared memory or a pipe, because that isolation is the entire point of a process boundary.
New, when it's first being created. Ready, when it's loaded into memory and waiting for CPU time. Running, when it's actually executing on a core. Waiting (or blocked), when it's stuck on I/O or some other event it can't proceed without. Terminated, when it's finished, whether that's a clean exit or a crash.
A process can bounce between ready and waiting dozens of times over its life. Every time it blocks on a disk read or a network call it drops to waiting, and every time that I/O completes it goes back to ready, not straight to running, because some other ready process might have a stronger claim on the CPU right now.
The current process's, or thread's, entire register state, program counter, stack pointer, and general-purpose registers get saved into that process's PCB, its process control block. The incoming process's saved state then gets loaded from its own PCB back into the CPU's registers. For a full process switch, the memory management unit also gets pointed at a different page table, since the incoming process has its own address space.
struct pcb {
int pid;
int state; // NEW, READY, RUNNING, WAITING, TERMINATED
unsigned long pc; // saved program counter
unsigned long regs[16]; // saved general-purpose registers
unsigned long *page_table;
int priority;
};None of that is real kernel source, just a simplified shape of what a PCB actually holds, but it's close enough to answer the follow-up almost every interviewer asks next: what makes a thread switch cheaper than a process switch. Drop the page_table pointer entirely, same address space, same page table, and you've got the whole answer.
Mutual exclusion: at least one resource is held in a non-shareable mode, so only one process can use it at a time. Hold and wait: a process holding at least one resource is simultaneously waiting to acquire another. No preemption: a resource can only be released voluntarily by the process holding it, never forcibly taken away. Circular wait: a set of processes each waiting on a resource held by the next process in a cycle.
All four have to hold at once for deadlock to actually happen (they're sometimes called the Coffman conditions, after the 1971 paper that first laid them out formally). Break any single one and deadlock becomes structurally impossible, which is exactly the lever every prevention strategy pulls on.
Paging divides physical and virtual memory into fixed-size blocks, 4KB is the common default on x86-64, so every page is exactly the same size regardless of what's stored in it. Segmentation divides memory into variable-size logical units, a code segment, a data segment, a stack segment, each sized to whatever it actually needs to hold.
Paging's fixed size eliminates external fragmentation, free space scattered in gaps too small individually to be useful, but introduces internal fragmentation instead: a process that needs 4KB and 1 byte gets allocated two full pages, wasting most of the second one. Segmentation avoids that specific waste but reintroduces external fragmentation, since variable-size segments leave variable-size gaps behind as they're allocated and freed. Most modern systems, x86-64 included, use paging as the primary mechanism and treat segmentation as mostly a legacy feature at this point.
FIFO evicts whichever page has been resident the longest, regardless of whether it was just used a second ago or hasn't been touched in an hour, tracked with nothing more than a queue of arrival order. LRU, Least Recently Used, evicts whichever page hasn't been accessed for the longest stretch of time, which usually predicts future access better than arrival order does, since real programs tend to reuse recently touched data, what's called locality of reference. Optimal, also called Bélády's MIN, evicts whichever page won't be needed again for the longest time in the future, which requires knowing the future access pattern in advance.
Optimal isn't implementable in a real system, since no OS can see future memory accesses before they happen, but it's the theoretical best case every other algorithm gets measured against.
A race condition happens when the correctness of a program's output depends on the timing or interleaving of multiple threads or processes accessing shared data, and different timings produce different, sometimes wrong, results. A critical section is the specific piece of code that touches the shared resource, the part that has to run without another thread interleaving inside it for the result to stay correct.
Two threads incrementing the same counter without protection is the textbook case. counter++ isn't atomic, it's a read, an add, and a write, and if both threads read the same old value before either writes back, one increment gets silently lost even though the code looks correct on the page.
Pipes, and named pipes, for a simple byte stream between related processes, the classic ls | grep shell pattern. Message queues for structured, discrete messages that can persist even if the receiving process isn't running yet. Shared memory for the fastest raw throughput, since it skips the kernel entirely for the actual data transfer. Sockets when the processes might be on different machines instead of the same host, or when you want one mechanism that works for both cases without changing the code.
Signals belong in the same conversation but do a different job: they deliver a small, fixed set of asynchronous notifications, a process died, terminate now, a timer fired, not general-purpose data.
An inode is a data structure holding a file's metadata, permissions, owner, size, timestamps, and pointers to the actual data blocks on disk, everything except the file's name. The name lives in a separate directory entry that maps a filename to an inode number, which is the whole reason two names can point at the same underlying file.
Thrashing is a state where the system spends more time paging data in and out of memory than actually executing process code, and it happens when the combined working set of every running process, the pages each one is actively touching, exceeds the physical RAM available to hold them. Under real memory pressure, adding another process doesn't add more useful work per second. It adds another working set competing for the same undersized pool of frames, so every process's pages get evicted more often, faults spike further, and actual throughput drops even as the CPU shows high utilization, almost all of it spent servicing faults instead of running application code.
The kernel is the core piece of software that talks directly to hardware: it manages the CPU scheduler, memory, device drivers, and interprocess communication, and it runs in a privileged mode nothing else gets. The operating system is the whole package built around that kernel: the kernel plus system libraries, utilities, a shell, a package manager, sometimes a GUI. Linux, strictly speaking, is just a kernel. Ubuntu, Fedora, and Debian are operating systems that all happen to bundle the same Linux kernel with different userland tools on top.
A useful analogy is engine versus car. The engine is necessary and does the real work, but nobody calls an engine a car. Interviewers ask this because plenty of engineers use the terms interchangeably and it exposes whether you actually understand the layering.
The program executes a special trap instruction (syscall on x86-64, svc on ARM), which forces a switch from user mode to kernel mode. The CPU saves the user-space register state, and the kernel looks up the syscall number in a dispatch table to find the right handler. Because a user process could pass a garbage or malicious pointer, the kernel has to validate every argument before touching it, then it does the actual work (reading a file, allocating memory, sending a packet), copies any results back into the user's buffer, restores the saved registers, and returns control to the instruction right after the trap.
This round trip costs real cycles even for something trivial, which is why glibc wraps common syscalls and why the kernel exposes a vDSO for a handful of calls like gettimeofday, letting user code get the answer without ever trapping into the kernel at all.
Modern CPUs have privilege levels, rings on x86, exception levels on ARM. Kernel mode can execute privileged instructions: modifying page tables, masking interrupts, touching arbitrary physical memory, talking directly to device registers. User mode is deliberately restricted from all of that.
The split exists for isolation. A bug in a user application, a null pointer dereference or an infinite loop, shouldn't be able to corrupt kernel memory or bring down the whole machine. The only way to move from user mode to kernel mode is through a controlled gate: a system call, a hardware interrupt, or a CPU exception like a page fault. Virtualization added another layer on top of this (VMX root mode on Intel, sometimes called ring -1) so a hypervisor can isolate entire guest kernels from each other the same way a kernel isolates user processes.
A file descriptor is just a small non-negative integer that indexes into a per-process table the kernel maintains. Each entry points to an open file description, which tracks things like the current offset and access flags, and that in turn points to the underlying inode, socket, or pipe. Descriptors 0, 1, and 2 are stdin, stdout, and stderr, opened by convention before your program's main() even runs, usually inherited from the shell that launched it.
This is also how redirection actually works at the implementation level. Shell syntax like `cmd > file` doesn't move data around magically, it opens the file, then calls dup2() to make file descriptor 1 point at that new open file description instead of the terminal, and every subsequent write to stdout silently goes to the file.
A zombie is a process that has already called exit() but whose exit status hasn't been collected yet by its parent through wait() or waitpid(). It's not running, it holds no memory or CPU, but its entry stays in the process table just to preserve that exit code, and it consumes a PID slot. If a parent has a bug and never calls wait(), zombies pile up and can eventually exhaust available PIDs on the system.
An orphan is the opposite situation: the parent dies first while the child is still running. The child gets reparented, usually to init or PID 1, or to the nearest subreaper if one is configured, and that new parent periodically reaps any children that exit. Orphans aren't really a problem in practice because something is always responsible for cleaning them up. Zombies are the ones that actually indicate a bug.
Medium questions
25Creating a process means the OS sets up a new address space, a new page table, and copies or remaps a chunk of the parent's memory (fork() on Linux uses copy-on-write, so the copy itself is cheap upfront, but the page table setup and process control block allocation still aren't free). Creating a thread reuses the existing process's address space and page table entirely, so the OS only has to allocate a new stack and a new set of registers, a fraction of the bookkeeping.
That's also why switching between two threads in the same process is cheaper than switching between two different processes: the page table doesn't change, so there's no TLB flush to pay for.
First-Come-First-Served runs processes strictly in arrival order, no preemption, dead simple to implement, and exactly as fair as a single-lane checkout line: whoever got there first gets served first, regardless of how long their job takes. Shortest-Job-First always picks whichever ready process has the smallest estimated burst time next, which provably minimizes average waiting time, but it needs to know, or guess, how long each job will run before running it, information a real scheduler usually doesn't have with any precision.
The convoy effect is what happens to FCFS when a long process gets to the front of the queue first: every short process behind it sits and waits for the whole long burst to finish, even though scheduling the short ones first would've cleared far more of the queue in the same amount of time. It's the textbook case for why arrival order and job length being unrelated makes FCFS a bad default for anything latency-sensitive.
Too small, and context-switch overhead starts to dominate. A 1ms quantum with a 0.1ms switch cost means ten percent of every CPU second gets spent switching instead of running actual work, and that ratio only worsens as the quantum shrinks further. Too large, and Round Robin degenerates toward FCFS in practice, since most processes finish or block before their quantum expires anyway, so the preemption almost never actually triggers.
There's no universally correct quantum size. It's a tuning knob traded against the specific mix of process burst lengths and the real cost of a context switch on that hardware, which is part of why production schedulers don't run a literal fixed-quantum Round Robin at all anymore.
Because no actual application work happens during the switch itself. Every cycle spent saving one process's state and loading another's is a cycle not spent running either process's code, so a context switch is close to 100 percent cost with zero direct benefit, useful only because it lets the CPU eventually get back to running something else.
The real cost isn't just the register save and restore, that part is genuinely fast, tens of nanoseconds. It's the indirect cost: a process switch flushes the TLB unless the hardware supports tagged entries, and every memory access right after the switch pays a TLB miss until the cache warms back up. A workload that context-switches thousands of times a second can lose a real chunk of total throughput to cold caches alone, even though the switch mechanism itself looks cheap in isolation.
Prevention attacks one of the four conditions structurally so deadlock can never occur, for example enforcing a fixed global lock-acquisition order so circular wait becomes impossible by construction, or requiring a process to request every resource it'll ever need upfront so hold-and-wait can't happen. It's static and doesn't need runtime information about future requests, but it's often overly conservative, blocking a request that would've been perfectly safe just to guarantee safety against every possible future.
Avoidance is more dynamic. It lets a process request resources as it goes, but before granting any request it checks whether granting it could still leave the system in a state where every process can eventually finish. The Banker's algorithm is the canonical example, and the catch is that it needs to know the maximum resource claim every process might ever make in advance, information that's often unrealistic to have about processes nobody characterized upfront.
Virtual memory gives every process the illusion of its own private, contiguous address space, isolated from every other process and larger than physical RAM might actually allow, without the process needing to know or care where its data physically lives. A page table is the structure that makes that illusion work: it maps each virtual page number to a physical frame number, plus permission bits (readable, writable, executable) and a present bit that says whether the page is actually in RAM right now or has been swapped out.
On a 64-bit system a single flat page table covering the whole address space would be enormous, so real systems use multi-level page tables, a tree of smaller tables where higher levels only get allocated for address ranges a process actually uses, instead of reserving space for the entire theoretical address space upfront.
The CPU tries to access a virtual address, checks the page table, and finds the present bit is 0, meaning that page isn't currently mapped to physical RAM. That triggers a trap into the kernel, and the page fault handler figures out why: maybe the page was swapped out to disk and needs to be read back in, maybe it's a page that was never touched yet and just needs a fresh physical frame (common right after a process starts, or after an mmap), or maybe it's a genuinely invalid access, which turns into a segmentation fault instead of a normal resolved fault.
For a resolvable fault, the OS finds or evicts a physical frame, loads the needed data into it if it exists on disk, updates the page table entry to point at that frame, flips the present bit back to 1, and resumes the instruction that faulted, which retries the memory access, now successfully. All of that happens transparently to the running process. It never sees the fault at all unless something goes genuinely wrong.
True LRU needs a timestamp, or an ordered structure, updated on literally every memory access, which is expensive enough in time and hardware support that almost nobody implements it exactly as described. What real kernels use instead is an approximation: the clock algorithm, also called second-chance, which gives every page a single reference bit set by the hardware on access, sweeps through pages in a circular buffer, and evicts the first page it finds with that bit still at 0, clearing the bit for anything it skips over instead of evicting.
Linux's actual page reclaim is a further variant of the same idea, splitting pages into active and inactive lists and moving pages between them based on recent access, which approximates LRU ordering closely enough for real workloads without paying the cost of tracking exact access order on every single touch.
A mutex is binary and has ownership: exactly one thread can hold it at a time, and, on most real implementations, only the thread that locked it is allowed to unlock it. A semaphore is a counter, initialized to whatever value makes sense for the resource, decremented on acquire and incremented on release, and it has no concept of ownership at all. Any thread can release a semaphore a completely different thread acquired.
That ownership difference matters more than the counting difference in practice. A mutex is the right tool for protecting a single shared resource. A semaphore, especially a counting semaphore initialized above 1, is the right tool for limiting concurrent access to a pool of N interchangeable resources, N database connections, N worker slots, where which specific thread grabbed which specific slot genuinely doesn't matter.
Mutual exclusion: no two processes can be inside their critical sections for the same resource at the same time. Progress: if no process is currently in its critical section and some processes want in, the decision about who goes next can't be postponed indefinitely by processes that aren't even competing for it right now. Bounded waiting: there has to be a limit on how many times other processes get to enter ahead of a process that's already waiting, so nobody starves forever behind an endless stream of other requests.
Most naive locking attempts get mutual exclusion right and quietly fail one of the other two, usually bounded waiting, which is exactly why "just use a flag variable" solutions that look correct in a two-thread example tend to fall apart the moment a third thread gets added.
Because the speed comes from skipping the kernel, and the kernel is exactly what normally provides synchronization and message boundaries for free. Two processes writing to the same shared memory region with no coordination produce the same race conditions two threads would, except now an in-process mutex won't reach across processes. You need a mutex that itself lives in shared memory and works correctly across process boundaries, a named semaphore, or a pthread mutex explicitly created with PTHREAD_PROCESS_SHARED.
There's also no built-in message framing. A pipe or a socket gives you a stream, or datagrams, with some notion of read boundaries. Raw shared memory is just a block of bytes, and the two processes have to agree entirely on their own about how to signal "here's a complete message" inside it. Shared memory wins on raw throughput and loses on almost everything else, which is why it usually shows up paired with a lighter IPC mechanism just for the signaling, not used completely alone.
A hard link is a second directory entry pointing at the exact same inode as the original file, so both names are genuinely equal, there's no "original" anymore, and the file's actual data only gets freed once every hard link to that inode is removed, tracked by the inode's link count. A symbolic link is a separate, tiny file that just stores a path string pointing at another file by name, resolved fresh every time it's accessed.
That difference has real consequences. Delete the file a hard link points at and the hard link still works fine, because it was never "pointing at a file," it was always just another name for the same inode. Delete the file a symlink points at and the symlink breaks immediately, since the path it stores no longer resolves to anything. Hard links also can't cross filesystems, inode numbers are only unique within one filesystem, while symlinks can point anywhere, including across filesystems or to something that doesn't exist yet.
In a monolithic kernel, like traditional Unix or Linux, drivers, the file system, the network stack, and the scheduler all run in kernel space inside one address space. Calling from one subsystem to another is just a function call, which is fast, but a bug in an obscure driver runs with full kernel privilege and can crash the entire machine.
A microkernel, like Minix or QNX or seL4, keeps only the bare minimum in privileged mode: scheduling, IPC, and minimal memory management. Drivers and file systems run as ordinary user-space processes that talk to each other and the kernel through message passing. If a driver crashes, you can often restart it without rebooting, which is a real win for reliability and security isolation. The cost is that message-passing IPC is measurably slower than a direct function call, and that performance gap is a big part of why pure microkernels lost the mainstream server battle to Linux. Most real systems land somewhere in between: Linux uses loadable kernel modules to get some of the modularity, and macOS's XNU is a genuine hybrid, a Mach microkernel core with a BSD-style monolithic layer bolted on top.
Multiprogramming is the oldest of the three ideas: keep several programs resident in memory so that when one blocks on I/O, the CPU switches to another instead of sitting idle. It's purely about keeping the CPU busy, with no notion of fairness or interactivity. Multitasking builds on that by time-slicing the CPU so multiple programs appear to run at once from a user's perspective, either cooperatively (old Mac OS, Windows 3.1, where a program has to voluntarily yield) or preemptively (every modern OS, where the scheduler can forcibly interrupt it).
Multithreading is a different axis entirely. It's not about running more separate programs, it's about splitting the work of one program across multiple concurrent execution paths that share the same heap, globals, and file descriptors while each keeping its own stack and register state. You can have a single-threaded multitasking OS running many separate processes, and you can have one multithreaded program creating the illusion of parallelism through rapid context switching even on a single core with no real simultaneous execution happening.
fork() creates a near-identical copy of the calling process: same code, same open file descriptors, same memory contents, but a distinct PID and its own address space from that point forward. Naively duplicating every physical page of the parent's memory at fork time would be wasteful, especially since the overwhelming majority of fork() calls are immediately followed by exec(), which throws all that inherited memory away anyway.
So instead the kernel marks the page table entries in both parent and child to point at the exact same physical pages, and flips them to read-only. Nothing is actually copied yet. The first time either process tries to write to one of those shared pages, the CPU raises a page fault, the kernel's copy-on-write handler allocates a fresh physical page, copies the content over, updates that one process's page table entry, and execution resumes as if nothing happened. This is why fork() stays cheap even for a process holding gigabytes of resident memory, and why the fork-then-exec pattern remained idiomatic on Unix instead of a single combined CreateProcess call like Windows uses.
pid_t pid = fork();
if (pid == 0) {
execve("/bin/ls", argv, envp);
} else {
waitpid(pid, &status, 0);
}A mutex, when contended, puts the calling thread to sleep. The kernel deschedules it and wakes it later through a futex wait/wake mechanism, so a blocked thread burns zero CPU, but you pay the cost of at least two context switches, one to sleep and one to wake back up, before it acquires the lock. A spinlock just busy-loops checking the lock variable in place, wasting CPU cycles but avoiding any context switch entirely.
Spinlocks make sense when the critical section is extremely short, a handful of instructions updating a counter or a small shared structure, and you genuinely expect the lock to be held only briefly. In that case spinning for a few cycles is cheaper than paying for two context switches. Inside interrupt handlers, spinlocks are effectively mandatory since you can't put an interrupt handler to sleep. On a single-core machine a naive spinlock actually becomes dangerous rather than just inefficient, which is exactly why kernels disable preemption on the current CPU while a spinlock is held.
Priority inversion happens when a low-priority task L grabs a lock, a medium-priority task M then preempts L (because M doesn't need that lock and outranks L), and a high-priority task H comes along needing the same lock L is holding, so H blocks. Effectively H, which should run first, ends up waiting behind M, which shouldn't even be in the picture. This isn't hypothetical: it's what took down the Mars Pathfinder rover in 1997, where a low-priority meteorological task held a mutex that the high-priority bus management task needed, a medium-priority task kept preempting the low-priority one, and the watchdog kept resetting the whole system.
Priority inheritance fixes it by temporarily boosting L's priority to match H's the moment H blocks on L's lock. With L now running at H's priority, M can no longer preempt it, L finishes its critical section quickly, releases the lock, drops back to its normal priority, and H proceeds. On POSIX systems you enable this with pthread_mutexattr_setprotocol and PTHREAD_PRIO_INHERIT, and most real-time operating systems support it as a first-class scheduling feature rather than an add-on.
Deadlock is when threads are stuck waiting on each other in a cycle. Nobody makes progress and nobody is even burning CPU, everything is simply frozen. Livelock is when threads are actively running and changing state, responding to each other, but the system as a whole never makes forward progress, like two people in a hallway who keep stepping the same direction to avoid each other and never actually pass. In software this happens when two threads both detect a potential conflict and both back off and retry at the exact same moment, repeatedly.
Starvation is different again: a thread simply never gets scheduled, or never wins the lock, because other threads keep getting picked ahead of it, for example a lock implementation that always grants access to whichever thread happens to request it fastest. Starvation doesn't require a cycle at all, it can happen with only two threads and none of the classic deadlock conditions present. Fair locks with FIFO wait queues fix starvation, randomized backoff fixes livelock, and breaking one of the four necessary conditions fixes true deadlock, they're three separate problems with three separate fixes.
Polling means the CPU repeatedly checks a device's status register in a tight loop to see if it's ready, which wastes cycles whenever the device is slow relative to the CPU, and disks, network cards, and keyboards almost always are. Interrupts let the device signal the CPU asynchronously the moment it actually has something ready, so the CPU can do other useful work in the meantime and only pays the cost of handling the interrupt, saving state, jumping to the handler, doing the minimal urgent work, and deferring the rest to a bottom half or softirq, when there's genuinely something to process.
The catch is that interrupt handling has real per-event overhead, and for sufficiently high event rates, think a 10 gigabit network card pushing millions of packets a second, pure interrupt-driven I/O collapses under its own overhead. That's exactly why NAPI in the Linux networking stack switches to polling mode under heavy load, processing a batch of packets without taking an interrupt for each one, and only re-enables interrupts once traffic drops back down, getting the efficiency of polling under load and the responsiveness of interrupts when idle.
Under non-preemptive scheduling, once a process gets the CPU it keeps it until it voluntarily yields, blocks on I/O, or exits. The scheduler has no mechanism to forcibly take it back. This is simple and avoids a whole category of race conditions since a process can't be interrupted mid-operation, but it means one buggy or runaway process can freeze the entire system, which is exactly what happened under cooperative multitasking in old Mac OS and Windows 3.1, one hung app could take down the whole desktop.
Under preemptive scheduling, a timer interrupt (or a computed deadline in a tickless kernel) fires periodically, and the scheduler can forcibly suspend the running process and hand the CPU to something else whether that process wants to give it up or not. Every modern general-purpose OS uses this because it's the only way to guarantee responsiveness and fairness across processes. The cost lands on the kernel itself: since a process can now be preempted at literally any instruction, including mid-update of a shared kernel data structure, the kernel has to protect its internal state with locks or explicitly disable preemption around anything that isn't safe to interrupt.
An MLFQ scheduler keeps several queues at different priority levels, usually with different time quanta: higher-priority queues get short quanta so interactive or short jobs finish fast, lower-priority queues get longer quanta for CPU-bound batch work. The scheduler always runs the highest non-empty queue first. The "feedback" part is what makes it adaptive: a process that uses its entire time quantum without blocking gets demoted to a lower-priority queue, because that behavior suggests it's CPU-bound, while a process that blocks on I/O before its quantum runs out stays put or gets promoted, because that suggests it's interactive and deserves fast turnaround.
This solves a real problem: you don't know a job's true burst time in advance the way SJF assumes you do, so instead of requiring perfect foresight, the scheduler learns behavior over time purely by observing it. The one thing you have to bolt on top is periodic priority boosting, moving every process back to the top queue every so often, specifically to stop a long-running job from sinking to the bottom queue and starving there forever, a failure mode early implementations of this idea actually ran into.
External fragmentation happens when there's plenty of free memory in total, but it's scattered across chunks too small individually to satisfy a request, even though the sum would be more than enough. This is the classic problem with variable-sized allocation schemes like segmentation, and it's why those systems eventually need compaction, physically relocating running processes to consolidate free space, which is expensive and disruptive.
Internal fragmentation happens when memory is handed out in fixed-size chunks and a process doesn't need the whole chunk, so the leftover space inside it is wasted but can't be given to anyone else. That's exactly the tradeoff paging makes: every allocation rounds up to a whole number of fixed-size pages, typically 4KB on x86, so a process needing 4097 bytes ends up allocated two full pages, wasting almost 4KB internally. What paging gains in exchange is that external fragmentation disappears entirely, since every page frame is the same size and interchangeable, any free frame satisfies any request, there's no "hole too small" problem, you just pay a small, bounded, statistically predictable internal fragmentation cost instead.
Demand paging is lazy: none of a process's pages get loaded into physical memory until they're actually referenced and fault in. This makes process startup fast, since you never wait to load an entire binary before it can run, and memory only ever holds what's genuinely being used. The cost is that every first touch of a page triggers a page fault, and if a process's working set doesn't fit comfortably in memory you get a burst of faults right after it starts.
Prepaging tries to predict which pages a process will need soon and bring them in ahead of time. A common use case is resuming a process that was previously swapped out: the OS can reload its entire prior working set in one batch I/O operation instead of taking one fault per page, which is meaningfully more efficient on rotational disks where a single large sequential read beats many small scattered ones. The risk with prepaging is that the prediction can simply be wrong, wasting I/O bandwidth loading pages the process never ends up touching, so it only pays off when locality of reference is genuinely predictable. Most general-purpose operating systems default to pure demand paging and reserve prepaging-style heuristics for specific cases like sequential file readahead.
A signal is a narrow, asynchronous notification the kernel can deliver to a process to say something happened: SIGSEGV for an illegal memory access, SIGCHLD when a child exits, SIGTERM as a polite request to shut down, SIGKILL as an unstoppable one. Delivery interrupts whatever the process was doing, even a syscall it's blocked inside can get woken up early and return EINTR, and control jumps to a handler function if the process installed one with sigaction(), or otherwise the kernel runs a default action such as terminate, dump core, stop, or ignore, depending on which signal it is.
Handlers run under real restrictions. Most C library functions aren't async-signal-safe, so calling malloc() or printf() from inside a handler is a genuine bug waiting to happen, because if the signal arrives while the process is already inside malloc's internal lock, calling malloc again from the handler deadlocks against itself. SIGKILL and SIGSTOP are special: they can't be caught, blocked, or ignored by design, which is exactly what guarantees you always have a way to stop a process no matter what it's doing.
void handler(int sig) {
write(STDOUT_FILENO, "caughtn", 7);
}
signal(SIGINT, handler);The page cache is memory the kernel uses to hold copies of file-backed data, blocks read from or about to be written to disk, so that a second read of the same data comes straight from RAM instead of hitting the disk again. It isn't a fixed reservation, it opportunistically grows to use whatever RAM isn't already claimed by running processes. That's why "free -m" on a healthy Linux box often shows very little in the "free" column but a large number under "available": the kernel is using that memory productively as cache, and it will hand it back instantly the moment a process actually needs it.
A dirty page is a page sitting in the cache that's been modified in memory but not yet written back to its underlying disk block. A normal buffered write() call typically just updates the page cache and returns immediately, the actual disk write happens later, asynchronously, through a background writeback thread, or immediately if the application explicitly calls fsync() or msync(). This is exactly why a hard power loss can lose recent writes even though write() reported success, and why databases and anything else that needs durability guarantees call fsync() explicitly instead of trusting a successful write() to mean the data actually survived a crash.
Hard questions
12Because a stack holds function-call state, local variables, return addresses, and saved registers, and that state is only meaningful to the one thread currently executing on it. If two threads shared a stack, one thread's function call would overwrite another thread's return address the moment both were mid-call, which would happen constantly the instant real concurrency showed up.
Each thread gets its own stack (typically 1MB to 8MB of virtual address space reserved for it, though only a few pages actually get committed unless the thread recurses deep enough to need more), while the heap, global variables, and open file descriptors stay shared across every thread in the process. Miss this distinction and "thread-safe" stops making sense as a concept, since it's specifically about protecting the shared parts, not the stack.
Not directly, and this is where textbook scheduling and what Linux actually runs diverge. Since kernel 2.6.23, Linux's default scheduler for normal (non-realtime) tasks is the Completely Fair Scheduler, which tracks each runnable task's vruntime, its virtual runtime, and always picks the task with the least accumulated vruntime to run next, stored in a red-black tree for fast lookup (Linux Kernel Documentation, CFS Scheduler design). The effect approximates an idealized CPU that gives every task an equal, continuous slice of time, without the choppiness a literal round robin produces.
Priority still exists (nice values shift how fast a task's vruntime accumulates, so a higher-priority task earns more real CPU time for the same vruntime cost), but it's a weighting on top of a fairness model, not a separate queue a task jumps ahead in. If an interviewer asks whether Round Robin is still used in real systems, the honest answer is that its ideas survive inside something like CFS, not that any current mainstream kernel still schedules tasks with a literal fixed-size round-robin queue.
It runs a safety check before granting anything. Given the current allocation, the maximum future need each process declared upfront, and the resources still available, it simulates whether there's some order in which every process could still get its full maximum need satisfied and finish, one at a time, using what's currently free plus whatever gets released as each simulated process completes. If such an order exists, the state is safe and the request gets granted for real. If none exists, granting the request would risk deadlock, so it gets denied or deferred even though no deadlock has actually happened yet.
Dijkstra described this in a 1965 manuscript literally as a banker deciding whether to extend a loan without risking being unable to satisfy every customer's maximum possible draw, which is where the name comes from (Dijkstra, EWD108, "Een algorithme ter voorkoming van de dodelijke omarming," An Algorithm to Prevent the Deadly Embrace). It's elegant on paper, and almost nobody runs it in a real general-purpose kernel, because it needs every process to declare its maximum resource need in advance, information application code almost never provides honestly.
Most general-purpose operating systems just ignore the problem structurally and deal with it if it happens, sometimes called the ostrich algorithm, because in practice deadlocks are rare enough for most workloads that preventing every possible one costs more than occasionally hitting one. Detection plus recovery is the more disciplined version of that same bet: periodically build a wait-for graph of who's blocked on what, check it for cycles, and if a cycle exists, deadlock exists.
Recovery from there is blunt. Kill one or more processes in the cycle, usually whichever is cheapest to restart by priority, runtime so far, or how many resources it holds, until the cycle breaks, or forcibly preempt a resource and roll a process back. Database systems are the one place this actually gets built out properly. Most, Postgres included, run an active deadlock detector on lock waits and abort one of the transactions in the cycle automatically rather than leaving both connections hung forever.
A page is mapped and present in RAM in both cases, the only question is whether the CPU's translation lookaside buffer, a small, fast cache of recent virtual-to-physical translations, happens to have that specific translation cached. A TLB miss means the mapping exists in the page table but wasn't cached in the TLB, so the hardware (or on some architectures, a short software handler) walks the page table, finds the valid mapping, and loads it into the TLB, no kernel trap needed, no interruption to normal execution. A page fault means the mapping doesn't exist yet, or the present bit is 0, which always traps into the kernel.
People confuse them because both show up as "the memory access got slower than expected," but the cost difference is enormous, a TLB miss costs maybe a few dozen cycles, a page fault that has to read from disk costs millions of cycles, five or six orders of magnitude apart. Confusing a bad TLB hit rate for a page-fault problem leads to fixing the wrong thing: huge pages help TLB pressure, more RAM or better prefetching helps actual page faults, and the two fixes aren't interchangeable.
No, and that's the whole surprise. Under FIFO specifically, Bélády, Nelson, and Shedler showed in 1969 that increasing the number of available page frames can sometimes increase the total number of page faults for the exact same reference string, the opposite of what intuition, and every other algorithm, suggests should happen (Communications of the ACM, 1969). It's a narrow result. It doesn't happen under LRU or Optimal, which both have the stack property, meaning the set of pages held at N frames is always a subset of the set held at N+1 frames, guaranteeing more frames can't hurt. FIFO simply doesn't have that property.
It gets asked less to test whether you'll ever hit this in production, you probably won't, most systems don't run raw FIFO for exactly reasons like this one, and more to see whether a candidate understands why LRU and Optimal are safe from it and FIFO isn't. That's a sharper test of real understanding than reciting three algorithm definitions back to back.
Three semaphores: empty counts available slots and starts at the buffer's full capacity, full counts filled slots and starts at 0, and a binary mutex protects the actual buffer index from concurrent read-modify-write access.
sem_t empty, full, mutex;
sem_init(&empty, 0, BUFFER_SIZE);
sem_init(&full, 0, 0);
sem_init(&mutex, 0, 1);
void producer(void *item) {
sem_wait(&empty); // wait for a free slot
sem_wait(&mutex); // lock the buffer
insert_item(item);
sem_post(&mutex); // unlock the buffer
sem_post(&full); // signal a filled slot
}
void consumer(void) {
sem_wait(&full); // wait for a filled slot
sem_wait(&mutex); // lock the buffer
void *item = remove_item();
sem_post(&mutex); // unlock the buffer
sem_post(&empty); // signal a free slot
consume(item);
}empty and full regulate how many items exist without ever touching the buffer's contents directly, and they're what actually makes a producer block when the buffer's full and a consumer block when it's empty. mutex is only there to stop two producers, or a producer and a consumer, from touching the buffer's internal index at the exact same instant. Get the wait order backward, mutex before empty in the producer, and a full buffer can deadlock the whole thing: the producer holds mutex waiting for a slot that'll never open because the consumer needs mutex to free one.
Watch the relationship between CPU utilization and actual throughput, not either number alone. Normal memory pressure looks like a rising page fault rate with CPU utilization staying reasonably high and useful work still climbing or holding steady. Thrashing looks like the page fault rate spiking hard while throughput actually falls. CPU utilization can look deceptively high in both cases, since a CPU busy servicing page faults still reads as "busy" to a simple utilization metric that doesn't distinguish real work from fault handling.
The working set model is the classical fix: track each process's actual working set size over a recent window, and only admit a new process, or keep an existing one fully resident, if the total working sets in flight still fit in available memory. In practice, the blunter version wins more often: load shedding, killing or suspending the lowest-priority process to free its frames for everything else, or on Linux specifically, the OOM killer stepping in once things get bad enough, a crude fix, but one that actually runs in production instead of staying a textbook algorithm.
Two threads on different cores are each writing to their own, logically independent variables, no real data race, correct code by any normal definition. But if those two variables happen to land on the same cache line, typically 64 bytes, the cache coherence protocol treats the whole line as one unit: every time core A writes its variable, it invalidates core B's cached copy of that entire line, even though B's variable never changed, forcing B to refetch the line from a shared cache or from memory on its very next access.
Under heavy contention this turns what should be embarrassingly parallel, independent work into a ping-pong of cache line invalidations bouncing between cores, and throughput can drop by an order of magnitude with zero visible correctness bug, it just looks like the code doesn't scale past a couple of threads. The fix is padding: aligning per-thread counters or structures to cache line boundaries, alignas(64) in C++ or an explicit padding field, so each thread's hot data lives on its own line. This is a nasty production trap specifically because a profiler shows time spent in what looks like a trivial increment operation, and the fix, adding unused padding bytes that do nothing functionally, looks bizarre until you understand what coherence traffic is actually doing underneath.
On a single core there's only one CPU actually executing instructions at any given moment. If thread A acquires a spinlock and then gets preempted by the timer interrupt before it releases it, and the scheduler happens to run thread B next, and B then tries to acquire that same spinlock, B will spin forever. The only CPU that exists is busy running B's spin loop, thread A never gets scheduled again to reach its unlock call, and there's no second core available to make progress. It isn't a cycle of two locks like a textbook deadlock, it's a single lock deadlocking against the scheduler itself.
Real kernels avoid this by disabling preemption, and often local interrupts, on the current CPU for the entire time a spinlock is held. In Linux, spin_lock() calls preempt_disable() internally, so the holder physically cannot be preempted until it calls spin_unlock(), which re-enables preemption again. This is exactly why spinlock critical sections must stay extremely short and must never call anything that might block: if a function that can sleep, taking a mutex, allocating memory in a way that can trigger reclaim, gets called while holding a spinlock, you can hang the kernel with no recovery path short of a hard reset.
This is one of the trickiest corners of kernel design, because interrupt handlers generally can't block or sleep, and servicing a page fault properly, reading from swap, waiting on disk I/O, fundamentally requires the ability to block. If the faulting address belongs to memory the kernel guaranteed would stay resident, kernel code itself, page tables, anything explicitly pinned with something like mlock, touching it should never fault at all. An unexpected fault there indicates a genuine kernel bug, and the kernel typically treats it as fatal, panicking rather than risking corruption by trying to sleep somewhere sleeping simply isn't safe.
That's exactly why kernel code paths reachable from a hardware interrupt handler are written to only ever touch memory known for certain to be pinned and resident, and why interfaces like copy_from_user() and copy_to_user(), which can legitimately fault on a user page that's been swapped out, are only ever called from process context, never from an interrupt handler, specifically to avoid this scenario. On configurations where genuinely nested faults are possible, a fault occurring while already inside a different fault handler, the kernel needs careful re-entrant handling and strict lock ordering, since the outer fault handler might already hold a lock on the memory map that the inner one also needs. Getting that ordering wrong is a classic source of kernel deadlocks, and one of the hardest bug categories to reproduce because it depends on an interrupt landing at a precise instant mid-fault.
A reader-writer lock still requires every single reader to perform an atomic operation, at minimum a memory barrier, often an actual increment or decrement of a shared reader count, just to take the read lock. That means readers on entirely different cores end up contending on the same cache line even though they aren't racing with each other on the actual data, and that overhead compounds brutally on structures read millions of times a second across dozens of cores, routing tables and the dentry cache being classic examples in Linux.
RCU's trick is that readers do essentially nothing: rcu_read_lock() on many configurations is just a compiler barrier plus a preempt_disable(), no atomic operation, no lock, the reader simply dereferences a pointer to whichever version of the structure currently exists. A writer who wants to make a change builds an entirely new copy, or a new node, with the modification already applied, then atomically swaps a single pointer so the new version becomes visible to any reader who looks after that point. Readers already mid-traversal keep seeing the old, still-valid version, since nothing they're touching was mutated in place underneath them. The writer then has to wait out a grace period, proof that every CPU has passed through at least one context switch or idle point since the pointer swap, before it's safe to actually free the old version, because that's what guarantees no reader could still hold a reference to it. The tradeoff is real: old and new versions can briefly coexist in memory, and writers pay for the allocation, the copy, and the wait, but for read-mostly, write-rare structures, RCU eliminates read-side contention almost entirely in a way a plain rwlock structurally cannot match.
How to prepare for an operating systems interview in 2026
Skip another pass through a scheduling diagram you've already redrawn five times. Run strace -f against a program you didn't write and watch the actual syscalls it makes, or write the producer-consumer code above deliberately with the wait order backward and reproduce the deadlock yourself before you ever have to explain it in an interview. Open top or htop on any machine you have and just watch process states change in real time for ten minutes. Most candidates can define "waiting" but have never actually watched a real process sit in it.
Across mock interviews run through LastRoundAI tagged backend, DevOps, or systems, deadlock and virtual memory questions trip up more candidates than process-versus-thread does, even though process-versus-thread gets asked in nearly every OS round and virtual memory only shows up in maybe half of them. My read is that process-versus-thread feels safe to over-prepare because it's the first thing every course covers, while deadlock and paging get treated as one lecture near the end of the semester that half the class skips. We don't have a clean percentage to put on that pattern, only that it comes up often enough in review to flag here.
For anyone prepping around GATE CS or a campus placement cycle in India specifically, operating systems carries more weight on paper than almost any other core subject, and the placement-round interview questions lean on the exact same four or five topics as GATE does: process scheduling, deadlock, paging, and synchronization. The overlap between a GATE syllabus and a first-round systems phone screen is bigger than most candidates expect going in.
Get the reps in before the real thing
Reading a deadlock answer off this page is not the same as defending it once an interviewer changes one number on you, drops the free-frame count by one, adds a second circular wait, asks you to run the Banker's algorithm safety check out loud. LastRoundAI's mock interview mode runs systems and backend rounds with follow-up questions that adapt to what you actually said instead of a fixed script, and the free plan includes 15 credits a month that reset monthly rather than piling up. Starter is $19/mo if fifteen sessions a month isn't enough runway.
If the harder part of the job hunt right now is finding enough backend, platform, or systems roles to apply to, rather than passing the interview once you land one, Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.
Questions about either product go to contact@lastroundai.com. That's the only inbox we check.
How this list was built
Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.
What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.
If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.
Frequently asked questions
What is the most common mistake in operating systems interviews?
Answering the question that was asked and stopping there. The strongest candidates add the trade-off or the failure mode without being prompted, which is what signals real use rather than revision.
How long does it take to prepare for a operating systems interview?
If you already work with operating systems day to day, a focused week on the areas you avoid in practice is usually enough. Coming in cold, expect three to four weeks. The gap is rarely knowledge; it is being able to explain something you normally just use.
What operating systems topics come up most often?
Interviewers concentrate on the parts that cause production incidents rather than the parts that are pleasant to learn. Expect the fundamentals to be assumed and the follow-up questions to sit one layer below what a tutorial covers.
Do I need hands-on operating systems experience to pass?
It shows quickly either way. Textbook answers hold up until the interviewer asks what you did when it broke, and that is usually the question that separates candidates. A small real project you can discuss honestly beats a longer list of familiarity claims.
Is operating systems still worth learning in 2026?
For interview purposes the question is really whether the teams you are targeting use it, which is worth checking against their actual job postings rather than general popularity rankings. Where it is in use it tends to be deeply embedded and slow to replace.

