/proc Internals

/proc is a virtual filesystem — nothing on disk, generated by the kernel on read. Every read returns live kernel data.

Each section below ends with a quick knowledge check — see how many you can answer before revealing:

0/0 checks

/proc/[pid]/maps — Virtual Address Space

cat /proc/1234/maps
55a3c1200000-55a3c1201000 r--p 00000000 08:01 123456  /usr/bin/myapp
55a3c1201000-55a3c1220000 r-xp 00001000 08:01 123456  /usr/bin/myapp
55a3c1220000-55a3c1228000 r--p 00020000 08:01 123456  /usr/bin/myapp
55a3c1228000-55a3c122a000 rw-p 00028000 08:01 123456  /usr/bin/myapp
55a3c3000000-55a3c3021000 rw-p 00000000 00:00 0       [heap]
7f8b12000000-7f8b14000000 rw-p 00000000 00:00 0       [anon]
7fff9a000000-7fff9a021000 rw-p 00000000 00:00 0       [stack]
7fff9a1fe000-7fff9a202000 r--p 00000000 00:00 0       [vvar]
7fff9a202000-7fff9a204000 r-xp 00000000 00:00 0       [vdso]

Column breakdown:

Column Meaning
55a3c1200000-55a3c1201000 Virtual address range (start-end)
r--p Permissions: read/write/execute + private(p) or shared(s)
00000000 File offset (where in the file this mapping starts)
08:01 Device (major:minor)
123456 Inode number
/usr/bin/myapp Backing file (empty = anonymous)

Permission flags:

  • r read, w write, x execute, - not set
  • p private (copy-on-write), s shared

Named regions:

  • [heap]malloc arena
  • [stack] — main thread stack
  • [anon] — anonymous mappings (goroutine stacks, JVM heap, mmap'd allocations)
  • [vdso] — virtual dynamic shared object (kernel exposes clock_gettime here to avoid syscall overhead)
# Count memory regions
wc -l /proc/1234/maps

# Find all shared libraries loaded
grep "\.so" /proc/1234/maps | awk '{print $6}' | sort -u

# Find heap size
grep "\[heap\]" /proc/1234/maps

# Find all anonymous mappings (goroutine stacks, JVM heap)
grep "00:00 0" /proc/1234/maps | grep -v "\["

A region in /proc/[pid]/maps shows device/inode 00:00 0, no backing file, and it isn't labeled [heap] or [stack]. What is it?


/proc/[pid]/smaps — Per-Mapping Memory Stats

smaps is maps with detailed RSS/PSS/swap breakdown per region.

cat /proc/1234/smaps
55a3c1201000-55a3c1220000 r-xp 00001000 08:01 123456  /usr/bin/myapp
Size:                124 kB    ← virtual size of this mapping
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                  96 kB    ← pages in physical RAM
Pss:                  48 kB    ← proportional share (RSS / # processes sharing)
Shared_Clean:         96 kB    ← shared + unmodified (can be discarded)
Shared_Dirty:          0 kB    ← shared + modified (must be kept)
Private_Clean:         0 kB    ← private + unmodified
Private_Dirty:         0 kB    ← private + modified (your actual footprint)
Referenced:           96 kB    ← recently accessed
Anonymous:             0 kB    ← not file-backed
Swap:                  0 kB    ← pages swapped out

Key metrics:

Field Use
Rss Physical RAM used by this mapping
Pss Fairer than RSS — shared pages divided by sharing count
Private_Dirty Your process's actual unique memory footprint
Swap Bytes of this mapping currently on swap

Same four numbers, one at a time — flip through them:

Resident Set Size. How much of this mapping currently sits in physical RAM, full stop. If the mapping is a shared library, every process that maps it counts the same physical pages toward its own RSS — so summing RSS across processes overcounts actual memory used.
Proportional Set Size. RSS divided by however many processes share those pages. A page shared by 4 processes contributes 1/4 of its size to each process's PSS. Summing PSS across every process sharing a page adds back up to that page's real physical cost exactly once — which is why PSS, not RSS, is the fair number for "how much RAM is this thing actually using system-wide."
Private, modified pages. Pages that belong to this process alone and have been written to since they were mapped — never shared, never reclaimable by dropping a clean cache. This is the closest single number to "what would I get back if this process exited right now."
Paged out to disk. Bytes of this mapping the kernel decided to evict from RAM under memory pressure. Non-zero and growing means the system is short on physical memory, not just this process — touching that memory again means paying a disk-read fault to bring it back.

The sample above shows Rss: 96 kB but Pss: 48 kB for the same mapping. What does that gap tell you, and why would summing every process's RSS for this shared library overcount total memory used?

# Summarized rollup (fastest)
cat /proc/1234/smaps_rollup
# Rss:              45632 kB
# Pss:              32768 kB
# Private_Dirty:    28672 kB
# Swap:                 0 kB

# Total swap used by process
awk '/Swap:/ {sum += $2} END {print sum " kB"}' /proc/1234/smaps

# Largest anonymous mappings (heap, goroutine stacks, JIT)
awk '/^[0-9a-f]/{map=$0} /Anonymous:/{print $2, map}' /proc/1234/smaps \
    | sort -rn | head -10

/proc/[pid]/status — Process Summary

cat /proc/1234/status
Name:    myapp
State:   S (sleeping)
Pid:     1234
PPid:    1000
Threads: 8
VmPeak:  512000 kB    ← peak virtual memory ever used
VmSize:  480000 kB    ← current virtual memory
VmRSS:   45632 kB     ← current RSS (physical RAM)
VmData:  32768 kB     ← heap + BSS (data segment)
VmStk:   132 kB       ← main thread stack
VmExe:   124 kB       ← text segment (code)
VmLib:   8192 kB      ← shared libraries
RssAnon: 28672 kB     ← anonymous RSS (heap, stacks)
RssFile: 16960 kB     ← file-backed RSS (code, mmapped files)
SigBlk:  0000000000000000   ← blocked signals (bitmask)
SigIgn:  0000000000001000   ← ignored signals
SigCgt:  0000000180000000   ← caught signals (has handler)
voluntary_ctxt_switches:    1234   ← gave up CPU voluntarily
nonvoluntary_ctxt_switches: 56     ← preempted by kernel

Three separate 64-bit bitmasks, one bit per signal number, all present at once for every process — not states it moves between:

Blocked. Signals currently masked out — the process has told the kernel to hold delivery until it explicitly unblocks them. A blocked signal isn't dropped; for a regular (non-realtime) signal, at most one instance stays pending until the mask lifts.
Ignored. Signals set to be discarded outright — delivered by the kernel, then thrown away with no handler running. A process ignoring SIGPIPE so a write to a closed pipe doesn't kill it is the classic example.
Caught. Signals with a handler registered — instead of the kernel's default action (terminate, dump core, stop, ignore), the process's own code runs when one of these arrives.

What to look for:

Field If high/growing Likely cause
VmRSS growing Memory leak Use smaps to find growing region
nonvoluntary_ctxt_switches high CPU contention Too many threads competing
Threads high Thread leak Worker pool not bounded
VmSize >> VmRSS Normal (Go, JVM over-reserve) Virtual ≠ physical

A process shows VmSize of 2 GB but VmRSS of only 50 MB. Is this a memory leak?


/proc/[pid]/fd — Open File Descriptors

# Count open FDs
ls /proc/1234/fd | wc -l

# See what each FD points to
ls -la /proc/1234/fd
# lrwx 0 -> /dev/pts/0        (terminal)
# lrwx 1 -> pipe:[123456]     (stdout pipe)
# lrwx 5 -> socket:[789012]   (TCP connection)
# lrwx 7 -> /var/log/app.log  (open file)
# lrwx 9 -> anon_inode:epoll  (epoll FD — Go netpoller)

# Find all open sockets
ls -la /proc/1234/fd | grep socket

# FD limit vs current
cat /proc/1234/limits | grep "open files"

That symlink target — /dev/pts/0, pipe:[123456], socket:[789012] — isn't stored on the fd itself. It's the end result of a lookup that happens every time something reads /proc/[pid]/fd/N. Step through it:

1. The fd is just an integer. Every open file descriptor is an index (0, 1, 2, 3, ...) into this process's own private fd table. The number itself says nothing about what it points to.
2. The fd table entry points to an open file description. This is a kernel object tracking the file offset, access mode, and what's actually backing this open — shared with any other fd (in this or another process) opened via dup() or inherited across fork().
3. The open file description points at the real resource. A regular file's inode, a pipe buffer, a socket, a terminal device, or an anonymous kernel object like an epoll instance.
4. /proc renders that resource as a symlink. Reading /proc/[pid]/fd/N — via ls -la or readlink — shows exactly what step 3 resolved to: a real path, or an identifier like pipe:[123456], socket:[789012], or anon_inode:epoll for things that aren't files with paths.

ls -la /proc/1234/fd shows fd 5 pointing to socket:[789012] instead of a file path. Does that mean something's broken?


/proc/[pid]/net/tcp — TCP Connections

cat /proc/1234/net/tcp
# sl  local_address  rem_address  st  ...
# 0:  00000000:1F90  00000000:0000  0A  ...  ← LISTEN on :8080
# 1:  0F02000A:1F90  0102000A:C3E0  01  ...  ← ESTABLISHED

# Decode hex address (little-endian)
# 0F02000A = 10.0.2.15, 1F90 = 8080
# State 0A = LISTEN, 01 = ESTABLISHED, 06 = TIME_WAIT

local_address 0F02000A:1F90 decodes to 10.0.2.15:8080. Reading the address bytes 0F 02 00 0A left to right as-is would give 15.2.0.10 — why isn't that the real address?


Other Useful /proc Files

# System-wide
cat /proc/loadavg          # 1.23 0.45 0.32 2/456 12345
                           # load1 load5 load15 running/total lastpid

cat /proc/sys/kernel/pid_max          # max PID (default 32768)
cat /proc/sys/fs/file-nr              # open FDs / free / max
cat /proc/sys/net/ipv4/ip_local_port_range  # ephemeral port range

# Per-process
cat /proc/1234/cmdline | tr '\0' ' '  # full command line
cat /proc/1234/environ | tr '\0' '<br>' # environment variables
cat /proc/1234/wchan                  # kernel function process is sleeping in
cat /proc/1234/schedstat              # CPU time, wait time, switches