CoreHRx Jobs

Apply in one click, save jobs and take free mock tests.

Register free Log in
Hiring? Post jobs with CoreHRx
My account
user

View & update profile
Interview guide

Operating Systems Interview Questions and Answers: 60 Questions from Easy to Hard

This guide is for students and freshers in India preparing for the operating systems round of campus placements and technical interviews, and for developers with a few years of experience who face OS questions for backend, systems or embedded roles. It has 60 questions with answers in three levels. Easy covers processes, threads, system calls, scheduling basics and memory terms. Medium covers the numericals interviewers love, such as SJF, round robin, the Banker's algorithm, page replacement and disk scheduling, along with synchronization and fork(). Hard covers virtual memory internals, page tables, locks, file systems and interrupts. Every number and program output has been checked by running it. Answer each question aloud before reading the answer, solve the numericals on paper, then take the mock test.

What OS interviews cover

  • Processes and threads: process states, the PCB, context switching, fork(), exec(), zombies and orphans, and how threads differ from processes.
  • CPU scheduling: FCFS, SJF, SRTF, round robin and priority scheduling, usually as a numerical with a Gantt chart and the average waiting or turnaround time.
  • Synchronization and deadlocks: race conditions, mutexes and semaphores, producer-consumer and dining philosophers, the four deadlock conditions and the Banker's algorithm.
  • Memory management: paging, segmentation, the TLB, virtual memory, page faults, thrashing, and page replacement with FIFO, LRU and Optimal.
  • Storage and I/O: inodes and links, journaling, disk scheduling, interrupts and DMA.

How to prepare (freshers and experienced)

  • Freshers: learn each definition in your own words, then solve at least five numericals each on scheduling, page replacement, the Banker's algorithm and disk scheduling by hand, drawing the Gantt chart or frame table every time.
  • Write small C programs on Linux with fork(), exec(), waitpid() and pthreads, and run them. Seeing a zombie in ps or a lost update in a shared counter teaches more than any definition.
  • Experienced developers: connect each concept to problems you have seen in production, such as high load, out-of-memory kills, swapping, thread-pool sizing, deadlocked database transactions and slow disk I/O.
  • Read the man pages for fork, waitpid, pthread_mutex_lock, sem_wait and mmap. Interviewers for systems roles often ask about the exact behaviour they describe.

Common mistakes and interview-day tips

  • Mixing up turnaround time (completion minus arrival) and waiting time (turnaround minus burst), or ignoring arrival times in a scheduling numerical.
  • Saying that a mutex and a binary semaphore are the same thing, or that a cycle in a resource-allocation graph always means deadlock.
  • State your assumptions before solving: how ties are broken, whether C-SCAN counts the return jump, and whether the frames start empty. Interviewers value clear reasoning more than one convention.
  • When asked for an example, give a concrete one with numbers or a two-thread interleaving instead of repeating a definition. It shows you understand the mechanism.

Operating Systems interview questions: Easy questions (freshers)

20 questions

1. What is an operating system, and what are its main jobs?

An operating system is the software layer that manages the hardware and gives programs a simpler, safer machine to run on. Its main jobs are process management (creating, scheduling and ending programs), memory management (giving each process its own address space), file systems, device and I/O management through drivers, and protection between users and processes. It is both a resource manager that shares the CPU, RAM and disks fairly, and an abstraction layer: an app calls open() instead of driving a disk controller itself.

2. What is the difference between kernel mode and user mode, and why do CPUs have both?

The CPU runs at one of two privilege levels. In kernel mode code can run every instruction and reach all memory and devices; in user mode, privileged instructions such as halting the CPU, loading page tables or disabling interrupts are blocked. Applications run in user mode, so a buggy program cannot crash the machine or read another process's memory. If user code tries a privileged instruction, the CPU raises an exception and the kernel usually ends the process; on Linux x86-64, running hlt in a normal program kills it with SIGSEGV.

3. What is a system call? How is printf() different from write()?

A system call is the controlled way a user program asks the kernel for a service it cannot perform itself, such as reading a file, creating a process or sending data on a socket. The program puts a call number and arguments in registers and runs a special instruction; the CPU switches to kernel mode, the kernel does the work and returns. write() is a thin C library wrapper around the write system call. printf() formats text into a user-space buffer and calls write() only when the buffer is flushed, so ten printf calls can become one system call.

Example
#include <string.h>
#include <unistd.h>

int main(void) {
    const char *msg = "Namaste from write()\n";
    /* fd 1 is standard output: no formatting, no buffering */
    if (write(1, msg, strlen(msg)) < 0)
        return 1;
    return 0;
}

4. What is the difference between a program and a process? What does a process look like in memory?

A program is a passive file on disk, such as /usr/bin/python3. A process is a program in execution, with its own address space, program counter, registers, open files and state. One program can run as many processes at once, like three terminals each running bash. In memory a process has the text segment (machine code), the data and BSS segments (initialised and zero-initialised globals), the heap, which grows upward as malloc() asks for memory, and the stack, which holds function frames and grows downward.

5. What are the states of a process, and what moves a process from one state to another?

The classic states are new, ready, running, waiting (blocked) and terminated. A new process is admitted to the ready queue, and the scheduler dispatches a ready process to the CPU. A running process goes back to ready when its time slice ends or it is preempted, moves to waiting when it starts I/O or waits for an event such as a lock, and returns to ready when that event completes. A process never goes straight from waiting to running. On Linux, ps shows R (running or runnable), S and D (sleeping) and Z (zombie).

6. What is a Process Control Block (PCB), and what does it store?

The PCB is the kernel's record of one process, the structure it uses to manage, schedule and resume it. It stores the process ID and parent ID, the state, the saved program counter and CPU registers, scheduling data such as priority and CPU time used, memory data such as a pointer to the page tables, the list of open files, and accounting and security data such as the owner. During a context switch the kernel saves the running task's registers into its PCB and loads the next one's. In Linux the PCB is struct task_struct.

7. What is the difference between a process and a thread?

A process is a unit of resource ownership: it has its own address space, open files and permissions. A thread is a unit of execution inside a process. Threads of one process share the code, globals, heap and open files, but each has its own stack, registers, program counter and thread-local storage. So creating and switching threads is cheaper and sharing data is easy, but one thread can corrupt memory the others use, and a crash in one thread kills the whole process. Browsers run tabs as separate processes so one bad page cannot take down the rest.

8. What is a context switch, and why is it considered overhead?

A context switch is the kernel stopping one task and resuming another on the same CPU. It saves the current task's registers, program counter and stack pointer into its PCB, picks the next task, loads that task's saved state and, if the new task is in another process, switches the address space. A timer interrupt, a blocking call such as read() or a higher-priority task waking up can trigger it. It is overhead because no useful work happens during the switch, and afterwards the caches and TLB are cold for the new task.

9. What is the difference between preemptive and non-preemptive scheduling, and which criteria are used to compare schedulers?

In non-preemptive scheduling a process keeps the CPU until it finishes or blocks; in preemptive scheduling the OS can take the CPU away, usually on a timer interrupt or when a higher-priority process arrives. Modern desktop and server OSes are preemptive. Schedulers are compared by CPU utilisation, throughput (jobs finished per unit time), turnaround time (completion minus arrival), waiting time (time spent in the ready queue, which is turnaround minus burst for a CPU-only job) and response time (first run minus arrival).

10. Three jobs arrive at time 0 with CPU bursts of 20, 4 and 2 ms. What is the average waiting time under FCFS, and what is the convoy effect?

FCFS runs them in arrival order P1, P2, P3. P1 waits 0, P2 waits 20 and P3 waits 24, so the average waiting time is 44 / 3 = 14.67 ms. If the two short jobs ran first (P3, P2, P1), the waits would be 0, 2 and 6, an average of only 2.67 ms. That is the convoy effect: short jobs queue behind one long CPU-bound job, like scooters stuck behind a truck on a single-lane road. FCFS is simple and never starves anyone, but its average waiting time depends heavily on arrival order.

Example
Process  Arrival  Burst (ms)
P1       0        20
P2       0        4
P3       0        2

11. What is a race condition? Explain it with counter++ run by two threads.

A race condition is a bug where the result depends on the timing of threads that share data without coordination. counter++ looks like one step but is three: load the value, add one, store it back. If the threads interleave as shown, both read 5 and both write 6, so one update is lost. Such code can pass a thousand tests and fail under load in production. The fix is to make the read-modify-write atomic, with a mutex or an atomic increment. In C an unsynchronised data race is undefined behaviour, so even the size of the error is unpredictable.

Example
counter = 5              (shared, no lock)

Thread A: load counter into a register   -> 5
Thread B: load counter into a register   -> 5
Thread A: add 1, store counter           -> 6
Thread B: add 1, store counter           -> 6

Two increments ran, but counter is 6, not 7.

12. What is the critical section problem, and what must a correct solution guarantee?

A critical section is code that touches shared data, and the problem is making threads take turns in it. Each thread asks permission in an entry section, runs the critical section and leaves through an exit section. A correct solution guarantees mutual exclusion (at most one thread inside at a time), progress (if nobody is inside, a thread that wants to enter is not kept out by threads that do not) and bounded waiting (no thread is overtaken an unlimited number of times). Peterson's algorithm solves it for two threads on paper; real code uses mutexes built on atomic instructions.

13. What is the difference between a mutex and a semaphore?

A mutex is a lock with an owner: only one thread holds it, and the thread that locked it must unlock it. It protects a critical section. A semaphore is a counter with two atomic operations: wait (P) decrements it and blocks if it would go below zero, and signal (V) increments it and wakes a waiter. Any thread may signal. A semaphore set to 5 can cap a pool of five database connections; one starting at 0 lets a thread announce an event to another. A binary semaphore has no owner, so it cannot do priority inheritance or catch an unlock by the wrong thread.

14. What is a deadlock, and what are the four conditions that must hold for one?

A deadlock is a set of processes each waiting for a resource that another member of the set holds, so none can ever proceed. In a Bengaluru e-commerce backend, thread A locks the orders row and waits for the payments row while thread B holds payments and waits for orders. The four Coffman conditions are mutual exclusion (the resource cannot be shared), hold and wait (holding one resource while requesting another), no preemption (a resource cannot be taken away) and circular wait (a cycle of waiters). All four must hold, so breaking any one prevents deadlock.

15. What is the difference between internal and external fragmentation?

Internal fragmentation is space wasted inside an allocated block because memory is handed out in fixed sizes. With 4 KB pages, a process that needs 9 KB gets 3 pages, 12 KB, and 3 KB of the last page is unused. External fragmentation is free memory broken into small holes between allocations, so a request fails though the total free space is enough: five separate 2 KB holes, 10 KB in all, cannot satisfy one 8 KB contiguous request. Paging removes external fragmentation, because any free frame can hold any page, at the cost of some internal fragmentation.

16. How does paging work? For a 32-bit address with 4 KB pages, how is the address split?

Paging divides a process's logical address space into fixed-size pages and physical memory into frames of the same size. A per-process page table maps each page number to a frame number, so a process's pages can sit in any free frames. A 4 KB page needs 12 offset bits (2^12 = 4096), so a 32-bit address splits into a 20-bit page number and a 12-bit offset, allowing 2^20 = 1,048,576 pages. The MMU replaces the page number with the frame number and keeps the offset. For example, logical address 10,000 is page 2 at offset 1,808.

17. What is virtual memory, and what happens on a page fault?

Virtual memory gives each process a large private address space and keeps only the pages in use in RAM; the rest are on disk or not yet loaded. With demand paging a page is loaded only when first touched. Touching a page that is not present makes the MMU raise a page fault. The kernel checks the address is valid (if not, the process gets a segmentation fault), finds a free frame or evicts a victim page, reads the page from disk or fills it with zeros, updates the page table and restarts the instruction. The program only notices the delay.

18. What is an interrupt, and how is it different from polling?

An interrupt is a signal from hardware, such as a network card or a timer, that makes the CPU pause its current work, run a handler in the kernel and then resume. With polling, the CPU keeps reading a device status register in a loop, wasting cycles while the device is idle. Interrupts are efficient when events are rare, like keystrokes; polling can win when events are so frequent that per-interrupt overhead dominates, which is why fast network drivers switch to polling under heavy load. Traps such as system calls and exceptions such as divide by zero use the same entry mechanism.

19. What does fork() return, and what does this program print?

fork() creates a child process that is a copy of the parent and returns twice: 0 in the child and the child's PID in the parent, or -1 in the parent if it fails. Here the child prints its line and exits with status 7, while the parent blocks in waitpid() until the child ends and reads the code with WEXITSTATUS. So the output is always 'child: fork returned 0' and then 'parent: child exited with 7'. Without waitpid() the order of the two lines would not be guaranteed, because the scheduler may run either process first.

Example
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    }
    if (pid == 0) {
        printf("child: fork returned 0\n");
        return 7;
    }
    int status;
    waitpid(pid, &status, 0);
    printf("parent: child exited with %d\n", WEXITSTATUS(status));
    return 0;
}

20. What is the difference between a zombie process and an orphan process?

A zombie is a child that has finished but whose parent has not yet called wait() to collect its exit status. Its memory is freed, but its process-table entry and PID stay, and ps shows it with state Z and the word defunct. You cannot kill a zombie, even with kill -9, because it is already dead: the parent must reap it, or the parent must exit so that init adopts and reaps it. An orphan is a child still running when its parent exits; it is re-parented to init (PID 1) or a subreaper, which reaps it later. Thousands of zombies can exhaust PIDs.

Ready to check yourself? Take the free Operating Systems mock test

Operating Systems interview questions: Medium questions (1-3 years)

20 questions

21. Find the average waiting time for these processes under non-preemptive SJF and under SRTF.

SJF: only P1 is present at 0, so it runs 0-8. At 8 the shortest is P4 (8-9), then P3 (9-11), then P2 (11-15). Waits are P1 0, P2 10, P3 7, P4 4, an average of 5.25. SRTF: P1 runs 0-1, P2 preempts it (4 < 7), P3 preempts P2 at 2 (2 < 3) and runs 2-4, P4 runs 4-5, P2 finishes 5-8 and P1 8-15. Waits are 7, 3, 0 and 0, an average of 2.5, and average turnaround falls from 9 to 6.25. SRTF minimises average waiting time, but it needs burst estimates and can starve long jobs.

Example
Process  Arrival  Burst
P1       0        8
P2       1        4
P3       2        2
P4       4        1

22. Four processes arrive at time 0 with bursts of 7, 4, 9 and 2 ms. Schedule them with round robin, quantum 3 ms, and compare with FCFS.

Each process runs for 3 ms or until it finishes, then goes to the back of the queue, which gives the Gantt chart shown. Completion times are P1 19, P2 15, P3 22 and P4 11, so the waiting times are 12, 11, 13 and 9, an average of 11.25 ms, worse than 9.5 ms for FCFS. But the average response time (first run) is 4.5 ms against 9.5 ms, which is why RR suits interactive systems. There are 8 context switches. Too large a quantum turns RR into FCFS; too small wastes CPU on switching.

Example
Process  Burst    (all arrive at 0, quantum = 3)
P1       7
P2       4
P3       9
P4       2

| P1 | P2 | P3 | P4 | P1 | P2 | P3 | P1 | P3 |
0    3    6    9    11   14   15   18   19   22

23. What is starvation in priority scheduling, and how does aging fix it?

Priority scheduling always runs the highest-priority ready process, preemptively or not. If high-priority work keeps arriving, a low-priority process may never run: that is starvation, or indefinite blocking. Aging fixes it by raising a process's priority the longer it waits, for example one level for every few seconds in the ready queue, so even the lowest job eventually reaches the top. State the convention in an interview: in many textbooks and in Linux nice values a lower number means higher priority, while Windows uses higher numbers for higher priority.

24. How do you fix the counter race with a mutex? What does this program print?

It prints 2000000 every time. pthread_mutex_lock() lets only one thread into the counter++ critical section, so each read-modify-write completes before the other thread can read, and pthread_join() makes main wait for both threads before printing. Without the lock, increments can be lost and the total falls short. Locking a million times per thread is slow, so real code would use an atomic increment (atomic_fetch_add in C11) or let each thread count locally and add its total once under the lock. Keep critical sections short and never return without unlocking.

Example
#include <pthread.h>
#include <stdio.h>

static long counter = 0;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

static void *work(void *arg) {
    (void)arg;
    for (int i = 0; i < 1000000; i++) {
        pthread_mutex_lock(&lock);
        counter++;                     /* critical section */
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

int main(void) {
    pthread_t a, b;
    pthread_create(&a, NULL, work, NULL);
    pthread_create(&b, NULL, work, NULL);
    pthread_join(a, NULL);
    pthread_join(b, NULL);
    printf("%ld\n", counter);
    return 0;
}

25. How do you solve the producer-consumer (bounded buffer) problem with semaphores?

Use two counting semaphores and a mutex. empty starts at the buffer size and counts free slots; full starts at 0 and counts filled slots. The producer waits on empty before adding and posts full after; the consumer waits on full and posts empty. The mutex protects the buffer and its indexes. With one producer and one consumer this prints consumed 1 to consumed 6 in order. The classic bug is locking the mutex before waiting on empty: when the buffer is full the producer sleeps holding the mutex, the consumer can never take an item, and both are stuck.

Example
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>

#define SIZE 3
static int buf[SIZE], in = 0, out = 0;
static sem_t empty, full;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

static void *producer(void *arg) {
    (void)arg;
    for (int item = 1; item <= 6; item++) {
        sem_wait(&empty);              /* wait for a free slot */
        pthread_mutex_lock(&lock);
        buf[in] = item;
        in = (in + 1) % SIZE;
        pthread_mutex_unlock(&lock);
        sem_post(&full);               /* one more item ready */
    }
    return NULL;
}

static void *consumer(void *arg) {
    (void)arg;
    for (int i = 0; i < 6; i++) {
        sem_wait(&full);               /* wait for an item */
        pthread_mutex_lock(&lock);
        int item = buf[out];
        out = (out + 1) % SIZE;
        pthread_mutex_unlock(&lock);
        sem_post(&empty);              /* the slot is free again */
        printf("consumed %d\n", item);
    }
    return NULL;
}

int main(void) {
    pthread_t p, c;
    sem_init(&empty, 0, SIZE);
    sem_init(&full, 0, 0);
    pthread_create(&p, NULL, producer, NULL);
    pthread_create(&c, NULL, consumer, NULL);
    pthread_join(p, NULL);
    pthread_join(c, NULL);
    return 0;
}

26. What is the dining philosophers problem, why can the simple solution deadlock, and how do you fix it?

Five philosophers sit at a round table with one fork between each pair, and each needs both neighbouring forks to eat. If every philosopher picks up the left fork and then waits for the right, all five can hold one fork and wait forever, since all four deadlock conditions hold. Fixes break circular wait or hold and wait: number the forks and always take the lower-numbered one first; allow at most four philosophers at the table with a semaphore; or take both forks only when both are free, inside a monitor. Preventing deadlock does not by itself prevent starvation.

27. What are the four ways an operating system can handle deadlocks?

Prevention makes one of the four conditions impossible, for example a global lock order to remove circular wait, or requesting everything at once to remove hold and wait. Avoidance grants a request only if the system stays in a safe state, as the Banker's algorithm does, which needs each process's maximum demand in advance. Detection and recovery lets deadlocks happen, finds them periodically and then aborts or rolls back a victim, which is what databases do with transactions. Ignoring the problem is what Linux and Windows do for application locks, leaving it to the programmer.

28. Using the Banker's algorithm, is this state safe? If it is, give a safe sequence.

First compute Need = Max - Allocation: P0 (3,2,2), P1 (1,2,1), P2 (5,0,2), P3 (2,1,1), P4 (2,1,1). With Available (1,2,1) only P1 fits; it finishes and returns its allocation, so Work becomes (3,2,2). Then P0 fits, giving (4,3,2), then P3 (4,4,3), P4 (5,6,3) and finally P2 (8,6,5). So the state is safe, with safe sequence P1, P0, P3, P4, P2; other orders also work. If P0 now asked for (1,1,0), Available would drop to (0,1,1) and no Need would fit, so the request must wait even though the resources are free.

Example
        Allocation    Max         Available
        A  B  C       A  B  C     A  B  C
P0      1  1  0       4  3  2     1  2  1
P1      2  0  1       3  2  2
P2      3  0  2       8  0  4
P3      0  1  1       2  2  2
P4      1  2  0       3  3  1

29. Does a cycle in a resource-allocation graph always mean deadlock?

Only when every resource type has a single instance; then a cycle means deadlock. With multiple instances a cycle is necessary but not enough. Suppose R1 has two instances, held by P1 and P3, P2 holds R2, P1 waits for R2 and P2 waits for R1. The graph has the cycle P1 -> R2 -> P2 -> R1 -> P1, but P3 is waiting for nothing. When P3 finishes and frees its instance of R1, P2 gets it, finishes and releases R2, and then P1 runs. So there is no deadlock. With multiple instances, run a detection algorithm instead of looking for cycles.

30. With 4 KB pages and the page table below, what physical address does logical address 13,000 map to?

Divide by the page size: 13,000 / 4096 gives page 3, and the offset is 13,000 - 3 x 4096 = 712. The page table maps page 3 to frame 7, so the physical address is 7 x 4096 + 712 = 29,384. Hex makes this easy: 13,000 is 0x32C8, and with 4 KB pages the low three hex digits (12 bits) are the offset, so page 0x3 is replaced by frame 0x7, giving 0x72C8, which is 29,384. The offset never changes in translation; only the page number is swapped. A page with no valid entry causes a page fault.

Example
Page size = 4 KB (4096 bytes)

Page   Frame
0      5
1      9
2      2
3      7

31. What is a TLB, and how do you calculate the effective memory access time with one?

The TLB (translation lookaside buffer) is a small, fast cache of recent page-to-frame translations inside the MMU. Without it, every memory access would need an extra access to read the page table. With m = 80 ns, t = 5 ns and a 95% hit ratio, a hit costs 85 ns and a miss 165 ns, so EAT = 0.95 x 85 + 0.05 x 165 = 89 ns, against 160 ns with no TLB. At a 99% hit ratio it is 85.8 ns. TLBs work because programs have locality. With multi-level page tables a miss costs one access per level, so the hit ratio matters even more.

Example
EAT = h x (t + m) + (1 - h) x (t + 2m)

h = TLB hit ratio, t = TLB lookup time, m = memory access time
(single-level page table: a TLB miss costs one extra memory access)

32. How is segmentation different from paging? Translate these addresses with the segment table.

Segmentation splits a program into variable-sized logical units such as code, data and stack, each with a base and a limit. An address is (segment, offset): the hardware checks offset < limit, then adds the base. So (1, 2500) is valid and maps to 20000 + 2500 = 22,500, while (2, 950) traps because 950 is beyond segment 2's limit of 900. Segments match the program's structure and make per-segment protection easy, but variable sizes cause external fragmentation. Paging uses fixed sizes, so no external fragmentation. x86-64 keeps segmentation mostly flat and relies on paging.

Example
Segment  Base     Limit
0        8000     1200
1        20000    3000
2        15000    900

Translate (segment, offset): (1, 2500) and (2, 950)

33. For the reference string below with 3 frames, how many page faults do FIFO, LRU and Optimal give?

FIFO gives 9 faults, LRU 7 and Optimal 5. The first three references fault in every algorithm because the frames start empty. The difference shows at the sixth reference, page 4: FIFO evicts page 3 because it was loaded earliest, though it was just used, and faults again when 3 returns; LRU evicts page 2, the least recently used. Optimal evicts the page used farthest in the future, so it is the lower bound, but it needs future knowledge and is used only to judge other algorithms. Kernels approximate LRU because exact LRU on every access is too costly.

Example
Reference string: 4 3 2 3 1 4 1 3 4 2 1 4
Frames: 3 (empty at the start)

34. What is Belady's anomaly? Show it with an example.

Belady's anomaly is when giving a process more frames causes more page faults. It can happen with FIFO: on this reference string FIFO gives 9 faults with 3 frames but 10 faults with 4. The cause is that FIFO evicts by load order, not use, so the pages held with 4 frames are not always a superset of those held with 3. LRU and Optimal are stack algorithms: with n + 1 frames they always keep everything n frames would, so they never show the anomaly. On the same string LRU gives 9 faults with 3 frames and 8 with 4.

Example
Reference string: 6 1 6 4 2 6 1 3 6 1 5 2 3
FIFO, frames empty at the start: compare 3 frames with 4 frames

35. What is thrashing, how do you recognise it, and how do you fix it?

Thrashing is when the system spends more time paging than executing, because the pages the running processes actively use, their working sets, do not fit in RAM together. Each fault evicts a page another process needs soon, which then faults too. Signs are low useful CPU work, heavy disk or swap I/O and constant swap-in and swap-out in vmstat. Old schedulers made it worse by admitting more jobs when CPU use fell. Fixes: run fewer processes at once, add RAM, cap memory per container, or improve locality. The working-set model admits a process only if its working set fits.

Example
vmstat 1 5     # one line per second, five lines
# si / so = memory swapped in / out per second
# wa      = CPU time spent waiting for I/O

36. What is an inode, and what is the difference between a hard link and a symbolic link?

An inode is the record of a file in Unix file systems: type, permissions, owner, size, timestamps, link count and pointers to the data blocks. It does not hold the name; a directory is a list of name-to-inode entries. A hard link is a second name for the same inode, so the link count becomes 2, and the data stays until the count reaches 0 and no process has the file open. A symbolic link is a separate small file that stores a path. It can cross file systems and point to directories, but it dangles when the target is removed, as shown. Hard links cannot cross file systems.

Example
$ echo hi > a.txt
$ ln a.txt b.txt          # hard link: same inode
$ ln -s a.txt c.txt       # symbolic link: its own inode
$ stat -c '%n links=%h' a.txt b.txt
a.txt links=2
b.txt links=2
$ rm a.txt
$ cat b.txt
hi
$ cat c.txt
cat: c.txt: No such file or directory

37. The disk head is at cylinder 60 and moving toward higher numbers. Find the total head movement for FCFS, SSTF, SCAN and LOOK.

FCFS serves requests in arrival order: 820 cylinders. SSTF always goes to the nearest request: 40, 20, 10, then 95, 110, 130, 150 and 175, a total of 215, but a stream of nearby requests can starve far ones. SCAN goes up through 95 to 175, continues to the end at 199, then reverses to 40, 20 and 10: 328. LOOK turns at the last request, 175, instead of the disk end: 280. C-SCAN serves one direction only and jumps back to 0 for more uniform waits: 378 if you count the return jump, so state your convention. SSDs have no seek time.

Example
Head at cylinder 60, cylinders 0-199, moving toward 199
Queue (arrival order): 150 20 95 175 40 130 10 110

38. What is DMA, and why does it reduce CPU load?

Direct Memory Access lets a device controller move data between the device and RAM without the CPU copying each word. The driver gives the DMA engine a buffer address, a length and a direction, the device transfers the whole block over the bus, and it raises one interrupt when it is done. Without DMA, programmed I/O would make the CPU move every word, which is hopeless for a fast disk or a 10 Gbps network card. The CPU is free during the transfer, though it shares memory bandwidth. Drivers must handle cache coherence, and an IOMMU limits which memory a device can reach.

39. How many times does this program print hello, and how many child processes does it create?

It prints hello 8 times and creates 7 child processes. Each fork() doubles the number of processes: 2 after the first call, 4 after the second and 8 after the third, and each of the 8 runs the printf. In general n forks in sequence give 2^n processes and 2^n - 1 children. A child continues the loop from the value of i it inherited, which is why the count doubles instead of growing by one. The order of the lines is not fixed, because the scheduler decides who runs first. A related trap: text still in a stdio buffer before fork() is copied too and can print twice.

Example
#include <stdio.h>
#include <unistd.h>

int main(void) {
    for (int i = 0; i < 3; i++)
        fork();
    printf("hello\n");
    return 0;
}

40. How does a shell run a command? Explain fork, exec and wait with this program.

A shell forks a child, the child calls exec to replace its program image with the command, and the parent waits. Here the child runs echo, so the output is 'report.csv ready' and then 'child exit code 0'. exec does not create a process: the PID stays the same, and a successful exec never returns, so the perror line runs only if exec fails, for example on a missing command, and 127 follows the shell convention. Keeping fork and exec separate lets the child prepare in between, such as redirecting output with dup2(), which is how the shell implements > and pipes.

Example
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    }
    if (pid == 0) {
        execlp("echo", "echo", "report.csv ready", (char *)NULL);
        perror("execlp");          /* reached only if exec failed */
        _exit(127);
    }
    int status;
    waitpid(pid, &status, 0);
    printf("child exit code %d\n", WEXITSTATUS(status));
    return 0;
}

Ready to check yourself? Take the free Operating Systems mock test

Operating Systems interview questions: Hard questions (experienced)

20 questions

41. What exactly happens when a program makes a system call on x86-64 Linux?

The libc wrapper puts the call number in rax (write is 1 and getpid is 39 on x86-64) and up to six arguments in rdi, rsi, rdx, r10, r8 and r9, then runs the syscall instruction. The CPU switches to ring 0 and jumps to the kernel entry address stored in a model-specific register. The kernel saves user registers, indexes its system call table with rax, runs the handler and returns with the result in rax. A negative result is an error: the wrapper stores it in errno and returns -1. Calls such as clock_gettime usually go through the vDSO and never enter the kernel.

Example
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>

int main(void) {
    long raw = syscall(SYS_getpid);    /* raw system call */
    pid_t lib = getpid();              /* libc wrapper */
    printf("%s, SYS_write = %d\n",
           raw == lib ? "same pid" : "different", SYS_write);
    return 0;
}

42. Compare monolithic kernels and microkernels. Why is Linux called monolithic?

A monolithic kernel runs the scheduler, memory manager, file systems, network stack and drivers in one address space in kernel mode, so subsystems call each other directly: fast, but a bug in any driver can crash or compromise the system. A microkernel keeps only address spaces, threads, IPC and basic scheduling in the kernel and runs drivers and file systems as user-space servers, gaining fault isolation and a small core (seL4 is formally verified) at the cost of IPC on every request. Linux is monolithic but modular, loading drivers as modules at runtime; Windows NT and macOS XNU are hybrids.

43. What does a context switch really cost, and how do you reduce the number of switches?

The direct cost is saving and restoring registers, running the scheduler and, between processes, loading a new page-table base; this is on the order of microseconds. The larger indirect cost comes afterwards: the new task finds the caches and TLB full of the old task's data. Switching address spaces used to flush the TLB; process-context IDs (PCID on x86) tag entries so they survive. To reduce switches, size thread pools near the core count for CPU-bound work, use asynchronous I/O instead of thousands of blocking threads, and batch work. vmstat shows switches per second in the cs column.

44. What are user-level threads and kernel-level threads, and which threading models map one to the other?

Kernel-level threads are scheduled by the OS: a blocking call blocks only that thread, and threads run in parallel on several cores, but creating and switching them involves the kernel. User-level threads are managed by a library: switches are very cheap, but in the many-to-one model one blocking system call stalls them all and they cannot use more than one core. Linux (NPTL) and Windows use one-to-one; on Linux each thread is a task created by clone() with shared-memory flags. Many-to-many maps many user threads onto a pool of kernel threads, as Go goroutines and Java 21 virtual threads do.

45. How does a multilevel feedback queue scheduler work, and how does Linux schedule normal tasks?

An MLFQ has several ready queues of decreasing priority, often with longer quanta lower down. New jobs start at the top; a job that uses its whole quantum moves down, while one that blocks early stays high, so interactive jobs respond fast without the OS knowing burst times. Two fixes are needed: a periodic boost of every job to the top to stop starvation, and charging total CPU time per level so a program cannot stay high by yielding just before its quantum ends. Linux uses a different design for normal tasks: CFS from 2.6.23, picking the least virtual runtime, replaced by EEVDF in 6.6.

46. What is priority inversion, and how does priority inheritance solve it?

Priority inversion happens when a high-priority task H waits for a lock held by a low-priority task L, and a medium-priority task M, which needs no lock, preempts L. H now effectively waits for M. This caused the Mars Pathfinder resets in 1997. With priority inheritance, while L holds a lock that H wants, L runs at H's priority, so M cannot preempt it; L finishes its critical section, releases the lock and drops back. The priority ceiling protocol instead raises any lock holder to a preset ceiling. POSIX offers both, as PTHREAD_PRIO_INHERIT and PTHREAD_PRIO_PROTECT.

47. When is a spinlock better than a blocking mutex, and how does a Linux mutex avoid system calls?

A spinlock busy-waits until the lock is free; a blocking mutex puts the waiter to sleep. Spinning wins only when the lock is held for less time than two context switches and the holder is running on another core. On a single core spinning is pure waste, because the holder cannot run while you spin. The kernel uses spinlocks for short sections and in interrupt handlers, which cannot sleep. In user space, glibc mutexes are built on futexes: an uncontended lock and unlock is an atomic instruction in user space, and only under contention does a thread call futex() to sleep in the kernel.

48. Why cannot a lock be built from a plain flag variable, and how do atomic instructions fix it? What does this program print?

With a plain flag, 'while (flag) wait; flag = 1;' has a gap between the check and the set: two threads can both see 0 and both enter. Test-and-set closes the gap by reading the old value and writing 1 in one indivisible instruction, so only the thread that saw 0 gets in; compare-and-swap is the general form. The acquire and release orderings also stop the compiler and CPU from moving counter++ outside the lock. The program prints 2000000. This spinlock is correct but naive: under contention every waiter hammers one cache line, so real locks spin on a plain read, back off or sleep.

Example
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>

static atomic_flag busy = ATOMIC_FLAG_INIT;
static long counter = 0;

static void spin_lock(void) {
    /* set the flag and get its old value in one atomic step */
    while (atomic_flag_test_and_set_explicit(&busy,
                                             memory_order_acquire))
        ;                              /* spin */
}

static void spin_unlock(void) {
    atomic_flag_clear_explicit(&busy, memory_order_release);
}

static void *work(void *arg) {
    (void)arg;
    for (int i = 0; i < 1000000; i++) {
        spin_lock();
        counter++;
        spin_unlock();
    }
    return NULL;
}

int main(void) {
    pthread_t a, b;
    pthread_create(&a, NULL, work, NULL);
    pthread_create(&b, NULL, work, NULL);
    pthread_join(a, NULL);
    pthread_join(b, NULL);
    printf("%ld\n", counter);
    return 0;
}

49. Why must you wait on a condition variable inside a while loop and not an if?

pthread_cond_wait() atomically releases the mutex and sleeps, then re-acquires the mutex before returning, but returning does not prove the condition is true. POSIX allows spurious wakeups, and with several workers another thread may grab the mutex first and take the job, so an if would let a worker continue with jobs at 0. The while loop re-checks the predicate every time. The program prints 'worker took a job' and then 'jobs left 0', whether main signals before or after the worker starts waiting, because the worker checks jobs before it sleeps. Change shared state only under the mutex.

Example
#include <pthread.h>
#include <stdio.h>

static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
static int jobs = 0;

static void *worker(void *arg) {
    (void)arg;
    pthread_mutex_lock(&m);
    while (jobs == 0)                  /* re-check after every wakeup */
        pthread_cond_wait(&cv, &m);
    jobs--;
    pthread_mutex_unlock(&m);
    printf("worker took a job\n");
    return NULL;
}

int main(void) {
    pthread_t t;
    pthread_create(&t, NULL, worker, NULL);
    pthread_mutex_lock(&m);
    jobs++;                            /* change state under the lock */
    pthread_cond_signal(&cv);
    pthread_mutex_unlock(&m);
    pthread_join(t, NULL);
    printf("jobs left %d\n", jobs);
    return 0;
}

50. In the readers-writers problem, why can writers starve, and how do real systems handle read-mostly data?

Readers can share the data, but a writer needs it alone. In the first readers-writers solution a writer waits until the reader count reaches zero, so a steady stream of overlapping readers can keep it out forever. A writer-preferring lock blocks new readers once a writer waits, which can starve readers; fair locks queue both in order. glibc's pthread_rwlock_t prefers readers by default unless a non-portable attribute selects writers. For read-mostly data the Linux kernel uses RCU, where readers take no lock and writers publish a new copy, and seqlocks, where readers retry.

51. Two threads transfer money between the same two accounts in opposite directions. How do you make it deadlock-free?

If transfer() locked from and then to, the thread moving money from account 0 to 1 could hold lock 0 and wait for 1 while the other thread holds 1 and waits for 0: a circular wait. A global lock order, always the lower account id first, removes circular wait, so it cannot deadlock. This sketch of a Bengaluru fintech wallet ledger prints 800000 1200000 total 2000000, and the total never changes. Alternatives are pthread_mutex_trylock with back-off (watch for livelock), one coarser lock, or in a database, locking rows in key order and letting its deadlock detector abort a victim.

Example
#include <pthread.h>
#include <stdio.h>

typedef struct {
    int id;
    long balance;                      /* in rupees */
    pthread_mutex_t m;
} Account;

static Account acc[2] = {
    {0, 1000000, PTHREAD_MUTEX_INITIALIZER},
    {1, 1000000, PTHREAD_MUTEX_INITIALIZER},
};

static void transfer(Account *from, Account *to, long amount) {
    /* always lock the lower id first: no circular wait */
    Account *first = from->id < to->id ? from : to;
    Account *second = from->id < to->id ? to : from;
    pthread_mutex_lock(&first->m);
    pthread_mutex_lock(&second->m);
    from->balance -= amount;
    to->balance += amount;
    pthread_mutex_unlock(&second->m);
    pthread_mutex_unlock(&first->m);
}

static void *zero_to_one(void *arg) {
    (void)arg;
    for (int i = 0; i < 100000; i++)
        transfer(&acc[0], &acc[1], 5);
    return NULL;
}

static void *one_to_zero(void *arg) {
    (void)arg;
    for (int i = 0; i < 100000; i++)
        transfer(&acc[1], &acc[0], 3);
    return NULL;
}

int main(void) {
    pthread_t x, y;
    pthread_create(&x, NULL, zero_to_one, NULL);
    pthread_create(&y, NULL, one_to_zero, NULL);
    pthread_join(x, NULL);
    pthread_join(y, NULL);
    printf("%ld %ld total %ld\n", acc[0].balance, acc[1].balance,
           acc[0].balance + acc[1].balance);
    return 0;
}

52. Why do systems use multi-level page tables? Work out the sizes for 32-bit and for x86-64.

A flat page table needs an entry for every possible page, used or not. For 32-bit addresses with 4 KB pages that is 2^20 entries, 4 MB per process, so 100 processes need 400 MB for page tables alone. A two-level table splits the page number 10/10: the 4 KB outer table points to inner tables that exist only for regions in use, so a small process with code at the bottom and stack at the top needs about 12 KB. On x86-64 a flat table for 48 bits would need 2^36 eight-byte entries, 512 GB, so it uses four levels. The cost is a longer page walk on every TLB miss.

Example
32-bit addresses, 4 KB pages, 4-byte entries:
  one level:  2^20 entries x 4 B = 4 MB per process
  two levels: 10 | 10 | 12 bits, each table 1024 x 4 B = 4 KB

x86-64, 48-bit virtual addresses, 4 KB pages, 8-byte entries:
  four levels: 9 | 9 | 9 | 9 | 12 bits, each table 512 x 8 B = 4 KB

53. What is TLB reach, and why do huge pages help large databases and JVM heaps?

TLB reach is the memory the TLB can map at once: number of entries times page size. A TLB with 64 entries covers only 64 x 4 KB = 256 KB, so a program touching a 30 GB heap at random misses constantly and pays a page walk each time. With 2 MB pages the same 64 entries cover 128 MB, and the page tables shrink too. Linux offers hugetlbfs pages, reserved up front, and transparent huge pages (THP), which the kernel builds automatically. The trade-offs are internal fragmentation, memory bloat and latency spikes while the kernel compacts memory, so some databases advise turning THP off.

54. How does the page-fault rate affect effective access time, and how low must it be?

A fault here costs 75,000 times a memory access, so even a tiny fault rate dominates. With p = 0.001, one fault per thousand accesses, EAT = 0.999 x 80 + 0.001 x 6,000,000 = 6,079.92 ns, about 76 times slower than RAM. To keep the slowdown under 5%, EAT must stay below 84 ns, so p < 4 / 5,999,920, about one fault per 1.5 million accesses. That is why locality matters, why heavy swapping feels like a hang and why faster swap devices only reduce the pain. Minor faults, served without disk I/O, cost far less than major faults.

Example
EAT = (1 - p) x m + p x s

m = memory access time       = 80 ns
s = page-fault service time  = 6 ms = 6,000,000 ns
p = page-fault rate

55. How do real kernels approximate LRU? Explain the clock (second-chance) algorithm.

Exact LRU would need a timestamp or list update on every memory access, which is too costly. Instead the MMU sets a reference bit in the page table entry when a page is used. The clock algorithm keeps frames in a circle with a hand. On a fault the hand checks the frame it points to: if the reference bit is 1, it clears the bit and moves on, giving it a second chance; if it is 0, that page is the victim. The enhanced version also checks the dirty bit and prefers clean, unreferenced pages, which need no disk write. Linux uses active and inactive lists, and 6.1 added an optional multi-gen LRU.

56. How does copy-on-write make fork() cheap, and when is fork() still expensive?

After fork() the kernel does not copy the parent's memory. Parent and child share the physical pages, marked read-only in both page tables. When either writes to a page, the CPU raises a protection fault and the kernel copies just that page and maps the copy writable for the writer. So fork plus exec copies almost nothing. fork is still costly for a process with a large address space, because its page tables are copied, and if the parent keeps writing while a child snapshots 20 GB of data, memory use can approach double. vfork and posix_spawn avoid this when the child only calls exec.

57. How should a server process reap its children so that zombies never pile up?

Handle SIGCHLD and, in the handler, call waitpid(-1, ..., WNOHANG) in a loop until it stops returning a PID. The loop matters because a pending signal is not queued twice: if five children exit close together, the parent may get one SIGCHLD, and one waitpid per signal would leave zombies. WNOHANG keeps the handler from blocking, and the handler saves errno. The program blocks SIGCHLD before forking and waits with sigsuspend(), so no exit is missed, and prints reaped 5 children. If exit codes are never needed, setting SIGCHLD to SIG_IGN makes Linux reap children automatically.

Example
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

static volatile sig_atomic_t reaped = 0;

static void on_child(int sig) {
    (void)sig;
    int saved = errno;
    while (waitpid(-1, NULL, WNOHANG) > 0)
        reaped++;                      /* one signal, many exits */
    errno = saved;
}

int main(void) {
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = on_child;
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    sigaction(SIGCHLD, &sa, NULL);

    sigset_t block, old;
    sigemptyset(&block);
    sigaddset(&block, SIGCHLD);
    sigprocmask(SIG_BLOCK, &block, &old);
    for (int i = 0; i < 5; i++)
        if (fork() == 0)
            _exit(0);
    while (reaped < 5)
        sigsuspend(&old);              /* sleep, SIGCHLD unblocked */
    printf("reaped %d children\n", (int)reaped);
    return 0;
}

58. How does a classic Unix inode address a file's data blocks, and what is the maximum file size?

A 4 KB block holds 1024 four-byte block numbers. The 12 direct pointers cover 48 KB, so small files need no extra reads. The single indirect block adds 4 MB, the double indirect 4 GB and the triple indirect 4 TB, a maximum of 4,402,345,721,856 bytes, just over 4 TB. The byte at offset 1 GB falls in the double-indirect range, so with only the inode cached it costs two pointer-block reads plus the data block. The tree keeps small files cheap and large ones possible. ext4 uses extents by default instead, each mapping a run of contiguous blocks, which shrinks metadata for big files.

Example
Inode block pointers (4 KB blocks, 4-byte block numbers):
  12 direct             -> 12 blocks
  1 single indirect     -> 1024 blocks
  1 double indirect     -> 1024 x 1024 blocks
  1 triple indirect     -> 1024 x 1024 x 1024 blocks

59. How do journaling file systems survive a crash, and how should an application save a file safely?

Updating a file touches several blocks: data, inode, block bitmap and directory. A crash in between leaves the file system inconsistent, needing a full fsck scan. A journaling file system first writes the changes to a journal, then a commit record, and only then writes them in place. After a crash it replays committed transactions and discards incomplete ones. ext4 by default journals only metadata, in ordered mode, writing data blocks before the metadata that points to them. To replace a file safely, write a temp file, fsync it, rename() it over the old one, then fsync the directory.

60. How does Linux keep interrupt handlers fast, and what happens at a very high interrupt rate?

A hardware interrupt handler runs with interrupts disabled on that CPU, so it must be short. Linux splits the work: the top half acknowledges the device and grabs urgent data, then schedules a bottom half, a softirq or tasklet that runs soon with interrupts enabled, or a workqueue or threaded IRQ that runs in process context and may sleep. At millions of packets a second, an interrupt per packet would cause livelock, so NAPI masks the interrupt and polls the device in batches until the queue is empty. Cards also coalesce interrupts, and /proc/interrupts shows per-CPU counts.

Practice questions for November 2026

New every month

3 questions from the Operating Systems mock test, the way they appear in it. A new set comes on the 1st of every month. Pick your answer, then open it.

  1. EasyCPU Scheduling

    All processes arrive at time 0. Using non-preemptive priority scheduling where a lower number means higher priority, what is the average waiting time?

    Process  Arrival  Burst  Priority
    P1       0        4      3
    P2       0        2      1
    P3       0        6      4
    P4       0        3      2
    • 4
    • 5
    • 7.75
    • 5.5
    Show the answer

    Answer: 4. Order by priority number: P2 0-2, P4 2-5, P1 5-9, P3 9-15. Waiting times: P1 5, P2 0, P3 9, P4 2. Sum 16, average 4 ms. A higher number here means lower priority.

  2. MediumDisk Scheduling

    Compared with SCAN, why does C-SCAN give a more uniform waiting time?

    • It serves in one direction only, so both edges of the disk wait about as long
    • It serves the nearest request first, so every request waits the least
    • It always moves less in total than SCAN for every possible request queue
    • It processes requests in arrival order, so the waits are equal by definition
    Show the answer

    Answer: It serves in one direction only, so both edges of the disk wait about as long. After SCAN reverses at one edge, requests just behind the head get served again soon while those at the far edge have waited a full sweep. C-SCAN jumps back to the start and sweeps the same way again, treating the disk as a circle.

  3. HardVirtual Memory

    With 3 frames, initially empty, how many more page faults does LRU have than Optimal on this string?

    6 1 4 6 2 1 7 4 6 1 2 7 3 6
    • 5
    • 7
    • 6
    • 4
    Show the answer

    Answer: 5. LRU: F F F - F F F F F F F F F F = 13 faults. Optimal: F F F - F - F - F - F - F - = 8 faults, 5 fewer. Here each page returns just after LRU evicted it, while Optimal looks ahead and keeps the pages needed soonest.

Take the full Operating Systems mock test

Ready to apply?

There are 17,463 open IT jobs on CoreHRx right now. Create a free profile once and apply with it.