$RodHat_
Console Tips

ps lies about memory. /proc/smaps_rollup does not.

Published by

ps lies about memory. /proc/smaps_rollup does not.
Photo: AI-generated — no human photographer / RodHat AI Cover

Here is a thing that happens regularly: someone looks at ps aux, sees a process using 800 MB of RSS, panics, adds more RAM, and the problem doesn’t change. Or they add up all the RSS values for their processes, get a number larger than the physical RAM installed, and think something is badly wrong.

Neither of these people is stupid. The RSS column in ps is just wrong. Not buggy-wrong — wrong by design, because it’s measuring the wrong thing. Every shared page (libc, libssl, every shared library your process loaded) is counted in full for every process that maps it. Fifty processes all linked against the same libc get fifty full copies of libc’s pages charged to their RSS. The actual physical cost is one copy.

The correct number is PSS: Proportional Set Size. It’s been in the kernel since 2.6.25. It’s in /proc/<pid>/smaps_rollup. Almost nobody reads it.

What the kernel is tracking

Every process has a set of virtual memory areas (VMAs). Each VMA has a type and a backing:

  • Anonymous: heap, stack, mmap(MAP_ANONYMOUS) — no file backing, this is “your” memory
  • File-backed: shared libraries, mmap’d files, executable text segment
  • Shared: mapped with MAP_SHARED, potentially shared with other processes

For each page in each VMA, the kernel knows how many processes have it mapped. PSS takes each page’s size and divides by the number of processes sharing it, then sums across all pages. A page shared by four processes contributes page_size / 4 to each process’s PSS. A private anonymous page contributes its full size.

The result: sum PSS across all your processes and you get a number that closely approximates actual physical RAM consumed. Do the same with RSS and you get a meaningless overcount.

Reading smaps_rollup

cat /proc/<pid>/smaps_rollup

Output looks like this:

00400000-7ffdc0000000 ---p 00000000 00:00 0                [rollup]
Rss:              184320 kB
Pss:               31408 kB
Pss_Anon:          28160 kB
Pss_File:           3248 kB
Pss_Shmem:             0 kB
Shared_Clean:     156160 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:     28160 kB
Referenced:       184320 kB
Anonymous:         28160 kB
LazyFree:              0 kB
AnonHugePages:         0 kB
ShmemPmdMapped:        0 kB
FilePmdMapped:         0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
SwapPss:               0 kB
Locked:                0 kB

The fields that matter:

Rss — what ps shows you. This process has 184 MB of resident pages, counting shared pages at full cost. Useless for comparative analysis.

Pss — 31 MB. This is the process’s proportional share of physical RAM. 153 MB of the RSS was shared library pages that are also charged to other processes; PSS gives you your actual slice.

Pss_Anon — your private anonymous memory: heap allocations, stack, anonymous mmaps. This is “your” memory in the sense that no other process shares it. If this number is large and growing, you have a heap growth problem.

Pss_File — your proportional share of file-backed mappings (shared libraries, mmap’d files). A process that has loaded forty shared libraries will have a larger Pss_File than one that statically linked everything, even if the actual unique work they do is similar.

Shared_Clean — shared pages that haven’t been written to since the last page fault. Mostly executable text from shared libraries. These pages can be evicted by the kernel with no cost (they can be reloaded from disk). Not “your” problem in an OOM situation.

Shared_Dirty — shared pages that have been written to. mmap(MAP_SHARED) writable mappings, for instance. These cannot be evicted without writing to disk. More concerning than Shared_Clean.

Private_Dirty — private pages that have been written to. Heap, stack, BSS. These cannot be evicted without swap. This is where your actual RAM pressure lives.

Anonymous — total anonymous (heap + stack + MAP_ANONYMOUS) pages. Should be close to Pss_Anon for most processes.

USS: the paranoid number

USS (Unique Set Size) isn’t directly in smaps_rollup but you can derive it: Private_Clean + Private_Dirty. This is pages that are exclusively yours — shared with no other process. If you kill this process, this is roughly how much RAM the system gets back.

awk '/Private_/{sum += $2} END {print sum " kB"}' /proc/<pid>/smaps_rollup

USS is useful for OOM analysis: which process, if killed, frees the most unique memory? PSS is useful for capacity planning: what is the realistic total RAM cost of running N of these?

Scripting it: PSS for all processes

#!/bin/sh
# PSS for every process, sorted descending
for pid in /proc/[0-9]*/smaps_rollup; do
    p=${pid%/smaps_rollup}
    p=${p##/proc/}
    pss=$(awk '/^Pss:/{sum+=$2} END{print sum}' "$pid" 2>/dev/null)
    comm=$(cat /proc/$p/comm 2>/dev/null)
    [ -n "$pss" ] && printf "%8d kB  %s (%s)\n" "$pss" "$comm" "$p"
done | sort -rn | head -20

This gives you a ranked list of actual RAM consumers, proportionally attributed. Run this instead of ps aux --sort=-%mem and you will see a different ordering. Processes that load many shared libraries look cheaper; processes with large private heaps look more expensive. Both are more accurate.

# Total PSS across all processes (rough system RAM accounting)
awk '/^Pss:/{sum+=$2} END{print sum/1024 " MB"}' /proc/*/smaps_rollup 2>/dev/null

This should come out close to your actual used RAM (from free -m). RSS summed across all processes would be wildly higher.

Per-region breakdown: full smaps

smaps_rollup aggregates all VMAs. If you need per-region breakdown (which library is costing the most, where the anonymous heap segments are, what’s swapped), read /proc/<pid>/smaps directly:

cat /proc/<pid>/smaps

Each VMA gets its own block with the same fields. The file can be large (hundreds of entries for a process with many loaded libraries), but you can grep it:

# Find the heap
grep -A 20 '\[heap\]' /proc/<pid>/smaps

# Find all anonymous segments sorted by size
awk '/^[0-9a-f].*---/{vma=$0} /^Private_Dirty:/{print $2, vma}' /proc/<pid>/smaps | sort -rn | head -20

When does this actually matter

Most of the time you are debugging the wrong thing. If you have a process you suspect is leaking memory, watch Pss_Anon over time — that is heap growth. If it grows monotonically and doesn’t come back down, you have a leak. RSS will also grow, but less cleanly, because shared library pages get paged in and out based on access patterns and will cause noise.

For capacity planning (“how many instances of this service can I run on this box”), use PSS. RSS gives you the worst-case overcount because it ignores sharing. The real answer is somewhere between USS (best case, zero sharing) and RSS (worst case, no sharing). PSS splits the difference correctly.

The OOM killer uses its own accounting (/proc/<pid>/oom_score_adj, /proc/<pid>/oom_score), which approximates RSS with adjustments — it is not PSS-based, so the process the OOM killer kills is not necessarily the one with the highest PSS. If you are tuning OOM behavior, read oom_score and adjust oom_score_adj explicitly, do not assume the highest-PSS process will be killed first.


The full details on Linux memory management — VMAs, page tables, anonymous vs file-backed mappings, TLB behavior — are in Kerrisk’s The Linux Programming Interface, Chapter 49 (memory mappings) and Chapter 50 (virtual memory operations). For the kernel internals side, the Mel Gorman document “Understanding the Linux Virtual Memory Manager” is old but still accurate on the structural pieces. Stevens’ Advanced Programming in the UNIX Environment covers the process address space model in Chapter 7 if you want the POSIX-layer view before going into Linux specifics.