The OS Simulator: Cores, Processes, RAM, I/O and Locks
A configurable model of a kernel — set cores, processes, threads, RAM, I/O operations and locks, then watch the scheduler, context switches, memory, page faults, blocked tasks and lock contention respond; every number is an educational simulation of the mechanisms in this domain, not a measurement of any real machine.
The problem
What is being simulated — and what is not
The playground is a discrete-time model. Each tick, a simulated scheduler picks runnable threads for each core, charges a context-switch cost when a core changes thread, advances each thread’s work, and decides whether it blocks on I/O, faults on a page or waits for a lock. Memory is a set of pages with an LRU-style replacement policy (LRU Cache); I/O is a queue with a fixed service time; locks are mutexes with a wait queue. The numbers on every panel come from that model. They are chosen to be *realistic in shape* — a context switch costs on the order of a few microseconds, a page fault to a simulated SSD costs on the order of 100 µs, a lock hand-off costs less than a switch — but they are not measurements, and the interactive says so with a Simulated tag.
What is deliberately left out: cache effects, NUMA, interrupt handling, priority inheritance, real scheduler heuristics (CFS/EEVDF weights, Windows priority boosts), write-back, and the difference between minor and major page faults. Those are the reasons a real system’s curve will differ from the simulator’s in slope, never in direction. The goal is to make the *mechanism* visible: if the model shows throughput falling when you add threads, the real system will too — at a different thread count.
- Every panel value is
simulated; compare shapes and directions, never absolute numbers, with a real machine. - The model is deterministic for a given configuration (seeded), so an experiment is repeatable and you can predict, then check.
- Real tools for the same questions:
vmstat 1,top -H,perf stat,/proc/<pid>/status— see The OS Debugging Playbook: Four Symptoms, Fourteen Causes.
The controls
Each control models one resource from a lesson in this domain, and each one is a constraint the real kernel juggles.
Two of the controls interact in ways the others do not. Threads and cores together decide the switching regime — throughput rises with threads until threads equal cores and falls after — and RAM and processes together decide the working set, because every process brings its own pages. Set one control at a time when you are learning a mechanism; set two together when you are learning why production incidents rarely have one cause.
| Control | Models | Lesson | Turn it and expect |
|---|---|---|---|
| CPU cores | how many threads can run truly in parallel | Concurrency versus Parallelism | more cores: higher throughput until threads < cores, then idle cores |
| Processes | independent address spaces and descriptor tables | The Anatomy of a Process | more processes: more memory (each has its own pages), costlier switches between them |
| Threads per process | schedulable units sharing one address space | Threads: Several Instruction Streams in One Process | more threads: more runnable work, then more switching than working |
| RAM (pages) | physical frames available before replacement | Memory Pressure, Swap and the OOM Killer | less RAM than the working set: page faults climb steeply |
| I/O operations | blocking reads/writes per thread per unit of work | Blocking, Non-blocking, Multiplexed, Asynchronous | more I/O: more threads in the blocked state, cores idle unless there are spare runnable threads |
| Locks | shared mutexes threads must take for part of their work | Mutexes | one hot lock: threads queue on it; adding cores stops helping |
| Time slice | the scheduler quantum | Scheduling Simulator: FCFS, Round Robin, Priority | shorter slice: better responsiveness, more switch overhead |
Reading the panels
The scheduler panel shows each core and which thread it is running, with the ready queue beside it (Queue → ready queue is the DSA transfer). A long ready queue with busy cores is CPU saturation; an empty queue with idle cores and many blocked threads is I/O-bound. The context switches counter and the switch overhead % show what fraction of each core’s time was spent saving and restoring state rather than doing work (Context Switching) — when it passes a few percent, adding threads is now costing throughput.
The memory panel shows frames in use, the simulated working set, and page faults per tick. Faults near zero mean the working set fits; a fault rate that jumps when RAM drops below the working set is the thrashing knee (Page Faults, Memory Pressure, Swap and the OOM Killer) — the same shape as a real system where vmstat starts showing si/so and %wa. The blocked panel lists threads waiting on I/O or a lock and how long they have waited; the lock contention panel shows, per lock, its wait-queue length and the fraction of time it is held. A lock held 90% of the time with a queue of seven is a serial section — Amdahl’s law made visible.
- Throughput = work completed per tick. It is the number every experiment is ultimately about.
- Utilisation without throughput is the classic trap: 100% busy cores that are switching, faulting or spinning.
Three guided experiments
1. Add threads until context switching dominates. Set 4 cores, 1 process, 4 threads, ample RAM, no I/O, no locks. Throughput should be near the maximum. Now raise threads to 8, 16, 64, 256. Predict first: throughput cannot rise beyond 4 cores’ worth; what will it do? Watch the switch overhead % climb and throughput bend downward as more of each slice is spent switching. Then shorten the time slice and watch the knee move left. The real-world version is a thread-per-connection server (Thread per Connection) at 10,000 connections, or a Node worker pool sized at os.cpus().length * 8.
2. Shrink RAM until page faults explode. Set 2 cores, 4 processes, RAM comfortably above the combined working set. Faults should be near zero after warm-up. Now shrink RAM in steps. Predict the knee: at what fraction of the working set does the fault rate leave zero? Observe that it is not gradual — LRU replacement keeps up until the working set no longer fits, then every access evicts something needed soon. Throughput falls off a cliff while cores show as busy-waiting on faults. That is the swap storm of memory-pressure-swap-storm, and why “add RAM” fixes some slow systems and does nothing for others.
3. Add one lock and watch contention. Set 8 cores, 8 threads, no I/O. Throughput ≈ 8×. Add one lock that each thread takes for 10% of its work. Predict: does throughput stay at 8×, drop to 1×, or land between? Watch the lock’s hold fraction approach 80% and the queue grow; throughput settles far below 8× because the 10% serial section caps the speed-up (Amdahl: 1/(0.1 + 0.9/8) ≈ 4.7×). Raise the locked fraction to 50% and cores go idle waiting. Then split it into two locks and see the queue halve. This is high-cpu-spin-lock and every “we added cores and nothing changed” story.
- Write the prediction down before running. The point of a simulator is to find out where your model of the OS is wrong.
- Combine: I/O plus few threads shows idle cores; I/O plus many threads shows the switching cost; I/O plus an event loop (one thread, non-blocking) is the The Event Loop argument.
Key points
- Every number in the playground is a simulation of the mechanisms in this domain; compare shapes and directions with real systems, not values.
- Cores bound parallelism; threads beyond cores add switching cost, not throughput.
- Page faults stay near zero until the working set exceeds RAM, then climb steeply — the thrashing knee.
- A single hot lock caps speed-up regardless of cores (Amdahl); the lock panel makes the serial section visible.
- Blocked threads with idle cores mean I/O-bound; long ready queues with busy cores mean CPU-bound.
- Predict, run, compare: the simulator is for finding where your mental model is wrong.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why simulate instead of measuring a real machine?
A real machine cannot be shrunk to 4 pages of RAM or told to pay exactly 3 µs per switch, and its numbers are entangled with caches, NUMA and background noise. A model isolates one mechanism so its curve is legible.
▸Why does adding threads eventually reduce throughput?
Each core can run one thread; extra runnable threads only change which thread runs, and each change costs a context switch plus cache warm-up. Past the core count, every added thread adds switching and no work.
▸Why is the page-fault curve a cliff rather than a slope?
With LRU-style replacement, as long as the working set fits every page is reused before eviction. The moment it does not fit, pages are evicted just before they are needed again, and nearly every access faults.
OS simulator
throughput = min(cores, runnable) × (1 − switch overhead − lock wait) × (1 − fault penalty)
How it fails
What the failure looks like from inside real software.
- Quoting a simulator number (“a context switch costs 3 µs”) as a fact about production; the simulator’s tag says
Simulatedfor this reason. - Tuning a thread pool by intuition instead of measurement: production shows 100% CPU and falling throughput — the switching regime from experiment 1.
- Provisioning RAM at exactly the working set: a small growth in data pushes the system over the knee from experiment 2 overnight.
- A coarse global lock added “for safety” caps a 32-core service at 3× — experiment 3 with real money attached.