📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

πŸ“š Interview question

What is a page fault? Explain soft (minor) vs hard (major) page faults, and how you would investigate a .NET service whose latency spikes line up with a high page fault rate.

Page faults, minor vs major faults, what triggers them, and how to diagnose paging-related latency in a .NET service.

Senior Debugging Advanced linux operating-system page-fault performance virtual-memory windows

🎯 Key points

  • A page fault is a CPU trap raised when a virtual page is not mapped (or not mapped with the needed permission) for the access; the kernel resolves it and the instruction retries.
  • Minor (soft) fault: the page is already in RAM (page cache, shared library, zero page, copy-on-write) and only the page table changes, costing roughly microseconds.
  • Major (hard) fault: the page is read from swap, the page file or a mapped file. It blocks the faulting thread for the whole I/O, about 0.1 ms on idle NVMe and tens of milliseconds to seconds on a busy or network disk.
  • An access to an address with no valid mapping becomes an access violation or SIGSEGV, which is a different thing from a normal page fault.
  • Demand paging means the first touch of newly committed memory causes minor faults, so high minor-fault counts at startup or after large allocations are normal.
  • Major faults hurt .NET badly because a gen2 GC walks the whole heap; swapped-out heap pages turn a short pause into seconds and stall every thread waiting on that GC.
  • Diagnose by correlating latency with major faults: vmstat, sar -B, pidstat -r and /proc/pressure/memory on Linux, Hard Faults/sec on Windows, plus dotnet-counters and dotnet-trace GC events for heap size and pause time.
  • Fix the cause, not the symptom: reduce co-located memory use, set container memory limits, disable swap for latency-sensitive nodes, stream instead of loading whole files, and consider large pages only for very large heaps after measuring TLB misses.

⏱ 30-second answer

A page fault is a CPU exception raised when code touches a virtual address whose page is not currently mapped to physical RAM for that process. The kernel handles it:

  • Soft (minor) fault: the page is already in RAM (file cache, shared library, a freshly zeroed page, copy-on-write) and only the page table needs updating. Costs microseconds.
  • Hard (major) fault: the page must be read from disk (swap file, page file or a memory-mapped file). Costs milliseconds, around 1,000 times slower.
  • Invalid access: no mapping at all, so the process gets an access violation / SIGSEGV.

Minor faults are normal. A burst of major faults means the working set does not fit in RAM, and latency spikes follow.

πŸ—£ Full answer (senior)

What actually happens

Every memory access goes through the MMU, which translates a virtual address using the page tables (with the TLB as a cache). If the page table entry is not present or does not allow the access, the CPU raises a page fault and the kernel decides what to do.

Fault type Where the data is Typical cost Common causes
Minor (soft) Already in RAM ~0.1–10 Β΅s First touch of newly allocated memory, shared DLL/.so pages, file cache, copy-on-write after fork
Major (hard) On disk ~0.1 ms (NVMe) to 10 ms (HDD, network disk) Swapped-out pages, page file, first read of a memory-mapped file, binaries paged back in
Invalid Nowhere Process crash Null or wild pointer, guard page (stack overflow)

Note that Windows Task Manager reports "page faults" that include soft faults, so a big number there is not by itself a problem. On Linux ps -o min_flt,maj_flt separates them.

Production example

A .NET 10 API on a VM had p99 latency jump from 40 ms to 900 ms every few minutes. CPU was low. vmstat 1 showed si/so (swap in/out) non-zero and sar -B showed majflt/s spiking at the same time as the latency. A batch job on the same VM was loading a 6 GB file into memory and pushing the API's heap pages to swap. When a GC ran, it had to walk the whole heap, which faulted the swapped pages back in one by one.

Fix: move the batch job off the box, set a memory limit for each workload (cgroups/containers), and in Kubernetes keep swap off for latency-sensitive pods. After that, major faults went to almost zero and p99 returned to normal.

How I investigate

  1. Confirm the correlation: sar -B 1 / vmstat 1 (Linux) or the Memory\Page Reads/sec and Memory\Pages Input/sec counters (Windows), against the latency graph.
  2. Find the process: pidstat -r 1 shows majflt/s per process.
  3. Check memory pressure: free -m, /proc/pressure/memory (PSI), container memory limit vs working set.
  4. Check what the .NET runtime sees: dotnet-counters monitor -n MyApi System.Runtime for working set, GC heap size and GC pause time.
  5. Fix the cause: more RAM or fewer co-located workloads, a smaller heap (e.g. GCHeapHardLimit, DATAS / Workstation GC for small containers), or streaming data instead of loading it all at once.

πŸ” What the interviewer is checking

Checks that the candidate understands virtual memory beyond buzzwords and can connect an OS mechanism to a real latency symptom.

Green flags

  • Clearly separates minor, major and invalid faults, with rough costs.
  • Mentions demand paging and first touch.
  • Uses real tools (vmstat, sar -B, pidstat, PerfMon) and correlates with latency.
  • Links GC pauses with swapped-out heap pages.

Red flags

  • Says page faults are errors or crashes.
  • Cannot explain why a page might not be in RAM.
  • Jumps straight to GC tuning without checking memory pressure.

πŸ“š Learn in depth

1. Virtual memory recap

Each process sees its own virtual address space split into pages (usually 4 KiB on x64; 4 KiB or 16 KiB on Arm64, and 2 MiB / 1 GiB huge pages). The kernel keeps a page table that maps virtual pages to physical frames.

Virtual address ──► TLB hit? ──yes──► physical address ──► cache/RAM
                      β”‚
                      no
                      β–Ό
                 page table walk
                      β”‚
         entry present? ──no──► PAGE FAULT (trap to kernel)

2. What the kernel does on a fault

PAGE FAULT
   β”‚
   β”œβ”€ address not in any mapping ──────────► SIGSEGV / access violation
   β”‚
   β”œβ”€ page in RAM already (page cache,
   β”‚  shared lib, zero page, COW) ─────────► MINOR fault: fix page table, resume
   β”‚
   └─ page on disk (swap, page file,
      mmap'd file) ───────────────────────► MAJOR fault: block thread,
                                              issue disk I/O, map page, resume

The thread that faulted is blocked for the whole disk read. If that thread holds a lock or is the GC thread, everyone waits.

3. Why minor faults are everywhere

  • new byte[100_000_000] reserves and commits memory, but Linux only maps a physical page when you first touch it (demand paging). Every first write to a fresh 4 KiB page is a minor fault.
  • After fork, parent and child share pages until one writes (copy-on-write).
  • Loading System.Private.CoreLib.dll maps it; pages fault in as code runs.

This is why startup shows tens of thousands of minor faults. That is fine.

4. Why major faults hurt .NET in particular

The GC marks by walking object references across the heap. If parts of the heap were swapped out, a gen2 GC touches them all, triggering thousands of major faults in a row. A 50 ms GC becomes a multi-second pause. This is why many teams disable swap for latency-sensitive services, and why Kubernetes historically required swap to be off.

5. Memory-mapped files

MemoryMappedFile maps a file into the address space. Reading it causes major faults the first time (data comes from disk), then minor faults or none once it is in the page cache. This is often faster than FileStream for random access to large files, because the OS handles caching.

6. Huge pages and the TLB

The TLB holds a limited number of translations. With 4 KiB pages, a 32 GB heap needs ~8 million entries, so TLB misses are frequent. Huge pages (2 MiB) cut that by 512 times. .NET supports large pages for the GC heap (GCLargePages with GCHeapHardLimit), mostly useful for very large server heaps.

7. Tools

OS Tool What it shows
Linux ps -o pid,min_flt,maj_flt,cmd Totals per process
Linux pidstat -r 1 minflt/s, majflt/s per process
Linux sar -B 1, vmstat 1 System-wide paging, swap in/out
Linux perf stat -e page-faults,major-faults Counts for one command
Windows PerfMon Memory\Pages/sec, Page Reads/sec Hard faults
Windows Resource Monitor β†’ Memory β†’ Hard Faults/sec Per process
Both dotnet-counters Working set, GC heap, pause time

8. Try it yourself

  1. Write a console app that allocates new byte[1_000_000_000] and then writes one byte every 4096 bytes.
  2. Run it with /usr/bin/time -v dotnet run and look at Minor (reclaiming a frame) page faults: about 244,000 (1 GB / 4 KiB).
  3. Run it again but only allocate, no writes. Minor faults drop sharply, because untouched pages are never mapped.
  4. On a test VM with swap, start a process that eats most of RAM, then rerun step 2 and watch Major faults climb.

πŸ’» Code

// Demonstrates demand paging: memory is only mapped when first touched.
using System.Diagnostics;

const int PageSize = 4096;
var buffer = GC.AllocateUninitializedArray<byte>(512 * 1024 * 1024); // 512 MiB

var before = Process.GetCurrentProcess();
Console.WriteLine($"Working set before touch: {before.WorkingSet64 / 1024 / 1024} MiB");

var sw = Stopwatch.StartNew();
for (int i = 0; i < buffer.Length; i += PageSize)
{
    buffer[i] = 1; // first write to each page = one minor page fault
}
sw.Stop();

var after = Process.GetCurrentProcess();
Console.WriteLine($"Working set after touch:  {after.WorkingSet64 / 1024 / 1024} MiB");
Console.WriteLine($"Touching {buffer.Length / PageSize:N0} pages took {sw.ElapsedMilliseconds} ms");
# Linux: count minor and major faults for the run
/usr/bin/time -v dotnet run -c Release 2>&1 | grep -i "page faults"

# Watch faults for a running service once a second
pidstat -r -p $(pgrep -f MyApi) 1

# System-wide: majflt/s and swap activity
sar -B 1
vmstat 1

⚠ Common mistakes

Mistake: Treating every page fault as a problem. Why it is wrong: Task Manager and many dashboards count soft faults, which happen constantly on first touch, copy-on-write and shared libraries and cost only microseconds. Instead: Look at major (hard) faults per second and check whether they line up with the latency spikes.

Mistake: Thinking a page fault means a crash. Why it is wrong: Most page faults are resolved silently by the kernel and the code carries on. Instead: Explain that only a fault on an address with no valid mapping, or a protection violation, becomes an access violation or SIGSEGV.

Mistake: Quoting a fixed cost for a major fault. Why it is wrong: The cost depends on the device and on queueing; a fault that takes 0.1 ms on an idle NVMe drive can take hundreds of milliseconds on a saturated network disk while other I/O is in flight. Instead: Measure it with disk latency (iostat -x, await) alongside the fault rate.

Mistake: Blaming the GC for long pauses without checking paging. Why it is wrong: The GC is often slow because it is faulting swapped heap pages back in, so GC settings will not fix it. Instead: Check majflt/s and swap in/out during the pause before you tune GC settings.

Mistake: Ignoring other workloads on the same host. Why it is wrong: Another process can push your pages out even when your own service has not changed. Instead: Use per-process tools such as pidstat -r, and give each workload its own memory limit.

πŸ› Senior insight: trade-offs

The senior point is that major faults turn a memory problem into a latency problem, and the cost lands on whichever thread touches the page, often the GC or a thread holding a lock. So the fix is usually about capacity and isolation rather than micro-tuning code.

Option Benefit Cost or risk When to use it
Disable swap on service nodes No paging stalls, predictable latency Memory spikes become OOM kills Latency-sensitive APIs, Kubernetes
Container memory limits One workload cannot evict another Needs accurate sizing and headroom Any shared host
Smaller GC heap (GCHeapHardLimit, DATAS) Lower working set More frequent GCs Small containers, many replicas
Large pages (GCLargePages) Fewer TLB misses on big heaps Needs a hard limit, contiguous memory, privilege on Windows Very large server heaps, after measuring
Memory-mapped files OS manages caching, fast random access Cold-cache reads become major faults Large read-mostly data

Monitoring is part of the answer. I would have a dashboard with p99 latency, GC pause time and heap size from dotnet-counters or OpenTelemetry runtime metrics, next to node-level major faults, swap in/out and memory pressure (PSI). With dotnet-trace I can confirm which GCs were long, and with perf or PerfView I can see whether the time was in the kernel fault handler. That combination proves cause rather than guessing.

πŸ”— Follow-ups the interviewer may ask

  1. When: Candidate is unclear on how virtual addresses map to RAM Why: Checks the underlying paging model
    How to answer

    Virtual memory gives every process its own large, private address space, so programs don't see each other's memory or need to know where physical RAM is. Memory is managed in pages (usually 4 KB). The OS keeps a page table per process that maps virtual pages to physical frames; the CPU's MMU translates every access, and the TLB caches recent translations. Pages get physical memory only when first touched (demand paging), can be shared (shared libraries, copy-on-write), can be backed by files (memory-mapped files), and can be pushed to swap or the page file under pressure. For .NET this explains why reserved and committed memory differ, why the GC reserves big ranges cheaply, and why working set is what really matters.

  2. When: Candidate mentions GC walking the heap Why: Probes managed heap layout and generations
    How to answer

    Each thread allocates from its own allocation context, a small chunk of gen 0 where new is just a pointer bump plus zeroing; when it runs out, the thread asks the GC for another chunk, and when the gen 0 budget is used up, a gen 0 GC runs. The GC is generational because most objects die young: gen 0 collections are cheap, survivors are promoted to gen 1 and then gen 2, which is collected rarely (and mostly in the background). Objects of 85,000 bytes or more go to the large object heap (LOH), which is collected only with gen 2 and isn't compacted by default, so it can fragment. Pinned objects can't be moved, which blocks compaction; the pinned object heap (.NET 5+) keeps long-lived pinned buffers separate. In hot paths I reduce the allocation rate rather than tuning the GC.

  3. When: Candidate mentions memory pressure or swap Why: Connects to working set, container limits and OOMKilled
    How to answer

    RAM is physical memory. Each process gets a virtual address space that the OS maps onto RAM page by page. The working set is what's resident in RAM right now; private (committed) memory is what the process owns. The GC heap is only one part: native allocations (native libraries, NativeMemory, unmanaged buffers), thread stacks, JIT code, and runtime structures add to it. In Kubernetes, the cgroup limit counts all of it, and going over gets the container OOMKilled (exit code 137), not an OutOfMemoryException. So if the GC heap is small but the pod dies, I compare GC heap size with working set in dotnet-counters. A big gap points to native memory or too many threads; a growing GC heap points to a managed leak I'd chase with dotnet-gcdump. Then I fix the cause and set limits and GC settings deliberately.

πŸ›  Real-world problems: how you handle them

GC pauses of several seconds caused by swapped-out heap pages
Situation
[Order API] on .NET [10] running on a shared [16 GB] VM next to a nightly [report batch job]. p99 latency jumped from [40 ms] to [900 ms+] in bursts.
Signals
Latency spikes every few minutes, CPU low, GC pause time spiking in dotnet-counters, `vmstat` showing non-zero `si/so`, `sar -B` majflt/s in the [thousands].
Investigation
Correlated the latency graph with `sar -B 1` and `vmstat 1`. Used `pidstat -r 1` to see the API had high majflt/s while the batch job's RSS was growing. Checked `free -m`: swap use [3 GB]. GC pause events lined up with the fault spikes.
Resolution
Immediate: rescheduled the batch job onto a separate node. Long term: moved both to containers with memory limits, disabled swap on API nodes, and changed the batch job to stream the file instead of loading it whole.
Prevention
Alert on major faults per second and swap usage per node, dashboards with GC pause time next to paging metrics, capacity review before co-locating workloads.

How to tell it (STAR)

**S:** Our [order API] had latency spikes nobody could explain because CPU looked fine. **T:** I was asked to find the cause before [peak trading]. **A:** I put the latency graph next to `sar -B` and saw major page faults spiking at the same moments. `pidstat` showed the API faulting while a [batch job] on the same VM grew to [12 GB]. The GC was walking heap pages that had been swapped out. We moved the batch job, set memory limits and disabled swap for the API nodes. **R:** p99 dropped back to [45 ms] and stayed there, and we added a paging alert so we would see it first next time.

↩ Usually asked after

More in Operating System

Last updated 23 Oct 2026.

🌐 Sangam's Webring

βͺ Previous 🏠 Main Next ⏩
πŸ’¬ Chat with me