This page is for anyone facing an operating systems round, from a campus placement to a backend or systems role. Most OS interviews start with processes, threads and their states, move to CPU scheduling and synchronization, then test deadlock and memory management: paging, virtual memory, page replacement and thrashing. Stronger rounds add system calls, interrupts, file system basics and a real debugging story. Each question shows what the interviewer is really checking, the shape of a good answer and a short answer you can say out loud. Practise saying them, then swap in your own examples.
Search all questions by round, difficulty and level, or save the ones you want to practise.
Process: a running program with its own address space, open files and other resources, isolated from other processes.
Thread: a unit of execution inside a process, with its own stack, registers and program counter.
Shared: threads share code, globals, the heap and open files, so they talk cheaply but need locks.
"A process is a program in execution with its own private address space, its own open files and its own resources, and the OS keeps it isolated from other processes. A thread lives inside a process. Each thread has its own stack, its own registers and its own program counter, but all threads in one process share the code, the global data, the heap and the open file descriptors. That sharing is the big trade-off. Threads are cheaper to create and switch between, and they can pass data just by writing to memory. But the same sharing means two threads can corrupt the same data, so you need synchronization. And because there's no isolation, one thread that crashes the process takes every other thread down with it. Separate processes are heavier but a crash in one doesn't touch the others."
Saying threads have their own heap, or that a crashing thread leaves the rest of its process running normally.
States: new, ready, running, waiting (blocked) and terminated.
Scheduler moves: ready to running when dispatched; running back to ready on preemption.
Blocking: running to waiting on I/O or an event; waiting to ready when it completes.
"A process starts in the new state while the OS sets up its control block and memory. Once it's admitted, it goes to ready, which means it could run but is waiting for a CPU. When the scheduler dispatches it, it moves to running. From running, three things can happen. If its time slice ends or a higher-priority process arrives, it's preempted and goes back to ready. If it asks for I/O, like reading from disk, or waits on a lock, it moves to waiting, also called blocked. When that I/O finishes, an interrupt tells the OS, and the process goes back to ready, not straight to running, because it still has to be picked again. Finally, when it calls exit or is killed, it moves to terminated. One detail people miss: a waiting process never goes directly to running."
Saying a blocked process goes straight to running when its I/O finishes.
Trigger: a timer interrupt, a blocking call or a system call hands control to the kernel.
Save and load: registers, program counter and stack pointer go into the old task's control block; the next task's are restored.
Process extra: a new address space means switching page tables, which can flush the TLB and leaves caches cold.
"A context switch starts when the kernel gets control, say from a timer interrupt or because the running task blocked on I/O. The kernel saves the current task's CPU state, meaning its registers, program counter and stack pointer, into its control block. Then the scheduler picks the next task and the kernel loads that task's saved state, so it resumes exactly where it left off. None of that is useful work, so it's pure overhead. Switching between two threads of the same process is cheaper because they share an address space, so the memory mapping stays the same. Switching between processes also changes the page tables, which can flush the TLB unless the hardware tags entries per address space. And the new process finds the CPU caches full of someone else's data, so it runs slowly for a while. That indirect cost is often bigger than the save and restore itself."
Describing only the register save and ignoring the TLB and cache effects that make process switches expensive.
fork: creates a copy of the calling process; returns 0 in the child, the child's PID in the parent, and -1 on failure.
exec: replaces the current program image with a new one; it only returns if it fails.
wait: the parent blocks until the child exits and collects its exit status.
"fork makes a near-copy of the calling process. Both copies continue from the same line, and the return value is how they tell each other apart: the child gets 0, the parent gets the child's PID, and if fork fails the parent gets -1. Modern systems don't copy all the memory straight away; they use copy-on-write, so pages are only duplicated when one side writes to them. A shell uses that like this. It forks, and in the child it calls exec with the command, which throws away the shell's code and loads the new program in the same process. exec never returns on success, so any line after it only runs if it failed. Meanwhile the parent calls wait or waitpid, so it blocks until the command finishes and reads its exit status. That wait also stops the child from lingering as a zombie."
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
execlp("ls", "ls", "-l", (char *)NULL);
perror("execlp"); /* only reached if exec failed */
return 1;
}
int status;
waitpid(pid, &status, 0); /* parent waits for the child */
puts("child finished");
return 0;
}
Saying exec creates a new process, or getting the fork return values for parent and child backwards.
Zombie: the child has exited but its parent hasn't collected the exit status, so a process-table entry remains.
Orphan: the parent exited first; the child keeps running and is adopted by init or a subreaper.
Cleanup: the parent calls wait; for orphans, the adopting process reaps them when they exit.
"When a child process exits, the kernel frees almost everything, but it keeps a small entry in the process table holding the exit status, so the parent can read it with wait. Until the parent does that, the child is a zombie. It uses no CPU and almost no memory, but it holds a PID, so if a buggy server forks thousands of children and never waits, it can run out of PIDs. You can't kill a zombie because it's already dead. The fix is for the parent to reap it, or to kill the parent. An orphan is the opposite case: the parent dies while the child is still running. The child isn't stopped. It gets adopted, usually by init, which is PID 1, or by a designated subreaper, and that process calls wait when the orphan eventually exits, so it doesn't stay a zombie."
Saying you fix a zombie with kill -9 on the zombie itself.
Context: what the work was and what mattered most: speed, shared data or fault isolation.
Trade-off: threads share memory cheaply but a crash or corruption hits everyone; processes isolate failures but need IPC.
Outcome: what you chose, how it behaved in practice, and what you'd change.
"At my last company we had a service that converted uploaded documents using a third-party parsing library. The first version ran each conversion on a thread in one big process, which was fast and easy because the results went straight into shared memory. But every few days one bad file made the library crash, and that took down the whole process, including every other conversion in flight. I argued for moving the parsing into a small pool of worker processes. The main process sent each job over a pipe and read the result back. It cost us some speed, since we had to copy data between processes, but a crash now killed only one worker, which we logged and restarted, and the file was marked as failed. We kept threads inside the main process for the network handling, where the code was ours and well tested. The lesson for me was to isolate the code I don't trust."
Choosing threads or processes out of habit, with no mention of what happens when one of them crashes.
Non-preemptive: a task keeps the CPU until it blocks or finishes; simple but one long task delays everyone.
Preemptive: the OS can take the CPU away, usually on a timer interrupt or when a higher-priority task becomes ready.
Trade-off: preemption gives responsiveness but needs care around shared data and adds switching overhead.
"In non-preemptive scheduling, once a task gets the CPU it keeps it until it finishes or blocks on something like I/O. It's simple and has less switching, but one long CPU-bound task can freeze everything behind it. FCFS and plain SJF are examples. In preemptive scheduling, the OS can pull the CPU away from a running task, usually when a timer interrupt says its time slice is up or when a more important task becomes ready. Round robin and shortest remaining time first work this way. On a desktop I'd want preemptive, because the user expects the mouse and keyboard to stay responsive even if some background job is crunching numbers. The cost is more context switches, and because a task can be interrupted at almost any point, shared data needs proper locking. Every mainstream general-purpose OS today uses preemptive scheduling."
Thinking preemption only happens when a process voluntarily gives up the CPU.
FCFS: run in arrival order; each job waits for the sum of the bursts ahead of it.
SJF: run shortest first; short jobs stop waiting behind long ones.
Catch: SJF is optimal for average waiting time but needs burst lengths it can only predict, and can starve long jobs.
"Call them P1 to P4 with bursts 6, 8, 7 and 3. Under FCFS they run in order. P1 waits 0, P2 waits 6, P3 waits 14 and P4 waits 21. That's 41 in total, so the average is 10.25. Under SJF I sort by burst: P4 with 3, then P1 with 6, P3 with 7 and P2 with 8. P4 waits 0, P1 waits 3, P3 waits 9 and P2 waits 16. That's 28 in total, so the average drops to 7. SJF wins because every job's wait includes the bursts ahead of it, so putting short jobs first keeps that sum small. In fact it gives the lowest average waiting time when all jobs are known up front. The problem is the OS doesn't know the next burst length, so real systems estimate it from past bursts, and a steady stream of short jobs can starve the long ones."
FCFS: P1 0-6 | P2 6-14 | P3 14-21 | P4 21-24 waits 0+6+14+21 = 41, avg 10.25
SJF: P4 0-3 | P1 3-9 | P3 9-16 | P2 16-24 waits 0+3+9+16 = 28, avg 7
Mixing up waiting time with turnaround time, or claiming the OS knows burst lengths in advance.
Mechanism: ready tasks sit in a circular queue; each runs for one quantum, then goes to the back if not done.
Too large: it behaves like FCFS and interactive tasks feel sluggish.
Too small: the CPU spends a big share of its time context switching instead of doing work.
"Round robin keeps the ready tasks in a queue. The scheduler gives the first one a fixed slice of time, the quantum. If the task finishes or blocks before the quantum ends, the next one starts. If it's still running when the timer fires, it's preempted and moved to the back of the queue. With n tasks, nobody waits more than n minus one quanta for their next turn, which is why it's fair and good for time-sharing. The quantum size is the whole game. If it's very large, most tasks finish within one slice, so it degrades into first come, first served, and an interactive task can sit behind a long batch job. If it's very small, you switch constantly, and since each switch has a fixed cost, a growing share of CPU time is wasted on switching. A good rule is to make the quantum a lot longer than a context switch, and long enough that most interactive bursts finish inside one slice."
Saying a smaller quantum is always better because the system feels more responsive.
Queues: several priority levels; higher levels get shorter quanta and run first.
Learning: a job that uses its whole quantum drops a level; one that blocks early for I/O stays high.
Safeguards: a periodic priority boost stops starvation; counting total time used at a level stops gaming.
"A multilevel feedback queue has several ready queues at different priorities. The scheduler always runs jobs from the highest non-empty queue, and the top queues use short time slices. Every new job starts at the top, because the scheduler doesn't know yet what kind of job it is. Then it watches. If a job uses its entire slice, it's probably CPU-bound, so it gets moved down a level, where slices are longer and priority is lower. If it gives up the CPU early, say to wait for keyboard input or disk, it looks interactive and stays high, so it gets quick response. Two problems need fixing. Long jobs at the bottom can starve if interactive work never stops, so every so often all jobs get boosted back to the top. And a clever program could sleep just before its slice ends to stay on top, so the scheduler counts total CPU used at a level, not per slice, before demoting it."
Describing fixed priority queues with no movement between them, which misses the feedback part entirely.
Race condition: the result depends on the timing of threads touching shared data.
Critical section: the code that touches the shared data and must not run in two threads at once.
Properties: mutual exclusion, progress and bounded waiting.
"A race condition is when the outcome depends on the exact timing of threads. The classic case is two threads doing count++ on a shared counter. It looks like one step, but it's really load, add, store, so both threads can load the same old value and one increment gets lost. The critical section is the piece of code that touches that shared data, and the fix is making sure only one thread is inside it at a time. A correct solution needs three things. Mutual exclusion: if one thread is in the critical section, no other thread can be. Progress: if nobody is inside and some threads want in, the choice can't be postponed forever, and threads that aren't trying can't block it. Bounded waiting: once a thread asks to enter, there's a limit on how many times others can go ahead of it, so it can't starve."
Listing only mutual exclusion and not knowing why progress and bounded waiting matter.
Mutex: a lock with an owner; only the thread that locked it should unlock it. Used for mutual exclusion.
Semaphore: a counter with wait and signal; any thread can signal. Used to count resources or to signal events.
Binary semaphore: can act like a lock, but has no ownership, so no priority inheritance or owner checks.
"A mutex is a lock with ownership. One thread locks it, does its critical section, and the same thread unlocks it. Because the system knows who owns it, it can do useful things like priority inheritance or catch a thread unlocking a mutex it doesn't hold. A semaphore is an integer counter with two operations. Wait decrements it and blocks if it would go below zero, and signal increments it and wakes a waiter. It's great for counting, like allowing five threads into a connection pool, or for signalling between threads, where one thread waits and a different thread signals when data is ready. A binary semaphore only goes between 0 and 1, so it can protect a critical section, but it isn't quite a mutex. There's no owner, so any thread can release it. That's fine for signalling but loses the safety checks you want for plain mutual exclusion."
Saying they're identical, or that a semaphore just means a lock that several threads can hold with no further explanation.
Two counters: one semaphore counts empty slots, starting at N; another counts full slots, starting at 0.
One lock: a mutex protects the buffer indexes while an item is added or removed.
Order: wait on the counting semaphore first, then take the lock; reversing it can deadlock.
"I use three things. An empty-slots semaphore starting at the buffer size, a full-slots semaphore starting at zero, and a mutex around the buffer itself. The producer waits on empty slots, so it blocks if the buffer is full. Then it takes the mutex, puts the item in, releases the mutex, and signals full slots so a consumer can wake up. The consumer is the mirror image: wait on full slots, lock, take an item, unlock, signal empty slots. The order matters. If the producer took the mutex first and then waited on empty slots while the buffer was full, it would sleep holding the lock. The consumer could never get the lock to remove an item, so neither side could move. That's a deadlock. The rule is: wait on the counting semaphore first, and only hold the mutex for the short moment you touch the buffer."
#define N 8
int buffer[N], in = 0, out = 0;
sem_t empty_slots; /* sem_init(&empty_slots, 0, N) */
sem_t full_slots; /* sem_init(&full_slots, 0, 0) */
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void produce(int item) {
sem_wait(&empty_slots);
pthread_mutex_lock(&lock);
buffer[in] = item;
in = (in + 1 == N) ? 0 : in + 1;
pthread_mutex_unlock(&lock);
sem_post(&full_slots);
}
int consume(void) {
sem_wait(&full_slots);
pthread_mutex_lock(&lock);
int item = buffer[out];
out = (out + 1 == N) ? 0 : out + 1;
pthread_mutex_unlock(&lock);
sem_post(&empty_slots);
return item;
}
Taking the mutex before waiting on the counting semaphore, or using a single semaphore for both full and empty.
Monitor: shared data plus the operations on it, where only one thread runs inside at a time, with condition variables for waiting.
Wait: atomically releases the lock and sleeps; on wakeup it re-acquires the lock before returning.
While loop: a wakeup is only a hint; the condition may be false again or the wakeup may be spurious.
"A monitor bundles shared data with the procedures that use it, and guarantees only one thread is active inside at a time. When a thread needs to wait for something, like a queue becoming non-empty, it waits on a condition variable. The key detail is that wait releases the monitor's lock and puts the thread to sleep in one atomic step, so no signal can slip through the gap. When it's woken, it has to re-acquire the lock before it continues. That's where the while loop comes in. In most real implementations, signalling just makes the waiter runnable. Between the signal and the moment the waiter gets the lock back, another thread might grab the item it was waiting for. Also, implementations are allowed to wake a thread spuriously, with no signal at all. So after waking I always re-check the condition, and go back to waiting if it's still false."
Using an if, or believing that a woken thread is guaranteed to find the condition still true.
Atomic instruction: test-and-set writes a value and returns the old one as one indivisible step; compare-and-swap writes only if the value still matches.
Spinlock: loop on the atomic instruction until you get the lock; good only for very short critical sections.
Interrupts: disabling them stops preemption on one core only; other cores still run, and user code can't do it anyway.
"A lock needs a step that reads and writes memory with no chance of another core sneaking in between, and the hardware gives us that. Test-and-set atomically sets a flag and returns what it was before. If the old value was false, I got the lock; if it was true, someone else has it and I try again. Compare-and-swap is more general: it writes a new value only if the location still holds the value I expect, and tells me whether it worked. Looping on this gives a spinlock. It burns CPU while waiting, so it only makes sense when the lock is held for a very short time and the waiter is on another core. For longer waits a blocking lock that puts the thread to sleep is better. Disabling interrupts used to work on a single CPU, because it stops preemption. On a multicore machine the other cores keep running, so it doesn't give mutual exclusion, and user programs aren't allowed to do it anyway."
#include <stdatomic.h>
atomic_flag lock = ATOMIC_FLAG_INIT;
void acquire(void) {
while (atomic_flag_test_and_set(&lock)) { /* spin */ }
}
void release(void) {
atomic_flag_clear(&lock);
}
Saying a normal load followed by a store is enough to build a lock.
Symptom: what went wrong, and why it was intermittent.
Investigation: how you narrowed it down: thread dumps, logs with thread IDs, a stress test that reproduced it.
Fix and lesson: the real cause, the change, and what you now do differently.
"In my final-year project we built a small multithreaded file server, and under load it would occasionally hang completely. Nothing crashed, it just stopped answering. Because it was rare, I wrote a stress script that fired hundreds of parallel requests, and that got it to hang within a minute or two. When it hung I attached a debugger and printed every thread's backtrace. Two worker threads were each stuck in a lock call. One held the lock on the client table and wanted the log lock; the other held the log lock and wanted the client table. A classic circular wait. The fix was to define one order for our locks and follow it everywhere, and I also shortened the log path so it no longer needed the table lock at all. The stress test then ran for an hour cleanly. Since then I write down lock order the moment code needs more than one lock."
A story where the fix was adding a sleep or a retry, with no idea what the real cause was.
Mutual exclusion: a resource can be held by only one process at a time.
Hold and wait: a process holds one resource while waiting for another.
No preemption and circular wait: resources can't be forcibly taken, and there's a cycle of processes each waiting on the next.
"There are four, and all of them have to be true together. First, mutual exclusion: at least one resource can only be used by one process at a time, like a printer or a lock. Second, hold and wait: a process is holding something and waiting for something else, say it holds the file lock and wants the database lock. Third, no preemption: the OS can't just take a resource away from a process; it has to be released voluntarily. Fourth, circular wait: there's a cycle, where process A waits for something B holds, and B waits for something A holds, or a longer chain that loops back. The reason this list matters is that if you can guarantee any one of the four never holds, deadlock can't happen. In practice, breaking circular wait by always taking locks in the same global order is the most common fix."
Saying any one condition on its own causes deadlock, or confusing deadlock with a process that is simply slow.
Prevention: design the rules so one of the four conditions can never hold, like lock ordering.
Avoidance: check each request at run time and only grant it if the system stays in a safe state; needs maximum needs up front.
Detection: let deadlocks happen, find cycles periodically, then recover by aborting or rolling back.
"Prevention works at design time. You make it structurally impossible for one of the four conditions to hold. For example, you require every thread to take locks in a fixed global order, which kills circular wait, or you make a process request everything it needs at once, which kills hold and wait. Avoidance works at run time. Before granting a request, the system checks whether it could still finish every process in some order, and only grants it if that stays true. The banker's algorithm does this, but it needs each process to declare its maximum needs in advance, which general-purpose programs can't really do. Detection lets deadlocks happen, periodically looks for a cycle in the wait-for graph, and recovers by killing or rolling back a victim. Many databases do exactly that with transactions. In application code I'd use prevention, mainly lock ordering, and add detection or timeouts as a safety net where a rollback is cheap."
Treating prevention and avoidance as the same thing, or suggesting the banker's algorithm for everyday application code.
Inputs: available resources, what each process holds, and its declared maximum; need equals maximum minus held.
Safety check: repeatedly find a process whose need fits in what's available, pretend it finishes and returns everything, and continue.
Meaning: if every process can finish in some order, the state is safe; unsafe means deadlock is possible, not certain.
"The banker's algorithm grants a request only if the system stays in a safe state afterwards. Each process declares its maximum need up front. Need is maximum minus what it already holds. A state is safe if there's some order in which every process can get what it still needs and finish. To check, I start with the available resources and look for any process whose remaining need fits. I pretend it runs to completion and releases everything it held, which adds to available, and I repeat. If I can finish everyone, it's safe. Take ten units of one resource. P0 holds 4 with a max of 7, P1 holds 2 with a max of 6, P2 holds 1 with a max of 3, so 3 are free. P2 needs 2, so it finishes and 4 are free. P0 needs 3, finishes, 8 free. P1 needs 4, finishes. So P2, P0, P1 is a safe sequence. An unsafe state doesn't mean deadlock has happened, only that it could."
Total 10, Available 3
Process Held Max Need
P0 4 7 3
P1 2 6 4
P2 1 3 2
P2 (avail 3 -> 4), P0 (4 -> 8), P1 (8 -> 10) => safe
Saying an unsafe state means the system is already deadlocked.
Acknowledge: a timeout can be a reasonable short-term guard so the service doesn't hang.
Risks: it hides the bug, can turn deadlock into livelock, and a retry may repeat half-finished work.
Plan: capture thread dumps when it happens, find the lock cycle, fix the ordering, keep the timeout only as a logged safety net.
"I'd say it's a fair short-term guard, because a hung service is worse than a failed request. But I wouldn't stop there. A timeout on every lock hides the actual bug, and if both threads time out and retry with the same timing, they can keep colliding, which is a livelock instead of a deadlock. Retries are also risky if the work between locks isn't safe to repeat. So I'd propose two steps. Right away, add the timeout only where a failed request can be retried safely, and make it log loudly with a thread dump when it fires, so the next occurrence gives us evidence. Then, from those dumps, find which locks form the cycle, and fix the root cause, usually by agreeing on a lock order or holding fewer locks at once. Once it's fixed, the timeout can stay as a safety net, but it should never fire in normal running."
Either blocking any short-term fix outright, or accepting timeouts as the permanent answer without finding the cycle.
Paging: memory split into fixed-size pages and frames; any page fits in any free frame.
Segmentation: memory split into variable-size logical pieces like code, stack and heap.
Fragmentation: paging wastes space inside the last page (internal); segmentation leaves unusable gaps between segments (external).
"Paging splits a process's logical memory into fixed-size pages and physical memory into frames of the same size. A page table maps each page to whatever frame it's in, so a process doesn't need contiguous physical memory. Because every frame is the same size, any free frame will do, so there's no external fragmentation. The waste is internal: if a process needs a bit more than a whole number of pages, most of its last page sits unused. Segmentation splits memory by meaning instead, into segments like code, data, heap and stack, each with its own base and length. That matches how programmers think and makes protection per segment natural. But segments are different sizes, so as they come and go, free memory gets chopped into holes that may each be too small to use, which is external fragmentation. Most modern systems rely mainly on paging, sometimes with segmentation kept only in a minimal form."
Swapping the two kinds of fragmentation, or saying paging has no wasted space at all.
Idea: each process sees a large private address space; only the pages in use need to be in RAM.
Fault: the MMU finds the page not present and traps into the kernel.
Handling: check the address is valid, find a frame (evicting if needed), load the page, update the page table, restart the instruction.
"Virtual memory gives each process its own large address space that doesn't have to fit in RAM all at once. The OS keeps only the pages in use in physical memory and the rest on disk, and the MMU translates addresses using the page table. When a program touches a page that isn't present, the MMU raises a page fault and the kernel takes over. First it checks whether the address is legal at all. If not, on Linux the process gets a segmentation fault. If it is, the kernel needs a free frame. If none is free, it picks a victim page with its replacement policy, and if that page was modified, writes it back to disk first. Then it starts reading the needed page in, and the process blocks meanwhile, so another process can run. When the read finishes, the kernel updates the page table entry to point at the frame and marks it present. Finally the faulting instruction is restarted, and this time it succeeds."
Treating every page fault as an error, or forgetting that the faulting instruction has to be restarted.
Split: the address is a page number plus an offset; the page number is looked up, the offset is kept.
TLB: a small hardware cache of recent translations; a hit skips the page-table walk.
Multi-level: a flat table is huge and mostly empty; a tree only allocates the parts that are used.
"Take a 32-bit address with 4 KB pages. The low 12 bits are the offset inside the page, and the top 20 bits are the page number. The MMU looks up the page number to get a frame number, then glues the same offset onto it. If every lookup meant reading the page table from memory, each access would cost two memory reads or more. The TLB fixes that. It's a small, fast cache of recent page-to-frame translations, and because programs have good locality, most lookups hit it. On a miss, the page table is walked and the result is cached. Now, a flat table for 20 bits of page number has about a million entries. At four bytes each that's around 4 MB per process, even for a tiny program. A two-level table splits the page number, say 10 bits for an outer table and 10 for inner tables, and inner tables only exist for regions actually in use. 64-bit systems use more levels for the same reason."
Thinking the TLB is a cache of data rather than of address translations.
FIFO: evict the page that has been in memory longest; simple but ignores use.
Optimal and LRU: optimal evicts the page used furthest in the future (a benchmark only); LRU evicts the least recently used page.
Belady's anomaly: with FIFO, adding frames can increase faults; LRU and optimal never do this.
"FIFO evicts whichever page came in first. It's cheap, but it can throw out a page that's used all the time just because it's old. Optimal evicts the page that won't be needed for the longest time in the future. It gives the fewest faults possible, but it needs to know the future, so we only use it as a yardstick. LRU uses the past as a guess for the future and evicts the page that hasn't been used for the longest time. It's usually close to optimal, but tracking exact recency on every memory access is too expensive, so real systems approximate it with a reference bit, like the clock algorithm. Belady's anomaly is the odd case where giving FIFO more frames causes more faults. With the reference string 1 2 3 4 1 2 5 1 2 3 4 5, FIFO takes 9 faults with three frames but 10 with four. LRU and optimal are stack algorithms, so more frames never hurt them."
Claiming LRU is easy to implement exactly in hardware, or that more memory always means fewer page faults.
Cause: the pages that running processes actively need add up to more than physical memory.
Symptom: constant page faults, the disk is busy, the CPU sits idle waiting, and little real work gets done.
Fix: reduce how many processes compete for memory, or add memory; working-set and fault-rate limits help the OS decide.
"Thrashing is when the system spends more time moving pages in and out than running programs. It happens when the working sets of the active processes, meaning the pages each one is actively using, add up to more than the RAM available. Each process faults, steals a frame from another process, and that process then faults to get its page back. The disk is flat out, and processes are mostly blocked waiting for it, so CPU usage drops. An old trap is a scheduler that sees low CPU use and admits more processes to use it, which makes the memory shortage worse. The way out is to lower the number of processes competing for memory, by suspending or swapping out some of them entirely so the rest have room for their working sets. The OS can track each process's working set or page-fault rate to decide. And of course, adding RAM or reducing memory use fixes it at the root."
Saying thrashing means the CPU is overloaded, when the CPU is actually mostly idle.
Problem: what was slow and how it showed up for users.
OS angle: which measurement pointed to memory, scheduling, I/O or system calls, and which tool showed it.
Result: the change you made, how you measured the improvement, and what you'd watch for next time.
"At my last company we had a batch service that got much slower after we doubled its worker threads, which surprised everyone. The CPU wasn't maxed out, so I looked at vmstat and saw two things. The context-switch count had shot up, and the machine had started swapping. Each worker built a large in-memory buffer, so doubling the workers pushed total memory past what the box had, and pages were being swapped in and out constantly. On top of that, far more threads than cores were fighting over the CPU. I cut the workers back to roughly the number of cores, and changed each one to stream its data in chunks instead of loading it all. Swapping stopped completely, and the job finished in less than half the old time. The lesson for me was that more threads is not more speed, and that I should check memory and switching before adding parallelism."
A story with no measurement, where the fix was a guess that happened to work.
Two modes: user mode can't run privileged instructions or touch hardware directly; kernel mode can.
Crossing: a system call uses a special trap instruction that switches to kernel mode at a fixed entry point.
Return: the kernel checks arguments, does the work, puts the result in a register and drops back to user mode.
"The two modes exist for protection. In user mode, a program can't execute privileged instructions, can't talk to devices directly and can't touch memory it doesn't own. Only the kernel runs in kernel mode. So if one program is buggy or malicious, it can't take down the machine or read another program's memory. When a program needs something only the kernel can do, like reading a file, it makes a system call. Usually it calls a library wrapper, which puts the system call number and arguments in registers and runs a special trap instruction. The CPU switches to kernel mode and jumps to a fixed kernel entry point, so user code can't choose where in the kernel it lands. The kernel looks up the handler for that number, checks the arguments, like whether the buffer really belongs to the process, does the work, and returns the result. Then the CPU switches back to user mode and the program carries on."
Saying a system call is just a normal library function call, with no mode switch.
Interrupt: asynchronous, raised by hardware like a disk, network card or timer, unrelated to the current instruction.
Trap or exception: synchronous, caused by the instruction running now, like a divide by zero, a page fault or a system call.
Handling: save state, look up the handler in the interrupt vector table, run it in kernel mode, defer heavy work, resume.
"A hardware interrupt comes from outside the running program. A network card says a packet arrived, a disk says a read finished, or the timer fires. It can arrive between any two instructions, so it's asynchronous. A trap or exception is caused by the instruction the CPU is running right now, like dividing by zero, touching an unmapped page or deliberately executing a system call instruction. That makes it synchronous and repeatable. When an interrupt arrives, the CPU finishes the current instruction, saves the minimum state it needs, switches to kernel mode and uses the interrupt number to find the handler in the interrupt vector table. The handler should be short because other interrupts may be held off while it runs. So it usually just acknowledges the device, grabs the data and schedules the heavier processing to run later, outside interrupt context. Then the kernel restores state, and it may pick a different process to run on the way out."
Calling a page fault or divide by zero a hardware interrupt from a device.
What load means: on Linux it counts runnable tasks plus tasks in uninterruptible sleep, usually waiting on disk or network storage.
Checks: look for processes in D state, I/O wait and disk stats, and swap activity.
Likely causes: a slow or failing disk, a hung network mount, or heavy swapping; fix the cause, not the load number.
"First, I wouldn't assume the CPU is the problem. On Linux the load average counts tasks that are runnable and also tasks in uninterruptible sleep, the D state, which almost always means they're stuck waiting on I/O. So high load with idle CPUs usually means lots of processes are waiting on storage. I'd list processes with their state and look for many in D, and see what they're waiting on. Then I'd check vmstat for the I/O wait column and for swap activity, and iostat to see if a disk is saturated or showing very long response times. Common causes are a failing or overloaded disk, a network file system mount that has stopped responding, or the box swapping because it's short of memory. The fix depends on which one it is: stop the job hammering the disk, fix or remount the storage, or free up memory. Restarting the app might help briefly, but the load would come right back."
Concluding the box needs more CPUs because the load number is high.
Inode: holds a file's metadata and pointers to its data blocks, but not its name.
Hard link: another directory entry pointing to the same inode; the data lives until the link count reaches zero.
Symbolic link: a small separate file holding a path; it can cross file systems and can dangle.
"In Unix-style file systems, an inode is the record for a file. It stores the owner, permissions, size, timestamps and pointers to the data blocks. What it doesn't store is the file's name. Names live in directories, which are just tables mapping names to inode numbers. A hard link is simply a second name pointing at the same inode. Both names are equal; there's no original. The inode keeps a link count, and the data is only freed when that count drops to zero and no process still has the file open. Because a hard link is an inode number, it can't point to another file system, and normally you can't hard link a directory. A symbolic link is different: it's its own small file whose content is a path. It can point anywhere, even across file systems, but if you delete or move the target, the symlink is left dangling and opening it fails."
Saying the file name is stored in the inode, or that deleting the original breaks a hard link.
Meaning: the process has hit its per-process limit on open file descriptors, which cover files, sockets and pipes.
Leak or load: watch the open-descriptor count over time; a steady climb that never falls back is a leak.
Fix: find what's left open and close it on every path; raise the limit only if real concurrent need is higher.
"That error means the process has used up its per-process limit on file descriptors. Descriptors aren't only files; every socket, pipe and connection uses one too. Raising the limit might be right, but only if the service genuinely needs that many open at once. The fact it fails after a couple of days makes me suspect a leak. So I'd check the current limit with ulimit or the process limits file, then count its open descriptors, for example by listing its entries under /proc or with lsof, and watch that number over a few hours. If it climbs steadily and never falls, it's a leak. lsof also shows what they are, so I can see whether it's log files, sockets to one backend, or something else, and that points me to the code. The fix is making sure every open is closed on every path, including errors. Raising the limit alone just moves the crash further out."
Raising the limit to a huge number without ever checking whether descriptors are leaking.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.