Writing
فارسیWhy virtual memory matters to a backend service
Address spaces, page tables, page faults, and RSS versus VSZ: the memory model behind the dashboards, and why a process can be killed while the host looks free.
A container has a 512 MiB limit. Its main process reports 430 MiB of RSS, the node has tens of gigabytes free, and the kernel still kills the process. The numbers on the dashboard are not lying, but they are answering a different question from the one you think you are asking.
The address space is a map
A process does not receive a contiguous block of physical memory. It receives a virtual address space, and the kernel maintains a translation from virtual pages to physical frames. The layout is familiar: text for code, data for initialized globals, BSS for zero-initialized globals, a heap that grows with allocation, a region for mmap (shared libraries, file mappings, large allocations), and a stack per thread.
Page tables and the TLB
On x86-64, translation walks a multi-level page table, with each level indexed by a slice of the virtual address. A leaf entry holds the physical frame number and flags such as present, writable, and executable. Walking several levels costs several memory accesses, so the CPU caches recent translations in the TLB. A TLB miss is not a page fault, but it is not free either; pointer-heavy workloads with poor locality pay for both.
Page faults are normal
A page fault happens when the CPU cannot resolve a translation. A minor fault means the page is already in memory but the mapping or permissions need work, as with copy-on-write after fork. A major fault means the page must be read from disk or swap. Demand paging means a freshly allocated page is often not backed until the first write, so a large allocation can succeed and then fault page by page as it is touched.
| Metric | What it counts | Where it misleads |
|---|---|---|
| VSZ | Virtual size of all mappings | Includes reserved and untouched pages |
| RSS | Resident pages in physical memory | Double-counts shared libraries across processes |
| PSS | RSS with shared pages split by sharers | Slower to collect, less familiar |
| Page cache | File-backed pages cached by the kernel | Not part of a process’s RSS |
mmap, malloc, and the OOM killer
malloc does not always return memory from the heap. glibc serves small allocations from arenas and uses mmap for large ones above a dynamic threshold. File-backed mappings let the page cache and the process share pages; anonymous mappings back the heap and thread stacks. MADV_DONTNEED tells the kernel that pages can be discarded, while MADV_FREE is lazier about reclaiming them. Transparent huge pages reduce fault and TLB overhead but can increase memory footprint and tail latency when the kernel compacts memory.
The OOM killer is the kernel’s last resort when reclaim cannot satisfy an allocation. Under cgroup limits it selects a victim within the group. A process can be killed for touching memory it mapped long ago, which is why a slow leak may look like a sudden death after a deploy that changed traffic patterns.
#define _GNU_SOURCE
#include <stddef.h>
#include <sys/mman.h>
int main(void) {
size_t len = 64UL * 1024 * 1024;
char *p = mmap(NULL, len, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED) return 1;
for (size_t i = 0; i < len; i += 4096) {
p[i] = 1; /* first write faults in one 4 KiB page */
}
munmap(p, len);
return 0;
}
grep -E 'Vm(Size|RSS|Swap)' /proc/$PID/status
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.current
What changes under load
Memory becomes a latency problem before it becomes an OOM problem. A process that keeps faulting pages in competes with the page cache and with other tenants for bandwidth. Allocators that retain freed memory keep RSS high even when the application’s live set is small. Under a cgroup limit, reclaim scans and direct reclaim can slow allocations without any failure, so p99 latency rises while CPU utilization looks ordinary.
What to carry forward
Treat VSZ, RSS, PSS, and cgroup usage as four different measurements. Map the address space before tuning anything: which regions are file-backed, which are anonymous, and which grow per request. Then choose a fix that matches the fault pattern, whether that is fewer allocations, a bounded cache, or a deliberate madvise policy. Huge pages and MADV_* flags are tunables with trade-offs, not defaults to copy from a benchmark post.