Operating Systems
Processes, scheduling, sync, deadlock, memory, files and disks — in simple words, with worked numbers for GATE-style questions. Print this page for a PDF.
1. What an Operating System Really Is
An operating system (OS) is the program that runs the computer for everyone else. It sits between your apps and the hardware.
Your app never talks to the disk, the screen or the network card directly. It asks the OS, and the OS does it safely.
Think of a hotel manager. Guests (apps) don't walk into the kitchen or the boiler room. They ask the front desk. The manager shares rooms, staff and hot water fairly, and stops one guest from barging into another's room.
The OS has two big jobs
- Resource manager — it shares the CPU, memory, disk and devices between many programs, fairly and safely.
- Extended machine — it hides messy hardware behind simple ideas. You say "open file notes.txt"; you never say "read sector 88123 from platter 2".
What it manages, one line each
| Area | What the OS does |
|---|---|
| Processes | Starts, pauses, switches and stops running programs |
| CPU | Decides which program runs next, and for how long (scheduling) |
| Memory | Gives each program its own space and stops them reading each other's |
| Files | Turns raw disk blocks into named files and folders |
| Devices (I/O) | Talks to keyboards, disks and network cards through drivers |
| Security | Users, passwords, permissions — who may do what |
Kinds of OS you may be asked to name
- Batch — jobs are collected and run one after another, no user interaction. Old mainframes.
- Multiprogramming — several programs sit in memory. When one waits for disk, the CPU runs another. Goal: keep the CPU busy.
- Time-sharing (multitasking) — switch between programs so fast that every user feels the computer is theirs. Goal: quick response.
- Real-time (RTOS) — must respond within a fixed deadline. Car airbags, pacemakers. Hard real-time: missing a deadline is a failure. Soft: missing it is just bad (video drops a frame).
- Distributed — many machines work together and look like one system.
2. User Mode, Kernel Mode & System Calls
The kernel is the core of the OS — the part that is always in memory and has full control of the hardware.
The CPU itself has (at least) two modes. In kernel mode any instruction is allowed. In user mode the dangerous ones (talking to devices, changing memory maps) are blocked.
Your apps run in user mode. When they need something only the kernel can do, they make a system call — a formal request to the kernel.
A bank. Customers (user mode) can't walk into the vault. They fill in a slip at the counter (system call). The cashier (kernel) checks it, goes into the vault, and hands back the result.
What happens during a system call
- Your code calls a library function, e.g.
printf, which callswrite(). - The library puts the system call number and arguments in registers.
- It runs a special trap instruction (
syscallon x86-64). The CPU jumps into the kernel and switches to kernel mode. - The kernel checks the request (is this file yours?), does the work, and puts the result in a register.
- The CPU switches back to user mode and your code carries on.
Common system calls by group
| Group | Linux examples |
|---|---|
| Process control | fork, exec, exit, wait, kill |
| File handling | open, read, write, close, lseek |
| Device handling | ioctl, read, write |
| Information | getpid, time, uname |
| Communication | pipe, socket, shmget, mmap |
System calls are slow-ish compared to a normal function call, because of the mode switch and checks. That's why libraries buffer: printf collects text and makes one write() instead of one per character.
3. Kernel Designs
The question is: how much of the OS lives inside the kernel?
A monolithic kernel puts almost everything inside — fast, but one bad driver can crash it all. A microkernel keeps only the bare minimum inside and runs the rest as normal programs — safer, but slower because parts must keep sending messages to each other.
Monolithic is one big open-plan office: quick to shout across, but one fire spreads everywhere. Microkernel is separate small offices that pass notes: slower, but a fire stays in one room.
| Monolithic | Microkernel | Hybrid | |
|---|---|---|---|
| Inside the kernel | Nearly everything | Only IPC, scheduling, basic memory | Core plus speed-critical services |
| Speed | Fastest — plain function calls | Slower — message passing between parts | Close to monolithic |
| Reliability | A driver bug can crash the OS | A crashed driver can be restarted | In between |
| Size of trusted code | Millions of lines | Tiny — easier to verify | Large |
Linux is monolithic but modular. You can load and unload kernel modules (drivers) at runtime with modprobe, so you don't rebuild the kernel for a new device. They still run in kernel mode, though — a bad module can still crash it.
4. Booting Up
Booting is how a machine goes from "power on" to "login screen". Each stage is a small program whose only job is to load the next, bigger one.
Like a relay race: firmware hands the baton to the bootloader, the bootloader to the kernel, the kernel to the first user program.
- Power on → firmware. The CPU starts running code stored on a chip on the motherboard: BIOS (old) or UEFI (modern).
- POST (Power-On Self-Test). Firmware checks RAM, CPU and devices are present and working.
- Find the boot device. Firmware looks at disks in a set order. BIOS reads the first sector (the MBR, 512 bytes); UEFI reads a bootloader file from a special EFI partition.
- Bootloader (e.g. GRUB) runs. It lets you pick an OS, then loads the kernel and a small temporary file system (
initramfs) into memory. - Kernel starts. It sets up memory management, detects hardware, loads drivers and mounts the real root file system.
- First process. The kernel starts
init— today usually systemd — as process ID 1. It starts every other service: networking, logging, the login screen.
5. Programs, Processes & the PCB
A program is a file on disk — just instructions sitting there, doing nothing.
A process is a program that is running. It has memory, open files, a current position in the code, and a state.
Open Chrome twice and you get one program, two processes.
A recipe in a book is a program. You actually cooking it — with your own pans, ingredients and "I'm on step 4" — is a process. Two people can cook the same recipe at once.
What a process looks like in memory
The PCB — the process's ID card
The OS keeps one Process Control Block (PCB) for every process. It's a record of everything needed to pause the process and later resume it exactly where it left off.
| Field | What it holds |
|---|---|
| PID | Process ID — a unique number |
| State | New, ready, running, waiting, terminated |
| Program counter | Address of the next instruction to run |
| CPU registers | Saved values of the registers when it was paused |
| Scheduling info | Priority, time used, which queue it is in |
| Memory info | Pointer to its page table, memory limits |
| Open files | List of files and sockets it has open |
| Parent / children | Who created it, whom it created |
In Linux the PCB is a C struct called task_struct. All of them are linked into the kernel's process table.
6. Process States
A process doesn't run non-stop. It moves between a few states: waiting for its turn on the CPU, actually running, or stuck waiting for something slow like the disk.
A queue at a single ATM. You join (new), wait in line (ready), use the machine (running). If the machine asks for your card PIN and you go hunting for it, you step aside (waiting) and rejoin the line when ready.
| State | Meaning |
|---|---|
| New | Being created — PCB set up, memory being given |
| Ready | Has everything it needs except the CPU. Sitting in the ready queue |
| Running | Its instructions are executing on a CPU right now |
| Waiting (Blocked) | Can't continue until something happens — disk read, key press, a lock |
| Terminated | Finished. The OS cleans up once the parent collects its exit status |
Suspended states (the 7-state model)
If RAM gets full, the OS can move a whole process out to disk — this is called swapping out. That adds two more states: Ready-Suspended and Blocked-Suspended. Same meaning as before, but the process is on disk instead of in RAM.
The three schedulers
- Long-term scheduler (job scheduler) — decides which new jobs are let into memory. Controls the degree of multiprogramming (how many processes are in memory).
- Short-term scheduler (CPU scheduler) — picks the next ready process to run. Runs very often, every few milliseconds, so it must be fast.
- Medium-term scheduler — swaps processes out to disk and back in, to free memory.
7. Threads
A thread is one path of execution inside a process. A process can have several threads running at once.
Threads of the same process share the code, global data, heap and open files. Each thread has its own stack, registers and program counter.
A process is a kitchen. Threads are cooks in that kitchen. They share the fridge, the stove and the recipe book, but each cook has their own hands and knows which step they're on.
Process vs thread
| Process | Thread | |
|---|---|---|
| Memory | Own separate address space | Shares the process's memory |
| Creation cost | Heavy — new memory map, PCB | Light — just a stack and registers |
| Context switch | Slower — memory map changes, TLB flushed | Faster — same memory map |
| Talking to others | Needs IPC (pipes, sockets…) | Just read/write shared variables |
| If one crashes | Others are unaffected | Usually the whole process dies |
| Example | Each Chrome tab is a process | A game's render thread and audio thread |
User-level vs kernel-level threads
- User-level threads are managed by a library inside your program. The kernel doesn't know they exist — it sees one process. Switching is very fast (no system call). But if one thread makes a blocking system call, the whole process blocks, and they can't run on two cores at once.
- Kernel-level threads are known to and scheduled by the OS. One can block while others run, and they can use many cores. Creating and switching them costs a bit more.
Multithreading models (how user threads map to kernel threads)
| Model | How | Catch |
|---|---|---|
| Many-to-one | Many user threads on one kernel thread | One blocking call blocks all; no true parallelism |
| One-to-one | Each user thread gets its own kernel thread | Too many threads strains the kernel. Used by Linux and Windows |
| Many-to-many | M user threads spread over N kernel threads | Flexible but complex. Go's goroutines work like this |
8. Context Switching
A context switch is the CPU putting one process down and picking another up.
The OS saves the current process's registers and program counter into its PCB, then loads the next process's saved values from its PCB.
Like putting a bookmark in one book before picking up another. The bookmark (PCB) lets you continue from the exact line later.
Process A running
│ timer interrupt / system call / I/O wait
▼
Kernel: save A's registers + PC → PCB(A)
A.state = ready (or waiting)
scheduler picks B
load B's registers + PC ← PCB(B)
switch memory map to B's
▼
Process B running
Why it matters
- It's pure overhead. While switching, no useful work is done. Typically a few microseconds.
- The hidden cost is bigger: the new process finds the CPU caches and the TLB (section 23) full of the old process's data, so it runs slowly for a while.
- Switching between threads of the same process is cheaper — the memory map stays the same.
What triggers a switch
- A timer interrupt — the process used up its time slice.
- The process makes a blocking system call (reads a file, waits on a lock).
- A higher-priority process becomes ready (in preemptive scheduling).
- The process exits.
9. fork, exec, wait — Zombies & Orphans
In Unix, new processes are made in two steps. fork() makes a near-exact copy of the current process. exec() then replaces that copy's program with a new one.
The parent can call wait() to pause until a child finishes and collect its exit code.
fork is photocopying yourself. exec is the copy then changing clothes and walking off to a different job. wait is the original waiting at the door for the copy to come home and report.
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork(); // ONE call, returns TWICE
if (pid < 0) {
perror("fork failed");
} else if (pid == 0) { // child sees 0
execlp("ls", "ls", "-l", NULL); // child becomes "ls"
perror("exec failed"); // only reached if exec fails
} else { // parent sees the child's PID
int status;
wait(&status); // block until child exits
printf("child done\n");
}
}
Key facts about fork
- It returns 0 in the child, the child's PID in the parent, and -1 on failure.
- The child gets a copy of the parent's memory, open files and registers. After fork, changes in one don't affect the other.
- Copying all memory would be slow, so Linux uses copy-on-write: both share the same pages until one of them writes, and only then is that page copied.
The classic counting question
fork();
fork();
fork();
printf("hi\n");
Each fork doubles the number of processes. After 3 forks: 2³ = 8 processes, so "hi" prints 8 times. Child processes created = 2ⁿ − 1 = 7. This exact question appears in GATE and campus tests.
Zombie and orphan
| Zombie | Orphan | |
|---|---|---|
| What | Child has finished, but the parent hasn't called wait() yet | Parent has finished, but the child is still running |
| Why it exists | The OS keeps its PCB entry so the parent can read the exit code | Parent crashed or exited early |
| What happens | Sits in the process table as <defunct> (state Z) | Adopted by init/systemd (PID 1), which waits for it |
| Harmful? | One is harmless. Thousands fill the process table and new processes can't start | Usually harmless |
| Fix | Parent must call wait() or handle SIGCHLD | Nothing needed |
kill -9 does nothing. You fix it by making the parent call wait(), or by killing the parent so init adopts and reaps the zombie.10. Scheduling Basics
Many processes are ready to run, but a core can run only one at a time. CPU scheduling is the rule the OS uses to pick who goes next.
The part that actually hands the CPU over is the dispatcher. The time it takes is the dispatch latency.
A doctor's clinic with one doctor. Do you see patients in arrival order? Quickest cases first? Emergencies first? Ten minutes each, then back of the line? Each rule is a scheduling algorithm, and each is unfair to someone.
Preemptive vs non-preemptive
- Non-preemptive — once a process gets the CPU, it keeps it until it finishes or waits for I/O. Simple, but one long job blocks everyone.
- Preemptive — the OS can take the CPU away mid-run (timer, or a more important process arrives). All modern desktop and server OSes do this.
The words every numerical uses
| Term | Meaning | Formula |
|---|---|---|
| Arrival time (AT) | When the process enters the ready queue | given |
| Burst time (BT) | CPU time it needs | given |
| Completion time (CT) | When it finishes | read from the Gantt chart |
| Turnaround time (TAT) | Total time from arriving to finishing | TAT = CT − AT |
| Waiting time (WT) | Time spent sitting in the ready queue | WT = TAT − BT |
| Response time (RT) | Time until it first gets the CPU | RT = first run − AT |
What a good scheduler aims for
- Maximise: CPU utilisation (keep it busy) and throughput (jobs finished per second).
- Minimise: turnaround time, waiting time and response time.
- Be fair: nobody waits forever (no starvation).
11. FCFS, SJF & SRTF
FCFS — First Come, First Served
Run processes in the order they arrive, each to the end. A plain queue. Non-preemptive.
A railway ticket counter. Fair in a sense — but if the person ahead is booking tickets for 40 relatives, everyone waits.
- Easy to write, no starvation.
- Suffers from the convoy effect: one long CPU-bound job makes many short jobs wait behind it, so the average waiting time is high.
SJF — Shortest Job First
When the CPU is free, pick the ready process with the smallest burst time. Non-preemptive version.
SJF gives the lowest possible average waiting time of any algorithm. That's provable and a common one-line answer.
- Catch 1: the OS can't know the future burst time. Real systems guess it from past bursts using an average that weights recent bursts more (exponential averaging:
τ(n+1) = α·t(n) + (1−α)·τ(n)). - Catch 2: long jobs can starve if short ones keep arriving.
SRTF — Shortest Remaining Time First
The preemptive version of SJF. When a new process arrives, compare its burst with the remaining time of the running one. If the newcomer is shorter, switch to it. Even better average waiting time than SJF, at the cost of more switches.
12. Priority, Round Robin & Multilevel Queues
Priority scheduling
Each process gets a priority number. The highest priority runs first. Can be preemptive or not.
SJF is actually a special case: priority = shortest burst.
- Problem — starvation: a low-priority process may wait forever if high-priority ones keep coming. (Legend says when MIT shut down an old IBM 7094 in 1973, they found a low-priority job submitted in 1967 still waiting.)
- Fix — aging: slowly raise the priority of a process the longer it waits. Eventually it becomes important enough to run.
- Watch the convention: in many textbooks and in Linux
nicevalues, a smaller number means higher priority. The question will say — read it.
Round Robin (RR)
Each process gets a fixed slice of time called the time quantum (say 4 ms). If it isn't finished when the slice ends, it goes to the back of the ready queue. Preemptive FCFS.
Kids sharing one swing: everyone gets 2 minutes, then back of the line. Nobody waits too long, but a lot of time goes on getting on and off.
- Designed for time-sharing. Gives good response time; no starvation.
- Quantum too large → it behaves like FCFS.
- Quantum too small → most time is wasted on context switches.
- Rule of thumb: about 80% of CPU bursts should finish within one quantum. Typical values: 10–100 ms.
- With
nprocesses and quantumq, no process waits more than(n − 1) × qbefore its next turn.
Multilevel queue
Split the ready queue into several queues by type — e.g. system, interactive, batch. Each queue has its own algorithm (RR for interactive, FCFS for batch). The queues themselves have fixed priority: batch runs only when the higher queues are empty. A process stays in its queue forever, so low queues can starve.
Multilevel feedback queue (MLFQ)
Like multilevel queue, but processes move between queues based on how they behave:
Queue 0 (highest) RR, quantum 8 ms ← every new process starts here
Queue 1 RR, quantum 16 ms ← used its full 8 ms? move down
Queue 2 (lowest) FCFS ← still CPU-hungry? move down again
Gives up the CPU early (I/O) → stays high (it's interactive)
Waits too long in a low queue → moved up (aging, prevents starvation)
MLFQ learns which processes are interactive without being told. It's the most general scheduler and the basis of real ones. Linux today uses CFS / EEVDF, which tracks how much CPU time each process has had and always runs the one that has had the least, weighted by priority.
All algorithms at a glance
| Algorithm | Preemptive? | Starvation? | Good at | Weakness |
|---|---|---|---|---|
| FCFS | No | No | Simplicity | Convoy effect |
| SJF | No | Yes | Lowest avg waiting (non-preemptive) | Needs burst time in advance |
| SRTF | Yes | Yes | Lowest avg waiting overall | Many switches; needs burst time |
| Priority | Either | Yes (fix: aging) | Important work first | Starvation |
| Round Robin | Yes | No | Response time, fairness | Higher avg waiting; quantum tuning |
| MLFQ | Yes | No (with aging) | Adapts to behaviour | Many settings to tune |
13. Worked Example — Gantt Charts
A Gantt chart is a timeline showing which process ran when. Draw it first, read off completion times, then use the formulas. That's the whole method for every scheduling numerical.
Four processes. Times in milliseconds.
| Process | Arrival (AT) | Burst (BT) |
|---|---|---|
| P1 | 0 | 8 |
| P2 | 1 | 4 |
| P3 | 2 | 9 |
| P4 | 3 | 5 |
FCFS
| P1 | P2 | P3 | P4 |
0 8 12 21 26
CT TAT = CT−AT WT = TAT−BT
P1 8 8 − 0 = 8 8 − 8 = 0
P2 12 12 − 1 = 11 11 − 4 = 7
P3 21 21 − 2 = 19 19 − 9 = 10
P4 26 26 − 3 = 23 23 − 5 = 18
total 61 total 35
Average TAT = 61 / 4 = 15.25 Average WT = 35 / 4 = 8.75
SJF (non-preemptive)
At time 0 only P1 is here, so it runs to 8. At 8, P2 (4), P3 (9) and P4 (5) are all waiting. Shortest first: P2, then P4, then P3.
| P1 | P2 | P4 | P3 |
0 8 12 17 26
CT TAT WT
P1 8 8 0
P2 12 11 7
P3 26 24 15
P4 17 14 9
Average TAT = 57 / 4 = 14.25 Average WT = 31 / 4 = 7.75
SRTF (preemptive SJF) and Round Robin (quantum = 4)
SRTF step by step. At 0, P1 starts (needs 8). At 1, P2 arrives needing 4 — less than P1's remaining 7, so P2 takes over. At 2 and 3, P3 (9) and P4 (5) arrive, but P2's remaining time is smaller, so it keeps running to 5. At 5 the remaining times are P1 = 7, P3 = 9, P4 = 5 → P4 runs to 10. Then P1 (7) to 17, then P3 (9) to 26.
SRTF CT TAT WT
P1 17 17 9
P2 5 4 0
P3 26 24 15
P4 10 7 2
Average TAT = 52 / 4 = 13.00 Average WT = 26 / 4 = 6.50
Round Robin step by step. P1 runs 0–4 (4 left). By then P2, P3, P4 have arrived and joined the queue in order; P1 goes behind them. Queue: P2, P3, P4, P1. P2 runs 4–8 and finishes. P3 runs 8–12 (5 left). P4 runs 12–16 (1 left). P1 runs 16–20 and finishes. P3 runs 20–24 (1 left). P4 runs 24–25 and finishes. P3 runs 25–26 and finishes.
RR q=4 CT TAT WT Response (first run − AT)
P1 20 20 12 0 − 0 = 0
P2 8 7 3 4 − 1 = 3
P3 26 24 15 8 − 2 = 6
P4 25 22 17 12 − 3 = 9
Average TAT = 73 / 4 = 18.25 Average WT = 47 / 4 = 11.75
Average response = 18 / 4 = 4.5
| Algorithm | Avg turnaround | Avg waiting |
|---|---|---|
| FCFS | 15.25 | 8.75 |
| SJF | 14.25 | 7.75 |
| SRTF | 13.00 | 6.50 (best) |
| RR (q = 4) | 18.25 | 11.75 |
14. Inter-Process Communication (IPC)
Processes have separate memory on purpose, so they can't just read each other's variables. IPC is the set of tools the OS gives them to share data anyway.
There are two basic styles: shared memory (both look at the same piece of RAM) and message passing (send copies of data through the kernel).
Shared memory is a whiteboard in a shared room — fast, but people must take turns writing. Message passing is sending letters through the post office — slower, but tidy, and nobody scribbles over anyone else.
| Method | How it works | Use it for |
|---|---|---|
| Pipe | One-way byte stream between related processes (parent → child). ls | grep txt is a pipe | Chaining commands |
| Named pipe (FIFO) | A pipe with a file name, so unrelated processes can use it | Simple local hand-off |
| Message queue | The kernel keeps a list of messages; processes add and remove them | Structured messages, ordering |
| Shared memory | The same RAM pages are mapped into both processes | Fastest — big data, no copying |
| Signal | A tiny notification with just a number, e.g. SIGINT (Ctrl+C) | "Stop", "reload", "child exited" |
| Socket | Two-way endpoint. Works on the same machine or across the network | Client–server, anything networked |
int fd[2];
pipe(fd); // fd[0] = read end, fd[1] = write end
if (fork() == 0) { // child
close(fd[1]);
read(fd[0], buf, sizeof buf);
} else { // parent
close(fd[0]);
write(fd[1], "hello", 5);
}
Shared memory vs message passing
| Shared memory | Message passing | |
|---|---|---|
| Speed | Fastest — after setup, no kernel involved | Slower — every message goes through the kernel |
| Synchronisation | Your problem — you need locks or semaphores | Built in — the kernel orders messages |
| Across machines? | No | Yes (sockets) |
15. Race Conditions & the Critical Section
A race condition happens when two threads use the same data at the same time, and the result depends on who happens to go first. Sometimes it works, sometimes it doesn't.
The critical section is the piece of code that touches the shared data. The fix is to make sure only one thread at a time can be inside it.
Two people edit the same Google Sheet cell offline. Both read 100, both add 50, both save 150. The right answer was 200. One update was simply lost.
Why count++ isn't safe
It looks like one step, but the CPU does three:
count++ is really: 1. load count into register
2. add 1 to register
3. store register into count
count = 5. Two threads, interleaved badly:
T1: load → reg1 = 5
T2: load → reg2 = 5 ← reads before T1 has stored
T1: add → reg1 = 6
T2: add → reg2 = 6
T1: store → count = 6
T2: store → count = 6 ← should be 7. One increment lost.
Three rules every solution must satisfy
- Mutual exclusion — if one thread is inside the critical section, no other can be.
- Progress — if nobody is inside and some want to enter, one of them must be allowed in soon. Threads not interested can't block the decision.
- Bounded waiting — after a thread asks to enter, there's a limit on how many times others can go ahead of it. No one waits forever.
Every critical-section answer is checked against these three. In GATE, a lot of "which of these is correct" questions are really "which rule does this code break".
do {
entry section // ask permission
critical section // touch shared data
exit section // give up permission
remainder section // everything else
} while (true);
Peterson's solution (two threads, pure software)
bool flag[2] = {false, false}; // flag[i]: "I want to enter"
int turn; // whose turn it is to go first
// Code for thread i (the other thread is j = 1 - i)
flag[i] = true; // I want in
turn = j; // but you can go first
while (flag[j] && turn == j)
; // wait while you want in AND it's your turn
/* critical section */
flag[i] = false; // I'm done
It's polite: each thread says "I want in, but after you". If both want in at once, turn can hold only one value, so exactly one gets through. It satisfies all three rules.
flag and turn. Real systems use hardware atomic instructions instead.Hardware help: atomic instructions
An atomic operation happens in one unbreakable step — no other thread can see it half-done.
// test_and_set: return the old value and set it to true, in ONE step
bool lock = false;
while (test_and_set(&lock))
; // spin until the old value was false
/* critical section */
lock = false;
Others: compare-and-swap (CAS) — "if the value is still X, change it to Y" — and atomic increment. All locks are built on these.
A lock that waits in a loop like this is a spinlock. Spinning wastes CPU, so it's only good when the wait will be very short and you have several cores. This kind of waiting is called busy waiting.
16. Mutexes & Semaphores
A mutex (mutual exclusion lock) is a simple lock with one key. Lock it before the critical section, unlock it after. Only the thread that locked it should unlock it.
A semaphore is a counter with two safe operations: wait (take one, or sleep if none are left) and signal (give one back, waking a sleeper).
A mutex is the single key to a shop's restroom. A counting semaphore is a parking lot with a "spaces free: 3" sign — cars enter while the number is above zero and wait at the gate when it hits zero.
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&m);
count++; // safe now
pthread_mutex_unlock(&m);
Semaphore operations
wait(S) also called P(S), down(S) signal(S) also called V(S), up(S)
{ {
S = S - 1; S = S + 1;
if (S < 0) if (S <= 0)
put this thread to sleep wake one sleeping thread
on S's queue; }
}
Both run atomically. A negative S tells you how many threads are asleep on it.
The old textbook version is simpler but busy-waits: wait(S) { while (S <= 0); S--; }. The version above sleeps instead of spinning, so it doesn't waste CPU.
Two kinds
- Binary semaphore — value only 0 or 1. Works like a lock.
- Counting semaphore — any value. Starts at the number of identical resources (e.g. 5 database connections). Each user does
waitto take one andsignalto return it.
A classic GATE-type question
A counting semaphore starts at 7. Then 20 P operations and 15 V operations are done. Final value? 7 − 20 + 15 = 2. (Each P subtracts 1, each V adds 1.)
Mutex vs semaphore
| Mutex | Semaphore | |
|---|---|---|
| Is a… | Lock | Counter (signalling tool) |
| Ownership | Yes — only the locker unlocks | No — any thread can signal |
| Values | Locked / unlocked | 0 … N |
| Typical use | Protect a critical section | Limit access to N resources, or signal "data is ready" |
wait(A); wait(B); and T2 does wait(B); wait(A);, they can each grab one and wait forever for the other — a deadlock (section 19). Always take multiple locks in the same order everywhere.17. Monitors & Condition Variables
Semaphores are easy to misuse — swap one wait and signal and you get a deadlock or a race that's hard to find.
A monitor is a safer, higher-level tool: an object whose methods automatically run one at a time. The language handles the locking for you.
A condition variable lets a thread inside the monitor sleep until something becomes true ("buffer not empty") and be woken when another thread changes it.
A monitor is a room with one door and a guard who lets in one person at a time. A condition variable is a bench inside where you can sit and nap until someone taps you saying "your item is ready" — and while you nap, you let others in.
Condition variable operations
wait(cond)— release the monitor lock and sleep. When woken, take the lock back before continuing.signal(cond)— wake one sleeping thread (if none sleep, nothing happens — the signal is lost).broadcast(cond)— wake all sleeping threads.
// Java: every object is a monitor
class Counter {
private int value = 0;
public synchronized void increment() { value++; } // one thread at a time
}
// Waiting for a condition — always in a loop
synchronized (queue) {
while (queue.isEmpty()) // NOT "if" — re-check after waking
queue.wait();
item = queue.remove();
}
wait inside a while, never an if. When you wake up, another thread may already have taken the item, and threads can also wake for no reason at all (a "spurious wakeup"). Re-check the condition every time.Semaphore signal vs condition signal: a semaphore remembers — signal adds 1 even if nobody is waiting. A condition variable forgets — signal with no waiters does nothing. A favourite interview distinction.
18. Classic Synchronisation Problems
Three famous puzzles. Each one stands for a pattern you meet in real code. Know the setup and the semaphore solution for each.
1. Producer–consumer (bounded buffer)
Producers put items into a buffer of size N; consumers take them out. Producers must wait when it's full, consumers when it's empty. This is every job queue, print queue and message queue.
semaphore mutex = 1; // protects the buffer itself
semaphore empty = N; // free slots
semaphore full = 0; // filled slots
Producer: Consumer:
while (true) { while (true) {
item = produce(); wait(full); // any item?
wait(empty); // any space? wait(mutex);
wait(mutex); item = remove();
insert(item); signal(mutex);
signal(mutex); signal(empty); // one more free slot
signal(full); // one more item consume(item);
} }
wait(empty) and wait(mutex). If the buffer is full, the producer grabs the mutex and then sleeps waiting for space. The consumer can't get the mutex to make space. Deadlock. Wait on the counting semaphore first, then the mutex.2. Readers–writers
Many threads may read shared data at once, but a writer needs it all to itself. Think of a database row or a config file.
semaphore rw_mutex = 1; // writers (and the first reader) take this
semaphore mutex = 1; // protects read_count
int read_count = 0;
Writer: Reader:
wait(rw_mutex); wait(mutex);
/* write */ read_count++;
signal(rw_mutex); if (read_count == 1) wait(rw_mutex); // first reader locks writers out
signal(mutex);
/* read */
wait(mutex);
read_count--;
if (read_count == 0) signal(rw_mutex); // last reader lets writers in
signal(mutex);
This is the "readers-preference" version: as long as readers keep arriving, a writer can starve. A writers-preference version fixes that but can starve readers. Real code uses a read-write lock (pthread_rwlock_t, Java's ReentrantReadWriteLock).
3. Dining philosophers
Five philosophers sit round a table with one chopstick between each pair. To eat, a philosopher needs both neighbouring chopsticks. It models processes needing several shared resources at once.
semaphore chopstick[5] = {1,1,1,1,1};
philosopher(i):
wait(chopstick[i]); // left
wait(chopstick[(i + 1) % 5]); // right
eat();
signal(chopstick[i]);
signal(chopstick[(i + 1) % 5]);
think();
Problem: if all five pick up their left chopstick at the same moment, everyone waits forever for the right one — deadlock.
Fixes:
- Allow only 4 philosophers at the table at once (a counting semaphore set to 4).
- Asymmetric: odd-numbered philosophers pick left first, even-numbered pick right first. This breaks the circle.
- Pick up both or neither — check both inside a critical section.
- Number the chopsticks and always pick the lower number first (resource ordering).
19. Deadlock & Its Four Conditions
A deadlock is when a group of processes are all stuck, each waiting for something another one in the group is holding. Nobody can move, ever.
Two cars meet on a narrow one-lane bridge from opposite ends. Each needs the other to reverse. Neither will. They sit there forever.
The four Coffman conditions — all four must hold
- Mutual exclusion — a resource can be used by only one process at a time (a printer, a lock).
- Hold and wait — a process holds at least one resource while waiting for another.
- No preemption — you can't forcibly take a resource away; the holder must give it up.
- Circular wait — P1 waits for P2, P2 waits for P3, … and the last waits for P1. A closed loop.
These are necessary conditions: if even one is missing, deadlock is impossible. That's the key to preventing it — break any one.
Resource allocation graph (RAG)
A picture of who holds what and who wants what. Circles are processes, squares are resource types, and dots inside a square are instances (e.g. 2 printers = 2 dots).
Reading a RAG
- No cycle → no deadlock. Always.
- Cycle, and every resource has one instance → deadlock. Certain.
- Cycle, but some resources have several instances → maybe deadlock. Another process outside the cycle might release an instance and break it.
Four ways to handle deadlock
| Strategy | Idea |
|---|---|
| Prevention | Design the system so one of the four conditions can never hold |
| Avoidance | Check each request in advance; only grant it if the system stays safe (Banker's) |
| Detection & recovery | Let it happen, notice it, then fix it |
| Ignore it | "Ostrich algorithm" — assume it's rare and reboot if it happens. What Windows and Linux do for most resources |
20. Prevention, Avoidance & Banker's Algorithm
Prevention — break one condition
| Condition | How to break it | Downside |
|---|---|---|
| Mutual exclusion | Make resources shareable (read-only files) | Impossible for printers, locks |
| Hold and wait | Ask for everything at the start, or release all before asking again | Poor resource use; starvation |
| No preemption | If you can't get what you want, release what you hold | Only works for things you can save and restore (CPU, memory) |
| Circular wait | Number every resource; always request in increasing order | Need to know the order — but this is the most practical one |
Lock ordering in code is exactly the circular-wait fix: if every thread takes lockA before lockB, a loop can't form.
Avoidance — the safe-state idea
Each process declares up front the maximum it might ever need. Before granting any request, the OS asks: "If I say yes, is there still some order in which every process can get its maximum and finish?"
If yes, the state is safe and the request is granted. If no, the process waits — even though the resource is free right now.
A bank manager with limited cash. Every customer has a credit limit. She only lends money if, afterwards, she could still satisfy every customer's full limit in some order as they repay. That's literally why it's called the Banker's algorithm.
- Safe state → no deadlock is possible.
- Unsafe state → deadlock may happen (not must). Unsafe ≠ deadlocked.
Banker's algorithm — the data
Available[j] = free instances of resource j right now
Max[i][j] = most of resource j process i will ever need
Allocation[i][j] = what process i holds now
Need[i][j] = Max[i][j] − Allocation[i][j] ← compute this first
Worked example
5 processes, 3 resource types. Total instances: A = 10, B = 5, C = 7.
| Process | Allocation (A B C) | Max (A B C) | Need = Max − Alloc |
|---|---|---|---|
| P0 | 0 1 0 | 7 5 3 | 7 4 3 |
| P1 | 2 0 0 | 3 2 2 | 1 2 2 |
| P2 | 3 0 2 | 9 0 2 | 6 0 0 |
| P3 | 2 1 1 | 2 2 2 | 0 1 1 |
| P4 | 0 0 2 | 4 3 3 | 4 3 1 |
Total allocated = (7, 2, 5). So Available = (10, 5, 7) − (7, 2, 5) = (3, 3, 2).
Safety check: start with Work = Available. Repeatedly find a process whose Need ≤ Work. Let it "finish" and return its Allocation to Work.
Work = (3,3,2)
P0 need (7,4,3) ≤ (3,3,2)? no — skip for now
P1 need (1,2,2) ≤ (3,3,2)? YES → Work = (3,3,2) + (2,0,0) = (5,3,2)
P2 need (6,0,0) ≤ (5,3,2)? no
P3 need (0,1,1) ≤ (5,3,2)? YES → Work = (5,3,2) + (2,1,1) = (7,4,3)
P4 need (4,3,1) ≤ (7,4,3)? YES → Work = (7,4,3) + (0,0,2) = (7,4,5)
P0 need (7,4,3) ≤ (7,4,5)? YES → Work = (7,4,5) + (0,1,0) = (7,5,5)
P2 need (6,0,0) ≤ (7,5,5)? YES → Work = (7,5,5) + (3,0,2) = (10,5,7)
All finished → SAFE. Safe sequence: <P1, P3, P4, P0, P2>
Sanity check: the final Work (10, 5, 7) equals the total — every instance came back. If your final Work doesn't match the total, you've made an arithmetic slip.
Should a new request be granted?
P1 asks for (1, 0, 2).
- Is it ≤ P1's Need (1, 2, 2)? Yes.
- Is it ≤ Available (3, 3, 2)? Yes.
- Pretend to grant it: Available = (2, 3, 0), P1 Allocation = (3, 0, 2), P1 Need = (0, 2, 0).
- Run the safety check on that pretend state: P1 (0,2,0) ≤ (2,3,0) → Work (5,3,2); P3 → (7,4,3); P4 → (7,4,5); P0 → (7,5,5); P2 → (10,5,7). Safe.
- So the request is granted. If it had been unsafe, P1 would wait and the pretend state would be thrown away.
Why it isn't used much in real OSes: processes rarely know their maximum needs, the number of processes changes all the time, and running the check on every request is slow (about m × n² steps for m resource types and n processes).
21. Detection, Recovery, Livelock & Starvation
Detection
- One instance per resource: simplify the RAG into a wait-for graph (just processes; an edge P1 → P2 means "P1 waits for P2"). Any cycle = deadlock.
- Several instances: run a Banker's-style check using current requests instead of maximum needs. Processes that can't finish are deadlocked.
- How often to check? Every request is expensive. Common choice: every so often, or when CPU use suddenly drops.
Recovery
- Kill processes — kill all deadlocked ones (simple, wasteful), or kill one at a time until the cycle breaks (pick the cheapest victim: lowest priority, least work done).
- Preempt resources — take a resource from one process and give it to another. The victim must be rolled back to a safe earlier point, often restarted.
- Make sure the same process isn't picked as victim every time, or it starves.
Databases do this all the time: they detect a deadlock between two transactions and abort one, which your app sees as an error to retry.
Deadlock vs livelock vs starvation
| What's happening | Everyday picture | |
|---|---|---|
| Deadlock | Processes are blocked, doing nothing, waiting on each other | Two cars stuck nose to nose on a bridge |
| Livelock | Processes keep running and changing state, but make no progress | Two people in a corridor both step left, then both step right, forever |
| Starvation | One process never gets a resource because others are always chosen first | A low-priority patient who keeps getting bumped by emergencies |
Deadlock always involves a cycle of waiting. Starvation doesn't — the system as a whole is making progress, just not for one unlucky process. Aging fixes starvation; random back-off (wait a random time before retrying) fixes livelock.
22. Memory Basics & Fragmentation
Every program thinks it has memory starting at address 0. That's its logical (or virtual) address. The real location in RAM chip is the physical address.
A piece of hardware in the CPU, the MMU (Memory Management Unit), translates logical addresses to physical ones on every single memory access.
Room numbers in a hotel. Your key card says "room 12" (logical). The hotel knows room 12 is on floor 3, east wing (physical). You never need to know the building's layout.
Address binding — when is the real address decided?
| When | How | Can the program move in RAM? |
|---|---|---|
| Compile time | Compiler writes real addresses into the code | No — must load at that exact spot |
| Load time | Addresses fixed when the program is loaded | Only by reloading it |
| Execution time | MMU translates on every access | Yes — used by all modern OSes |
The simplest MMU: base and limit registers
physical = base + logical if logical < limit
else → trap: "segmentation fault"
base = 14000, limit = 3000
logical 346 → 14000 + 346 = 14346 ✓
logical 3500 → 3500 ≥ 3000 → trap ✗ (outside its memory)
The limit check is what stops one process reading another's memory. Only the kernel can change base and limit.
Contiguous allocation
The oldest approach: each process gets one continuous block of RAM. As processes come and go, free memory becomes scattered gaps called holes. Where do you put a new process?
- First fit — the first hole big enough. Fast.
- Best fit — the smallest hole big enough. Leaves tiny useless leftovers.
- Worst fit — the biggest hole. Leaves a big, still-useful leftover — in theory.
Classic example. Holes (in order): 100, 500, 200, 300, 600 KB. Processes arrive: 212, 417, 112, 426 KB.
| Process | First fit | Best fit | Worst fit |
|---|---|---|---|
| 212 | 500 → 288 left | 300 → 88 left | 600 → 388 left |
| 417 | 600 → 183 left | 500 → 83 left | 500 → 83 left |
| 112 | 288 → 176 left | 200 → 88 left | 388 → 276 left |
| 426 | must wait — no hole ≥ 426 | 600 → 174 left | must wait |
Here best fit placed all four. In practice, first fit and best fit do about equally well, and both beat worst fit. First fit is usually preferred because it's faster.
Fragmentation — wasted memory
| Internal fragmentation | External fragmentation | |
|---|---|---|
| What | Wasted space inside a block you were given | Enough free memory in total, but split into gaps too small to use |
| Example | Need 18 KB, get a fixed 20 KB block → 2 KB wasted | Three free gaps of 50 KB; a 120 KB process can't fit |
| Caused by | Fixed-size blocks (fixed partitions, paging) | Variable-size blocks (contiguous allocation, segmentation) |
| Fix | Smaller blocks | Compaction (slide everything together — slow), or paging |
Picture it: internal is a 5-seater car carrying 3 people — 2 seats wasted inside. External: a car park with five single empty spaces scattered about, but you're towing a trailer and need two together.
23. Paging & the TLB
Paging cuts a process's logical memory into equal-size pieces called pages, and physical RAM into same-size slots called frames. Any page can go in any free frame.
Because pages don't have to sit next to each other, external fragmentation disappears. Each process has a page table that records which frame holds each page.
A book whose pages are stored in whatever pigeonholes are free, plus an index card saying "page 1 → hole 7, page 2 → hole 3…". You can still read it in order by following the card.
Splitting an address
A logical address is split into a page number (p) and an offset (d) — how far into the page. With page size 2ⁿ, the lowest n bits are the offset.
page number p = logical address ÷ page size (integer division)
offset d = logical address mod page size
physical = frame number × page size + d
The numbers in the diagram. Page size 1 KB = 1024 bytes. Logical address 2500 → p = 2500 ÷ 1024 = 2, d = 2500 − 2048 = 452. Page table says page 2 is in frame 5. Physical = 5 × 1024 + 452 = 5572.
Counting bits (very common in GATE)
32-bit logical address, 4 KB pages (4 KB = 2^12 bytes)
offset bits = 12
page-number bits = 32 − 12 = 20 → 2^20 ≈ 1 million pages
page table size = 2^20 entries × 4 bytes each = 4 MB — per process!
Physical memory 1 GB = 2^30, frame size 4 KB = 2^12
number of frames = 2^30 / 2^12 = 2^18 → frame number needs 18 bits
Paging still has internal fragmentation: a process needing 4097 bytes with 4 KB pages uses 2 pages and wastes 4095 bytes in the last one. On average, half a page per process.
The TLB — making paging fast
With a page table in RAM, every memory access would need two accesses: one for the page table, one for the data. That halves speed.
The TLB (Translation Lookaside Buffer) is a tiny, very fast cache inside the CPU holding recent page → frame translations. Thanks to locality (programs keep using the same few pages), hit rates are usually above 99%.
Effective Access Time (EAT)
TLB lookup = 20 ns, memory access = 100 ns, TLB hit ratio = 80%
hit: 20 + 100 = 120 ns (TLB, then data)
miss: 20 + 100 + 100 = 220 ns (TLB, page table, then data)
EAT = 0.80 × 120 + 0.20 × 220 = 96 + 44 = 140 ns
Without a TLB it would be 200 ns; perfect hits would give 120 ns.
24. Multi-level Page Tables & Segmentation
Why one big page table doesn't work
We just saw a 32-bit system needs a 4 MB page table per process, and it must be contiguous. 100 processes = 400 MB of page tables, mostly describing memory nobody uses. On 64-bit systems a flat table would be impossibly huge.
Two-level (hierarchical) paging
Page the page table. Split the page number into two parts. The first indexes an outer table, which points to a smaller inner table, which gives the frame.
Inner tables for unused regions are simply never created. That's the saving.
A phone book split by first letter. You don't print the "X" section at all if nobody's name starts with X.
32-bit address, 4 KB pages, 4-byte entries:
| p1 (10 bits) | p2 (10 bits) | offset (12 bits) |
outer index inner index within the page
outer table: 2^10 entries × 4 B = 4 KB (fits exactly in one page)
each inner table: also 4 KB, created only if needed
64-bit x86 uses 4 levels (9 + 9 + 9 + 9 + 12 bits = 48-bit addresses), newer chips 5. The cost: a TLB miss now needs several memory reads — which is why the TLB matters so much.
Other options worth naming: inverted page table — one entry per physical frame instead of per page, so its size depends on RAM, not on how many processes there are (slower lookup, needs hashing). Hashed page table — hash the page number into a table; common for large address spaces.
Segmentation
Segmentation divides memory the way a programmer thinks about it: a code segment, a data segment, a stack segment — each a different size.
A logical address is (segment number, offset). A segment table stores each segment's base and limit.
Segment table: seg base limit
0 1400 1000
1 6300 400
2 4300 400
(2, 53) → 53 < 400 → physical = 4300 + 53 = 4353 ✓
(2, 500) → 500 ≥ 400 → trap (segmentation fault) ✗
(0, 999) → 999 < 1000 → 1400 + 999 = 2399 ✓
| Paging | Segmentation | |
|---|---|---|
| Block size | Fixed (pages) | Variable (segments) |
| Split by | Hardware, blindly | Program's logical parts |
| Fragmentation | Internal only | External only |
| Programmer sees it? | No | Yes |
| Protection | Per page | Per segment — natural (code read-only, data writable) |
Segmented paging combines them: split into segments, then page each segment. Gets segmentation's logical view without external fragmentation. x86 historically did this; modern 64-bit OSes mostly use plain paging and keep segments flat.
25. Virtual Memory & Page Faults
Virtual memory lets a program use more memory than the RAM actually has. Only the pages being used right now sit in RAM; the rest wait on disk.
With demand paging, a page is loaded only when the program first touches it — not before.
A small study desk and a big bookshelf. You keep only the books you're reading on the desk. When you need another, you fetch it from the shelf, putting one back if the desk is full.
What you get from it
- Programs can be bigger than RAM.
- More programs fit in memory at once, since each only keeps its active pages.
- Faster start-up — you don't load the whole program first.
- Easy sharing (shared libraries mapped into many processes) and copy-on-write after fork.
The valid bit
Each page-table entry has a valid/invalid bit. Valid = the page is in RAM. Invalid = it's on disk (or not part of the process at all). Other bits worth knowing: dirty (modified since loading — must be written back before eviction), referenced (used recently — helps replacement), and protection (read/write/execute).
What happens on a page fault
A page fault is the trap the MMU raises when a process touches a page marked invalid.
- The CPU traps into the kernel.
- The kernel checks: is this address legal for the process? If not → kill it (segfault).
- If legal, find a free frame. If none is free, pick a victim page to evict (section 26). If the victim is dirty, write it to disk first.
- Schedule a disk read to bring the page into the frame. The process waits meanwhile; others run.
- Update the page table: set the frame number and mark it valid.
- Restart the instruction that faulted. This time it works.
How much a page fault costs
Memory access = 200 ns, page fault service = 8 ms = 8,000,000 ns
Page fault rate p
EAT = (1 − p) × 200 + p × 8,000,000
If p = 1/1000:
EAT = 0.999 × 200 + 8,000
= 199.8 + 8,000 ≈ 8,200 ns → about 40× slower than normal!
To stay within 10% of 200 ns (EAT ≤ 220):
200 + p × 7,999,800 ≤ 220 → p ≤ 0.0000025 (fewer than 1 in 400,000)
That's the whole point of the next two sections: a disk is around 100,000× slower than RAM, so page faults must be very rare.
/usr/bin/time -v on Linux shows both counts.26. Page Replacement
When RAM is full and a new page is needed, the OS must throw one page out. The page replacement algorithm decides which.
The goal is to evict the page you won't need for the longest time, so you cause the fewest future faults.
The algorithms
- FIFO — evict the page that has been in memory the longest. Simple queue. But the oldest page might be the most used one.
- Optimal (OPT / Belady's MIN) — evict the page that will next be used furthest in the future. Gives the fewest faults possible. Impossible in practice (needs the future) — used as a yardstick to measure others.
- LRU (Least Recently Used) — evict the page not used for the longest time. Uses the past to guess the future. Very good, but tracking exact order on every access is costly.
- Clock (Second Chance) — a cheap LRU approximation. Pages sit in a circle with a reference bit. The "hand" moves round: bit = 1 → clear it and skip (second chance); bit = 0 → evict. Real OSes use variants of this.
- LFU / MFU — least / most frequently used. Rarely used; mentioned for completeness.
Worked example — 3 frames
Reference string (the order pages are used): 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1
Each cell shows the frames after that reference. F = page fault, blank = hit.
| # | Ref | FIFO | LRU | Optimal |
|---|---|---|---|---|
| 1 | 7 | 7 – – F | 7 – – F | 7 – – F |
| 2 | 0 | 7 0 – F | 7 0 – F | 7 0 – F |
| 3 | 1 | 7 0 1 F | 7 0 1 F | 7 0 1 F |
| 4 | 2 | 2 0 1 F | 2 0 1 F | 2 0 1 F |
| 5 | 0 | 2 0 1 | 2 0 1 | 2 0 1 |
| 6 | 3 | 2 3 1 F | 2 0 3 F | 2 0 3 F |
| 7 | 0 | 2 3 0 F | 2 0 3 | 2 0 3 |
| 8 | 4 | 4 3 0 F | 4 0 3 F | 2 4 3 F |
| 9 | 2 | 4 2 0 F | 4 0 2 F | 2 4 3 |
| 10 | 3 | 4 2 3 F | 4 3 2 F | 2 4 3 |
| 11 | 0 | 0 2 3 F | 0 3 2 F | 2 0 3 F |
| 12 | 3 | 0 2 3 | 0 3 2 | 2 0 3 |
| 13 | 2 | 0 2 3 | 0 3 2 | 2 0 3 |
| 14 | 1 | 0 1 3 F | 1 3 2 F | 2 0 1 F |
| 15 | 2 | 0 1 2 F | 1 3 2 | 2 0 1 |
| 16 | 0 | 0 1 2 | 1 0 2 F | 2 0 1 |
| 17 | 1 | 0 1 2 | 1 0 2 | 2 0 1 |
| 18 | 7 | 7 1 2 F | 1 0 7 F | 7 0 1 F |
| 19 | 0 | 7 0 2 F | 1 0 7 | 7 0 1 |
| 20 | 1 | 7 0 1 F | 1 0 7 | 7 0 1 |
| Faults | 15 | 12 | 9 |
FIFO = 15 faults
LRU = 12 faults
Optimal = 9 faults (no algorithm can do better on this string)
Hit ratio for LRU = (20 − 12) / 20 = 40%
How to do it fast in an exam. FIFO: keep a queue, evict the front. LRU: look backwards from the current position; the page whose last use is furthest back goes. Optimal: look forwards; the page whose next use is furthest away (or never) goes.
Belady's anomaly
You'd expect more frames to always mean fewer faults. With FIFO, that's not guaranteed — adding a frame can increase faults. That surprise is Belady's anomaly.
Reference string: 1 2 3 4 1 2 5 1 2 3 4 5 (FIFO)
3 frames: 1F 2F 3F 4F 1F 2F 5F 1 2 3F 4F 5 → 9 faults
4 frames: 1F 2F 3F 4F 1 2 5F 1F 2F 3F 4F 5F → 10 faults (more!)
LRU and Optimal never suffer from it. They are stack algorithms: the set of pages kept with n frames is always a subset of the set kept with n + 1 frames. FIFO doesn't have that property.
27. Thrashing & the Working Set
Thrashing is when the system spends more time swapping pages in and out than actually running programs. The disk light is solid on, and nothing gets done.
It happens when processes don't have enough frames for the pages they're actively using. Every page brought in pushes out a page that's needed a moment later.
A tiny desk with ten books you need at once. You keep putting one book back to fetch another, then immediately need the first again. You spend the whole evening walking to the shelf and read nothing.
The vicious circle
too many processes → each has too few frames → lots of page faults
→ processes wait on disk → CPU use drops
→ OS thinks "CPU is idle, let's admit more processes"
→ even fewer frames each → even more faults … (collapse)
On a graph of CPU use vs number of processes, use rises, peaks, then falls off a cliff. That cliff is thrashing.
Locality and the working set
Locality of reference: at any moment, a program uses a small group of pages heavily (the current function, its data). That group is its locality, and it shifts over time.
The working set is the set of pages a process used in its last Δ references (Δ is a window, e.g. the last 10,000 accesses).
Δ = 5, recent references: … 2 6 1 5 7 7 7 7 5 1 | now
working set over the last 5 refs (7 7 7 5 1) = {1, 5, 7} → size 3
D = sum of all processes' working-set sizes
If D > total frames → thrashing is coming → suspend a process
If D is well below → room to admit another process
Other fixes
- Page-fault frequency (PFF) — set an upper and lower fault rate. Above the upper → give the process more frames. Below the lower → take some away.
- Reduce multiprogramming — swap out or suspend some processes.
- Local replacement — a process can only evict its own pages, so one thrashing process can't drag others down.
- And the honest real-world one: add more RAM.
28. File Systems
A disk only knows numbered blocks (say 4 KB each). A file system turns those blocks into named files inside folders, and remembers which blocks belong to which file.
A library. The shelves are blocks. The catalogue — title, author, which shelf — is the file system. Without the catalogue the books are still there, but you'd never find one.
What a file has
- Data — the content.
- Metadata — size, owner, permissions, timestamps (created, modified, accessed), and where its blocks are.
- Operations: create, open, read, write, seek (jump to a position), close, delete.
When you open a file, the OS returns a small number called a file descriptor (fd). Every later read/write uses that number. 0, 1 and 2 are always stdin, stdout and stderr.
Directory structures
- Single-level — all files in one folder. Names clash quickly.
- Two-level — one folder per user.
- Tree — folders within folders. What you use every day. Paths are absolute (
/home/ada/notes.txt) or relative (../notes.txt). - Acyclic graph — a tree plus links, so one file can appear in two folders.
How a file's blocks are stored — allocation methods
| Method | How | Good | Bad |
|---|---|---|---|
| Contiguous | File takes blocks next to each other; store start + length | Fastest reads; easy random access | External fragmentation; hard to grow a file |
| Linked | Each block holds a pointer to the next | No fragmentation; files grow easily | Random access is slow (follow the chain); one broken pointer loses the rest |
| Linked with FAT | The pointers are kept together in a table (File Allocation Table) | Table can be cached in RAM | Table gets big on large disks |
| Indexed | Each file has an index block listing all its block numbers | Fast random access; no external fragmentation | Index block overhead, even for tiny files |
Inodes (Unix's indexed scheme)
In Unix/Linux every file has an inode — a record with all the metadata except the name, plus pointers to the data blocks. The name lives in the directory, which is just a list of name → inode number pairs.
inode
├── owner, permissions, size, timestamps, link count
├── 12 direct pointers → data blocks
├── 1 single indirect pointer → a block full of pointers → data blocks
├── 1 double indirect pointer → pointers → pointers → data
└── 1 triple indirect pointer → three levels deep
Block 4 KB, pointer 4 B → one block holds 1024 pointers
direct: 12 × 4 KB = 48 KB
single indirect: 1024 × 4 KB = 4 MB
double indirect: 1024² × 4 KB = 4 GB
triple indirect: 1024³ × 4 KB = 4 TB
Small files (most files) are reached in one step through direct pointers. Huge files are still possible through the indirect ones. GATE often asks for the max file size — add up all four lines.
Hard links vs soft links
| Hard link | Soft (symbolic) link | |
|---|---|---|
| What it is | Another name pointing to the same inode | A tiny file that contains a path to another file |
| Create | ln a.txt b.txt | ln -s a.txt b.txt |
| Delete the original | Data survives — the inode lives until its link count hits 0 | Link breaks ("dangling") |
| Across file systems? | No — inode numbers are per file system | Yes |
| To directories? | Not allowed (could create loops) | Allowed |
Picture it: a hard link is a second front door into the same house. A soft link is a note on a door saying "the house is at 12 Baker Street" — if the house is demolished, the note still exists but leads nowhere.
Journaling — surviving a crash
Creating a file touches several places: the inode, the directory, the free-block list. If power fails halfway, the file system is left inconsistent. Old systems ran a slow full check (fsck) on reboot.
A journaling file system first writes a note of what it's about to do into a log (the journal), then does it, then marks the note done. After a crash it just replays or discards unfinished notes — seconds, not hours. Same idea as a database's write-ahead log. Examples: ext4, NTFS, XFS. (Newer ones like Btrfs, ZFS and Apple's APFS use copy-on-write instead: never overwrite, write new and switch the pointer.)
29. Disk Scheduling
On a spinning hard disk, a read/write head must physically move to the right track (cylinder). This movement — the seek — is the slowest part of a disk access.
When many requests are waiting, the order you serve them in changes how far the head travels. Disk scheduling picks a good order.
A lift in a tall building. If it went to floors strictly in the order buttons were pressed, it would zigzag wildly. Instead it goes up collecting everyone, then comes down. That's literally the SCAN algorithm — also called the elevator algorithm.
Disk access time
access time = seek time + rotational latency + transfer time
(move arm) (wait for sector (read the data)
to spin under head)
Average rotational latency = half a rotation.
7200 RPM → 7200/60 = 120 rotations per second → 1 rotation = 8.33 ms
→ average latency = 4.17 ms
The algorithms
- FCFS — in order of arrival. Fair, but lots of back-and-forth.
- SSTF (Shortest Seek Time First) — always go to the nearest request. Much less movement, but far-away requests can starve.
- SCAN (elevator) — move in one direction to the end of the disk, serving requests on the way, then reverse.
- C-SCAN (circular SCAN) — move in one direction to the end, then jump back to the start without serving, and sweep again. More even waiting times.
- LOOK / C-LOOK — like SCAN / C-SCAN, but only go as far as the last request, not the physical end.
Worked example
Cylinders 0–199. Head at 53, moving towards higher numbers. Queue: 98, 183, 37, 122, 14, 124, 65, 67.
FCFS: 53 → 98 → 183 → 37 → 122 → 14 → 124 → 65 → 67
45 + 85 + 146 + 85 + 108 + 110 + 59 + 2 = 640
SSTF: 53 → 65 → 67 → 37 → 14 → 98 → 122 → 124 → 183
12 + 2 + 30 + 23 + 84 + 24 + 2 + 59 = 236
SCAN: 53 → 65 → 67 → 98 → 122 → 124 → 183 → 199 → 37 → 14
(199 − 53) + (199 − 14) = 146 + 185 = 331
C-SCAN: 53 → … → 183 → 199 → jump to 0 → 14 → 37
146 + 199 (jump) + 37 = 382
(183 if the question says not to count the jump)
LOOK: 53 → 65 → 67 → 98 → 122 → 124 → 183 → 37 → 14
(183 − 53) + (183 − 14) = 130 + 169 = 299
C-LOOK: 53 → … → 183 → jump to 14 → 37
130 + 169 (jump) + 23 = 322
(153 if the jump isn't counted)
| Algorithm | Total head movement | Starvation? |
|---|---|---|
| FCFS | 640 | No |
| SSTF | 236 | Yes |
| SCAN | 331 | No |
| C-SCAN | 382 (183 without the jump) | No — most uniform wait |
| LOOK | 299 | No |
| C-LOOK | 322 (153 without the jump) | No |
none or mq-deadline) for them.30. I/O: Interrupts, Polling, DMA & Buffering
Devices (keyboard, disk, network card) are far slower than the CPU. The question is how the CPU finds out a device is ready, and how data gets moved without wasting the CPU's time.
Polling vs interrupts
| Polling | Interrupts | |
|---|---|---|
| How | CPU keeps asking the device "done yet?" in a loop | Device signals the CPU when it's done |
| Picture | A child asking "are we there yet?" every minute | The driver tells you when you've arrived |
| Good when | Device is very fast or almost always ready | Events are rare or slow (keyboard, disk) |
| Cost | Wastes CPU if the device is slow | Each interrupt has overhead; a flood can swamp the CPU |
What happens on an interrupt
- The device raises a signal on an interrupt line.
- The CPU finishes its current instruction, saves where it was, and jumps into kernel mode.
- It looks up the interrupt vector table to find the right handler (ISR, interrupt service routine) for that device.
- The handler does the minimum quickly (e.g. copy the key pressed) and schedules the rest for later.
- The CPU restores what it saved and carries on.
Hardware interrupts come from devices. Software interrupts / traps come from programs — system calls, or errors like divide-by-zero. Fast network cards switch between interrupts and polling depending on load (Linux's NAPI does this).
DMA — Direct Memory Access
Copying a 1 MB file from disk byte by byte through the CPU would waste millions of CPU cycles. A DMA controller copies data between the device and RAM by itself.
The CPU just says "move this block from the disk to this RAM address", goes off to do other work, and gets one interrupt when the whole transfer is done.
Instead of carrying every box from the lorry yourself, you hire a mover, tell them where it goes, and they ring you when it's finished.
Buffering, caching and spooling
- Buffer — a temporary holding area for data in transit, to smooth out speed differences. Video players buffer a few seconds ahead. Double buffering: fill one buffer while the other is being used.
- Cache — a fast copy of data kept for reuse. The OS page cache keeps recently read file blocks in RAM, which is why opening a file the second time is instant.
- Spooling — a queue on disk for a device that can serve only one job at a time. The printer spooler lets ten people "print" at once; jobs are written to disk and fed to the printer one by one. (SPOOL = Simultaneous Peripheral Operations On-Line.)
Blocking vs non-blocking I/O
Blocking: read() waits until data arrives — the thread sleeps. Non-blocking: read() returns straight away, with data or "nothing yet". Asynchronous: you ask, carry on, and get notified when done. Node.js is built on non-blocking I/O plus an event loop (using epoll on Linux), which is how one thread handles thousands of connections.
31. Linux Essentials for Developers
Everything above shows up the moment you run a server. These are the commands and ideas you'll actually use — and that interviewers ask about to see if you've touched a real machine.
Looking at processes
ps aux # every process: user, PID, CPU%, MEM%, state, command
ps -ef --forest # with parent–child tree
top / htop # live view, sorted by CPU
pgrep node # PIDs of processes named node
pstree -p # tree of processes
lsof -i :3000 # which process is using port 3000
free -h # RAM and swap usage
vmstat 1 # memory, swapping, CPU every second
strace -p 1234 # watch the system calls a process makes
The STAT column in ps: R running/ready, S sleeping (waiting, can be woken), D uninterruptible sleep (usually stuck on disk I/O), Z zombie, T stopped. These map straight onto the process states in section 6.
Background jobs
node server.js & # run in background
jobs # list background jobs
fg %1 # bring job 1 to foreground
Ctrl+Z, then bg # pause, then resume in background
nohup node server.js & # keep running after you log out
nice -n 10 ./backup.sh # start with lower priority (higher nice = nicer to others)
renice -n 5 -p 1234 # change priority of a running process
Signals
A signal is a small message the OS delivers to a process. The process can catch most of them and run its own handler.
| Signal | No. | Sent by | Can be caught? | Meaning |
|---|---|---|---|---|
SIGINT | 2 | Ctrl+C | Yes | Interrupt from keyboard |
SIGTERM | 15 | kill PID (default) | Yes | "Please shut down" — clean up and exit |
SIGKILL | 9 | kill -9 PID | No | Killed immediately by the kernel |
SIGHUP | 1 | Terminal closed | Yes | Often used to mean "reload config" |
SIGTSTP | 20 | Ctrl+Z | Yes | Polite pause from the terminal |
SIGSTOP / SIGCONT | 19 / 18 | kill -STOP / fg, kill -CONT | STOP: No | Forced pause / resume |
SIGCHLD | 17 | Kernel | Yes | A child process ended — time to wait() |
SIGSEGV | 11 | Kernel | Yes (rarely wise) | Invalid memory access |
(Signal numbers shown are for Linux on x86/ARM.)
process.on("SIGTERM", …) for a graceful shutdown.File permissions
$ ls -l deploy.sh
-rwxr-x--- 1 ada devs 512 Oct 6 10:00 deploy.sh
│└┬┘└┬┘└┬┘
│ │ │ └── others: --- (nothing)
│ │ └───── group: r-x (read, execute)
│ └──────── owner: rwx (read, write, execute)
└────────── type: - file, d directory, l symlink
r = 4, w = 2, x = 1 → add them up per group
rwx = 7, r-x = 5, --- = 0 → chmod 750 deploy.sh
chmod 644 file # rw-r--r-- normal file
chmod 755 dir # rwxr-xr-x normal directory / script
chmod +x run.sh # add execute for everyone
chown ada:devs f # change owner and group
On a directory, r means list its contents, w means create/delete files in it, and x means enter it (cd). That's why folders need x.
Root (user ID 0) bypasses permission checks. Run services as a normal user, and use sudo only for the one command that needs it.
/dev/sda), process info (/proc/1234/status), even sockets and pipes use file descriptors. That's why the same read/write calls work on all of them — and why "too many open files" is an error you'll hit on busy servers (raise it with ulimit -n).32. Virtual Machines vs Containers
A virtual machine (VM) pretends to be a whole computer. It runs its own full operating system on fake hardware provided by a hypervisor.
A container is just a normal process (or group) on the host that the kernel walls off, so it sees its own files, network and process list. All containers share the host's kernel.
VMs are separate houses on one plot — each with its own plumbing and wiring. Containers are flats in one building — private front doors, but shared foundations, water mains and electrics.
| Virtual machine | Container | |
|---|---|---|
| Isolates at | Hardware level | OS (process) level |
| Own kernel? | Yes — full guest OS | No — shares the host's |
| Size | GBs | MBs |
| Start time | Tens of seconds to minutes | Under a second to a few seconds |
| Run a different OS? | Yes — Windows VM on a Linux host | No — Linux containers need a Linux kernel |
| Security isolation | Stronger | Weaker — a kernel bug can affect all containers |
| Typical use | Cloud servers (EC2), running other OSes | Packaging and deploying apps, microservices |
Hypervisor types
- Type 1 (bare-metal) — runs directly on the hardware. VMware ESXi, Xen, Microsoft Hyper-V, KVM (part of Linux). Used in data centres and clouds.
- Type 2 (hosted) — runs as an app on a normal OS. VirtualBox, VMware Workstation. Used on laptops.
What makes a container possible (Linux kernel features)
- Namespaces — give a process its own view of things: its own process IDs (it thinks it's PID 1), network interfaces, mount points, hostname, users.
- cgroups (control groups) — limit how much CPU, memory and I/O a group of processes may use.
docker run --memory=512msets a cgroup limit. - Union / overlay file systems — image layers stacked on top of each other, shared between containers, with a thin writable layer on top.
Interview Q&A — quick answers
Basics
- What is an OS? The software that manages hardware and shares it safely between programs, and gives programs simple abstractions like files and processes.
- What is the kernel? The core of the OS that always stays in memory and runs in kernel mode with full hardware access.
- User mode vs kernel mode? User mode blocks privileged instructions; kernel mode allows everything. Apps run in user mode and enter the kernel only through system calls, interrupts or faults.
- What is a system call? A controlled request from a program to the kernel (open, read, fork). It uses a trap instruction to switch to kernel mode and back.
- Monolithic vs microkernel? Monolithic keeps all services in the kernel (fast, less isolated — Linux). Microkernel keeps only the bare minimum and runs the rest as user-space servers (safer, slower — QNX, MINIX).
- Multiprogramming vs multitasking? Multiprogramming switches when a process waits, to keep the CPU busy. Multitasking also switches on a timer, to keep response times low.
Processes & threads
- Program vs process? A program is a passive file on disk; a process is a running instance with its own memory, registers and state.
- What is a PCB? The kernel's record of a process — PID, state, program counter, registers, memory info, open files — used to pause and resume it.
- Process states? New, Ready, Running, Waiting, Terminated. Waiting goes back to Ready, never straight to Running.
- Process vs thread? Processes have separate memory; threads in a process share code, data, heap and files, but each has its own stack, registers and PC. Threads are cheaper to create and switch.
- What is a context switch? Saving one process's CPU state to its PCB and loading another's. Pure overhead, plus cache and TLB warm-up cost.
- What does fork return? 0 in the child, the child's PID in the parent, −1 on failure. n forks in a row give 2ⁿ processes.
- Zombie vs orphan? Zombie: child finished but parent hasn't called wait(). Orphan: parent finished first; init adopts the child.
- User vs kernel threads? User threads are managed by a library and invisible to the kernel (fast, but one block blocks all). Kernel threads are scheduled by the OS and can use multiple cores.
Scheduling
- Preemptive vs non-preemptive? Preemptive can take the CPU away mid-run; non-preemptive waits for the process to finish or block.
- Which algorithm gives minimum average waiting time? SJF (non-preemptive) / SRTF (preemptive) — provably optimal, but needs burst times.
- What is the convoy effect? In FCFS, short processes stuck behind one long process.
- How do you prevent starvation in priority scheduling? Aging — gradually raise the priority of waiting processes.
- Effect of time quantum in RR? Too large → becomes FCFS. Too small → time wasted on context switches.
- TAT and WT formulas? TAT = CT − AT. WT = TAT − BT. RT = first run − AT.
- What is MLFQ? Multiple queues with different priorities and quanta; processes move down if CPU-hungry and up if they wait long. Learns process behaviour.
Synchronisation
- What is a race condition? When the result depends on the timing of threads accessing shared data, e.g. two threads doing
count++. - Critical section requirements? Mutual exclusion, progress, bounded waiting.
- Mutex vs semaphore? A mutex is a lock with an owner (only the locker unlocks). A semaphore is a counter any thread can signal; counting semaphores manage N resources.
- Binary semaphore vs mutex? Similar values (0/1), but a mutex has ownership and a binary semaphore doesn't — so a semaphore can be used for signalling between threads.
- What is busy waiting / a spinlock? Looping until a lock frees up. Wastes CPU; only worth it for very short waits on multi-core machines.
- What is a monitor? A language-level construct whose methods run one at a time automatically, with condition variables for waiting. Java's
synchronized. - Why wait in a while loop? Spurious wakeups and other threads acting first — the condition must be re-checked after waking.
- Producer–consumer semaphores? mutex = 1, empty = N, full = 0. Wait on empty/full before mutex, or it deadlocks.
- How to avoid deadlock in dining philosophers? Allow at most 4 at the table, make one philosopher pick up in reverse order, or pick up both chopsticks atomically.
Deadlock
- Four necessary conditions? Mutual exclusion, hold and wait, no preemption, circular wait. Break any one to prevent deadlock.
- Most practical prevention? Break circular wait by always acquiring resources (locks) in a fixed global order.
- Safe vs unsafe state? Safe: some order exists in which every process can finish. Unsafe means deadlock is possible, not certain.
- What does Banker's algorithm do? Grants a request only if the resulting state is still safe. Need = Max − Allocation.
- Cycle in a RAG always means deadlock? Only if each resource has a single instance. With multiple instances, a cycle means possible deadlock.
- Deadlock vs starvation vs livelock? Deadlock: all stuck waiting on each other. Starvation: one process keeps being passed over. Livelock: processes keep reacting to each other but make no progress.
Memory
- Logical vs physical address? Logical is what the program uses; physical is the real RAM location. The MMU translates at run time.
- Internal vs external fragmentation? Internal: unused space inside an allocated block. External: free memory split into gaps too small to use. Paging removes external; segmentation removes internal.
- What is paging? Splitting logical memory into fixed-size pages and RAM into frames, mapped by a per-process page table.
- What is the TLB? A small fast cache of recent page-to-frame translations, avoiding an extra memory access per lookup.
- Why multi-level page tables? A flat table is huge (4 MB per process on 32-bit); multi-level tables only create the parts actually used.
- What is virtual memory? Letting processes use more memory than physical RAM by keeping only active pages in RAM and the rest on disk.
- What is a page fault? A trap when a process touches a page not in RAM; the OS loads it from disk and restarts the instruction.
- Best page replacement algorithm? Optimal in theory (needs the future). LRU in practice, usually approximated by the Clock algorithm.
- What is Belady's anomaly? With FIFO, more frames can cause more page faults. LRU and Optimal are immune.
- What is thrashing? The system spends most of its time paging rather than working, because processes don't have enough frames for their working sets.
Files, disks & I/O
- What is an inode? A Unix record of a file's metadata and block pointers — everything except its name.
- Hard link vs soft link? Hard link: another name for the same inode (survives deleting the original). Soft link: a file holding a path (breaks if the target goes).
- What is journaling? Logging intended changes before making them, so the file system can recover quickly after a crash.
- Which disk algorithm minimises seek but can starve? SSTF. SCAN/C-SCAN avoid starvation; C-SCAN gives the most uniform wait.
- What is DMA? A controller that moves data between a device and RAM without the CPU, interrupting once when the transfer is complete.
- Polling vs interrupts? Polling: CPU repeatedly checks the device. Interrupts: device notifies the CPU when ready.
- What is spooling? Queueing jobs on disk for a device that handles one at a time, like a printer.
Practical
- SIGTERM vs SIGKILL? SIGTERM (15) asks politely and can be handled for a clean shutdown. SIGKILL (9) can't be caught — the kernel kills the process immediately.
- What does chmod 755 mean? Owner rwx, group r-x, others r-x.
- VM vs container? A VM virtualises hardware and runs its own OS kernel. A container is an isolated process sharing the host kernel, using namespaces and cgroups — lighter and faster, less isolated.
- What are namespaces and cgroups? Namespaces control what a process can see; cgroups control how much it can use.
Learning Path
- The big picture — what the kernel does, user vs kernel mode, system calls. Run
strace lsand look at the calls it makes. - Processes — states, PCB, fork/exec/wait. Write a small C program that forks, and solve a few "how many times does this print" puzzles.
- Threads — write a program where two threads increment a shared counter a million times. See the wrong answer. Fix it with a mutex.
- Scheduling numericals — practise FCFS, SJF, SRTF, Priority and RR until drawing the Gantt chart is automatic. Always double-check by summing.
- Synchronisation — semaphores, then producer–consumer, readers–writers and dining philosophers from memory.
- Deadlock — the four conditions, RAGs, and at least three Banker's problems end to end.
- Memory — address splitting and bit-counting, TLB effective access time, then paging vs segmentation.
- Virtual memory — page faults and the three replacement algorithms on paper. Reproduce Belady's anomaly yourself.
- Files and disks — inodes and max file size, links, and disk scheduling numericals in both directions.
- Hands-on Linux —
ps,top,kill,chmod, signals. Write a Node server that shuts down gracefully on SIGTERM. - Containers — run a Docker container, then look at its processes from the host with
ps. Seeing it as "just a process" makes namespaces click. - Then — previous-year GATE OS questions, topic by topic. They repeat patterns heavily.