Skip to content
Educora
University25 min54 / 59

Operating systems

Learn processes and threads, CPU scheduling, virtual memory and paging, file systems and deadlock, with worked calculations.

Check yourself
In this lesson you will learn
  • Distinguish processes from threads and explain a race condition
  • Compute the average waiting time for FCFS, SJF and Round Robin
  • Translate a virtual address to a physical one and count page faults
  • Apply the four deadlock conditions and the banker's algorithm

Your laptop runs a browser, music, a messenger and an antivirus at the same time, yet it has only a few processor cores and limited memory. Every program believes the whole computer is its own — and the operating system (OS) creates that illusion. The OS shares out processor time, gives each program its own memory, organises files on disk and keeps programs from locking each other up.

Processes, threads and scheduling

Definition
Process and thread

A process is a program in execution: it has its own virtual address space (code, data, heap), open files and at least one thread. A thread is a sequence of execution inside a process: it has its own stack, registers and PC but shares the process's memory with the other threads. Threads are cheaper to create and switch than processes, but shared memory needs synchronisation.

Example 1: a race condition

Two threads each increment a shared counter = 5. Each does three steps: read (tmp = counter), add (tmp + 1), write (counter = tmp). What is the result if the scheduler interleaves them as: A read, B read, A add, A write, B add, B write?

Show solution
A reads: tmpA = 5. B reads: tmpB = 5.
A: tmpA = 6, counter = 6. B: tmpB = 6, counter = 6.
After two increments we have 6 instead of 7 — one update was “lost”. The result depends on the interleaving: this is a race condition. The fix: protect the critical section with a mutex (lock) so that read–add–write becomes indivisible.

Ready processes wait in a queue, and the scheduler decides which one gets the processor. FCFS — first come, first served; SJF — shortest job first (it minimises the average waiting time but needs to know job lengths in advance); Round Robin — each process gets a time quantum q, then goes to the back of the queue. Every switch costs a context switch: saving and restoring registers.

W = (1 / n) · ∑ (Cᵢ − Aᵢ − Bᵢ)W = (1 / n) · ∑ (Cᵢ − Aᵢ − Bᵢ)
where:
  • Waverage waiting time
  • Cᵢcompletion time of process i
  • Aᵢarrival time
  • BᵢCPU burst length
Example 2: three scheduling algorithms

Processes P1 = 10, P2 = 4, P3 = 2 ms arrive at time 0 in this order. Find the average waiting time for FCFS, SJF and Round Robin (q = 3).

Show solution
FCFS: P1 0–10, P2 10–14, P3 14–16 → waits 0, 10, 14 → W = 24/3 = 8 ms.
SJF: P3 0–2, P2 2–6, P1 6–16 → waits 0, 2, 6 → W = 8/3 ≈ 2.67 ms.
RR, q = 3: P1 0–3, P2 3–6, P3 6–8 (done), P1 8–11, P2 11–12 (done), P1 12–16 (done).
Wait = completion − burst: P1 16 − 10 = 6, P2 12 − 4 = 8, P3 8 − 2 = 6 → W = 20/3 ≈ 6.67 ms.
RR loses to SJF on waiting, but every process starts getting service within 6 ms — which is what interactive systems care about.
Python
from collections import deque

def fcfs(bursts):
    t, waits = 0, []
    for b in bursts:
        waits.append(t)
        t += b
    return waits

def round_robin(bursts, q):
    rem, t, done = list(bursts), 0, [0] * len(bursts)
    queue = deque(range(len(bursts)))
    while queue:
        i = queue.popleft()
        run = min(q, rem[i])
        t += run
        rem[i] -= run
        if rem[i]:
            queue.append(i)
        else:
            done[i] = t
    return [done[i] - bursts[i] for i in range(len(bursts))]

jobs = [10, 4, 2]
for name, w in [('FCFS', fcfs(jobs)), ('SJF', fcfs(sorted(jobs))), ('RR q=3', round_robin(jobs, 3))]:
    print(name, w, 'average wait:', round(sum(w) / len(w), 2))
▸ Expected output
FCFS [0, 10, 14] average wait: 8.0
SJF [0, 2, 6] average wait: 2.67
RR q=3 [6, 8, 6] average wait: 6.67
The simulation confirms our hand calculation (in the SJF line the waits are listed in sorted job order — P3, P2, P1). Change q: a very large q turns RR into FCFS, a very small one wastes time on context switches.

Virtual memory and paging

Each process sees its own virtual address space starting at 0. Memory is split into equal pages (e.g. 4 KB) and physical RAM into equal frames; each process's page table maps page → frame. The hardware (MMU) does the translation and keeps recent translations in the TLB cache. If a page is not in RAM, a page fault occurs and the OS loads it from disk — so programs can use more memory than physical RAM and cannot see each other's memory.

p = ⌊VA / P⌋, d = VA mod P, PA = frame(p) · P + dp = ⌊VA / P⌋, d = VA mod P, PA = frame(p) · P + d
where:
  • VA, PAvirtual and physical address
  • Ppage size, bytes
  • p, dpage number and offset within the page

When P = 2ᵏ the division just splits the bits: with 4 KB = 2¹² pages the low 12 bits are the offset and the rest is the page number.

Example 3: address translation and the cost of a page fault

a) Page size 4 KB, VA = 0x3A7C, page table maps 3 → 7. What is the physical address?
b) A RAM access takes 100 ns, handling a page fault takes 8 ms, fault probability p = 0.001. What is the effective access time?

Show solution
a) 0x3A7C = 14,972. p = ⌊14,972 / 4096⌋ = 3, d = 14,972 − 12,288 = 2684 (= 0xA7C).
PA = 7 · 4096 + 2684 = 28,672 + 2684 = 31,356 = 0x7A7C. In hex only the page digit changed: 0x3A7C → 0x7A7C.
b) EAT = (1 − p) · 100 + p · 8,000,000 ns = 99.9 + 8000 ≈ 8100 ns ≈ 8.1 µs — one fault per thousand accesses makes memory 81 times slower!

When RAM is full the OS must choose which page to evict to disk. FIFO evicts the page loaded earliest, LRU the one unused for the longest time. The theoretical ideal OPT evicts the page needed furthest in the future, but the future is unknown, so it serves only as a benchmark.

Python
def faults(refs, frames, policy):
    mem, count = [], 0
    for p in refs:
        if p in mem:
            if policy == 'LRU':
                mem.remove(p)
                mem.append(p)
            continue
        count += 1
        if len(mem) == frames:
            mem.pop(0)
        mem.append(p)
    return count

refs = [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5]
for frames in [3, 4]:
    print(frames, 'frames: FIFO', faults(refs, frames, 'FIFO'), '| LRU', faults(refs, frames, 'LRU'))

page_size, va = 4096, 0x3A7C
page, offset = divmod(va, page_size)
page_table = {3: 7}
pa = page_table[page] * page_size + offset
print('page', page, 'offset', offset, '-> physical', pa, hex(pa))
▸ Expected output
3 frames: FIFO 9 | LRU 10
4 frames: FIFO 10 | LRU 8
page 3 offset 2684 -> physical 31356 0x7a7c
A surprise: with FIFO, going from 3 to 4 frames increased the page faults from 9 to 10 — this is Belady's anomaly. LRU never suffers from it: 10 → 8. The last line confirms part (a) of Example 3.

File systems

A disk is divided into fixed-size blocks (usually 4 KB). In Unix file systems (ext4) each file's inode stores its size, owner, permissions, timestamps and pointers to the data blocks; a directory is just a “name → inode number” table. The classic ext2/ext3 inode has 12 direct pointers plus single, double and triple indirect pointers: with 4 KB blocks and 4-byte pointers, the direct blocks cover 48 KB, the single indirect block another 1024 · 4 KB = 4 MB, the double 4 GB and the triple 4 TB. Journaling (ext4, NTFS) first writes changes to a journal so the file system survives a power cut. In FAT32 a single file must be smaller than 4 GB.

Deadlock

Thread A holds lock 1 and waits for lock 2, while thread B holds lock 2 and waits for lock 1 — neither can move. Deadlock is possible when Coffman's four conditions all hold: mutual exclusion (a resource is used by one at a time), hold and wait, no preemption (resources are not taken away) and circular wait. Breaking any one is enough; the most practical way is to always acquire locks in the same global order, which rules out circular wait.

Example 4: the banker's algorithm

A system has 10 identical resources. P0: max 7, holds 3; P1: max 3, holds 1; P2: max 6, holds 3. Is the state safe? If P0 asks for 2 more, can the request be granted?

Show solution
Free: 10 − (3 + 1 + 3) = 3. Needs: P0 4, P1 2, P2 3.
P1: 2 ≤ 3 → finishes and returns → free 4. P2: 3 ≤ 4 → free 7. P0: 4 ≤ 7 → free 10.
The safe sequence P1, P2, P0 exists → the state is safe.
If P0 gets 2 more: P0 holds 5, needs 2, free 1. Nobody's need is ≤ 1 (P0 2, P1 2, P2 3) → an unsafe state, so the request must wait. Unsafe does not yet mean deadlock, but the OS will not take the risk.

Key points

  • A process has its own address space; its threads share memory, so shared data must be protected by locks.
  • SJF minimises average waiting, Round Robin shortens response time; wait = completion − arrival − burst.
  • Virtual address = page + offset; PA = frame · P + d; page faults sharply increase the effective access time.
  • LRU is usually better than FIFO; FIFO can suffer from Belady's anomaly.
  • Deadlock needs all four conditions; a fixed lock order and the banker's algorithm prevent it.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
Two processes arrive at time 0: P1 = 6 ms, P2 = 3 ms (in that order). Round Robin with q = 4. What is the average waiting time?