Memory Systems · Profiling Memory

Reading a Process Footprint: RSS, PSS, and Shared Pages

Why resident set size double-counts shared pages, how proportional set size divides them between users, and which memory a process consumes that no per-process counter attributes to it.

The first measurement anyone takes of a process is its memory, and the first number anyone reaches for is resident set size. It is available from /proc, from getrusage, from every monitoring tool, and it is almost always the wrong number for the question being asked. RSS counts physical pages that a process currently has mapped and resident, with no regard for whether anyone else maps the same pages. Two processes running the same binary each report the binary’s text as resident. Sum the RSS of all processes on a machine and the total can exceed installed memory without anything being wrong.

What the resident set actually counts

The kernel maintains a per-process set of resident pages, and RSS is the size of that set in pages. Residency is a property of a page frame, not of a byte range: a page is either present in a page table entry or it is not. On the common 4 KiB page configuration the counter therefore moves in 4096-byte steps, and a program that touches one byte of a freshly mapped page is charged for the whole page. Anonymous memory is zero-filled on first touch, so a mapping of a gigabyte that is written sparsely shows up as a few hundred kilobytes of RSS rather than a gigabyte.

Three properties follow from the definition, and each is a source of misreading:

  • RSS tracks the page tables, not the allocator. Memory returned to free does not leave RSS. The pages remain mapped and resident until the allocator either returns them to the kernel with munmap or the kernel reclaims them under pressure. A program whose live set collapses after a peak keeps its RSS at the peak, which is a legitimate reason for the number to stay flat while the live set shrinks.
  • RSS includes file-backed pages. Text, read-only data, and any file mapped with mmap count once they are faulted in. A process that reads a large file with mmap and touches every page has an RSS dominated by page cache, which is reclaimable at any moment.
  • RSS excludes some real costs. Page tables themselves, kernel stacks, and most kernel-side structures allocated on behalf of the process are not in RSS. On a process with many mappings and transparent huge pages, the page-table hierarchy is not negligible, and it appears in no per-process resident counter.

getrusage reports the same quantity as ru_maxrss, the peak rather than the current value, and the unit differs by platform: kilobytes on Linux and FreeBSD, bytes on macOS. Code that treats the field as bytes and prints it as such is off by a factor of 1024 on Linux, which is a large enough error to hide a real regression.

Shared pages and the double-counting problem

The interesting complication is sharing. A page frame can be mapped into many address spaces: the text of a shared library, the page cache of a file read by several processes, a page inherited across fork and never written (copy-on-write), or a page explicitly shared through a shared mapping. RSS charges the full frame to every process that maps it.

For a single long-running process this does not matter much, because most of its pages are private. For a fleet of processes running the same image it matters enormously. A hundred worker processes each mapping a 20 MiB shared library report 2 GiB of RSS between them for 20 MiB of physical memory. Any capacity plan built on summed RSS over-provisions; any alarm built on summed RSS fires constantly.

The kernel exposes two derived counters that correct for this, both obtained by walking every virtual memory area of the process and classifying its pages. For each mapping the kernel knows the number of processes that currently map each resident page, so it can divide:

  • PSS, proportional set size, charges each shared page as size / number_of_mappers and each private page at full size. Summing PSS across all processes on the machine yields approximately the true physical footprint, because the divisors cancel.
  • USS, unique set size, counts only pages mapped by exactly one process. It is the memory that would be freed if this process exited, which makes it the right counter for deciding what a process is worth killing.

Neither is free to compute. RSS is a maintained counter; PSS and USS require iterating over the process’s mappings and, for each, over its resident pages, which costs time proportional to the number of mappings and their size. On a process with tens of thousands of small mappings the walk is slow enough that reading it in a tight polling loop changes what is being measured. The kernel provides a rolled-up summary file that aggregates the same fields over all mappings without printing per-mapping detail, which reduces the output volume but not the walk itself.

Reading the fields of a mapping summary

The per-mapping summary is the most useful interface because it separates the components that RSS conflates. Each mapping is reported with its address range, permission bits, backing file or [anon]/[heap]/[stack] label, and a set of counters: total size, resident, shared and private clean and dirty pages, and the proportional share of each. A short parser over that output produces a breakdown by category in a few dozen lines.

# Read the rolled-up mapping summary and print the footprint counters it aggregates.
import sys

path = f"/proc/{sys.argv[1]}/smaps_rollup" if len(sys.argv) > 1 else "/proc/self/smaps_rollup"
fields = {}
for line in open(path):
    key, _, rest = line.partition(":")
    if rest.strip().endswith("kB"):
        fields[key] = int(rest.split()[0])

# Every value is in kilobytes; Pss divides each shared page among its mappers.
print(f"rss={fields.get('Rss', 0)} kB  pss={fields.get('Pss', 0)} kB")
print(f"anon={fields.get('Anonymous', 0)} kB  swap={fields.get('Swap', 0)} kB")

The rolled-up file reports totals only. The per-mapping file reports the same fields for each area, which is what turns a footprint number into an attribution: [heap] growing while the file-backed mappings stay flat points at allocation, whereas a single anonymous mapping growing in fixed steps points at a region allocator or a thread stack.

What never appears in any of these numbers

Several categories of memory a process causes to be consumed are invisible to all three counters.

Kernel memory charged on the process’s behalf. Page tables, kernel stacks, io_uring rings, and file descriptors are allocated by the kernel when the process asks for them and are not mapped into the process address space. They appear in slab and page-table counters system-wide, not in RSS.

Copy-on-write descendants. After fork, parent and child share every private page until one of them writes. The page is then copied, and the writer is charged the new frame. A forked worker therefore shows a footprint close to its parent’s at the moment of the fork and diverges only as it writes. A profiler that samples a freshly forked child sees a number that reflects the parent’s work.

Memory that has been swapped out. RSS counts resident pages. A process whose anonymous memory has been written to swap has a smaller RSS than live set, sometimes dramatically so. Swap-in is what restores the number, which is why RSS measured on an idle machine and RSS measured under memory pressure are not comparable.

Huge pages. With transparent huge pages, residency is charged in 2 MiB units on the common configuration. A single touch can fault in a 2 MiB page, so RSS jumps by that amount for a workload that would otherwise have touched a few kilobytes. The jump is real memory, but it is a fault behaviour, not an allocation.

The practical consequence is that footprint questions need to be stated precisely before they can be answered. “How much memory does this process use” has at least four answers (resident, proportional, unique, and the amount that would be released under pressure), and they differ by factors, not percentages, in any program with shared mappings or a large page cache.

Choosing a counter for the question

The choice follows from the decision being made. For a single process on an otherwise idle machine, RSS is adequate and cheap, provided the reader remembers that it does not fall when memory is freed. For capacity planning across many processes, PSS is the only counter whose sum is meaningful. For a supervisor deciding which of several processes to evict, USS is the right input, since it approximates the memory reclaimed by termination. For a leak investigation, none of them is the primary tool: a leak is a property of reachability, not residency, and the distinction between retained and resident memory is what reachability-based leak detection is built on.

A second caution applies to all of them: they are snapshots of a system that is concurrently changing. RSS sampled while a program is starting includes pages that will be evicted as the startup path finishes; RSS sampled after a garbage collection includes pages the collector has freed but not returned. The counters are correct at the instant they are read, and misleading when treated as a property of the program. The components that RSS aggregates are separable, anonymous from file-backed and private from shared, and the separation is what splitting footprint by mapping type covers in detail.

References

  1. Linux man-pages: proc(5) — Linux man-pages project
  2. Linux man-pages: getrusage(2) — Linux man-pages project
  3. What Every Programmer Should Know About Memory — Ulrich Drepper, 2007