Home / Alt manpages / proc_pid_stat(5)

  • proc_pid_stat(5)
  • File format
  • linux

Read Process CPU and Memory Data from /proc/pid/stat

You will finish with a small, repeatable way to inspect a process's state, CPU time, memory figures and start time from /proc/<pid>/stat. The examples use the local Linux man-pages package version 6.7-2 on a 6.8 kernel. Allow about fifteen minutes. You need a shell and a running process ID. Ordinary reads need no elevated privileges, although another user's process may expose restricted fields as 0.

1. Choose a live process

Start with your current shell. The shell expands $$ to its own process ID, so this does not depend on guessing a PID:

$ printf 'shell PID: %s\n' "$$"
shell PID: 24731
$ test -r "/proc/$$/stat" && echo readable
readable

Your number will differ. Check that the process still exists before reading it. A short-lived process can disappear between these two operations, which is normal rather than a permissions failure:

$ PID=$$
$ test -r "/proc/$PID/stat" && echo "${PID}: stat is available"
24731: stat is available

Checkpoint

Keep the value of PID visible while working. An empty or stale variable is an easy way to inspect the wrong path.

2. See the raw record, without treating it as a table of words

Read the record directly:

$ cat "/proc/$PID/stat"
24731 (bash) S 23890 24731 23890 34816 24731 4194304 1234 0 0 0 18 7 0 0 20 0 1 0 812345 123456789 4567 18446744073709551615 ...

The real line is longer and the counters change while the process runs. The record has 52 space-separated fields after the executable name, with the fields defined in a fixed order. The first three are pid, comm and state. The executable name is enclosed in parentheses and can contain spaces or parentheses, so a plain awk '{print $3}' is not a reliable way to obtain the state. It counts words, not the documented record structure.

Use ps as a human-readable cross-check, not as a substitute for understanding the file:

$ ps -p "$PID" -o pid=,stat=,comm=
24731 Ss   bash

The state may change between commands. That is expected for a live process.

3. Parse around the parenthesised executable name

This Python standard-library snippet splits at the final ) before parsing the remaining numeric fields. That avoids the common error where an executable name contains whitespace. It prints selected fields and checks that the record has the expected shape:

$ python3 - "$PID" <<'PY'
import os
import sys

pid = sys.argv[1]
with open(f"/proc/{pid}/stat", encoding="ascii") as stream:
    record = stream.readline().rstrip("\n")

head, tail = record.rsplit(") ", 1)
file_pid, comm = head.split(" (", 1)
fields = tail.split()
if len(fields) < 50:
    raise SystemExit(f"short stat record: {len(fields) + 2} fields")

state = fields[0]                 # field 3
utime = int(fields[11])            # field 14
stime = int(fields[12])            # field 15
starttime = int(fields[19])        # field 22
vsize = int(fields[20])             # field 23, bytes
rss_pages = int(fields[21])         # field 24, pages
ticks = os.sysconf("SC_CLK_TCK")

print(f"pid={file_pid} comm={comm} state={state}")
print(f"cpu_seconds={(utime + stime) / ticks:.2f}")
print(f"started_after_boot_seconds={starttime / ticks:.2f}")
print(f"virtual_bytes={vsize} resident_pages={rss_pages} clock_ticks={ticks}")
PY
pid=24731 comm=bash state=S
cpu_seconds=0.25
started_after_boot_seconds=8123.45
virtual_bytes=123456789 resident_pages=4567 clock_ticks=100

The sample numbers are illustrative; your output will be different. The installed machine reports 100 clock ticks per second, but do not hard-code that value in a portable script. The manpage says to divide utime, stime and starttime by sysconf(_SC_CLK_TCK), which is why the script asks the operating system.

4. Interpret the useful fields

state is a single character. R means running, S interruptible sleep, D uninterruptible disk sleep, T stopped, Z zombie and I idle on Linux 4.14 onward. A process can move between states while you inspect it. Do not treat one sample as a diagnosis of a performance problem.

utime is scheduled user-mode time and stime is scheduled kernel-mode time, both in clock ticks. Their sum is useful for a rough CPU-time reading. The documented user time includes guest time, so do not add guest_time again when calculating a total.

vsize is virtual memory in bytes. rss is the number of resident pages counted for text, data or stack, not bytes. Convert it only with the system page size:

$ getconf PAGESIZE
4096

For a more dependable memory view, use /proc/<pid>/statm or /proc/<pid>/status as appropriate. The manpage explicitly warns that the rss value in stat is inaccurate. It is a useful signal for quick inspection, not a billing-grade memory measurement.

5. Handle access limits and obsolete fields

Some address and wait-channel fields are protected by a ptrace access check and are shown as 0 when access is denied. Do not interpret a zero in those fields as proof that the process has no such address or is not waiting. Access to another user's process can also fail if the process has exited, the path is missing, or the host's security policy blocks the read. Try again with a process you own before reaching for elevated privileges.

Fields 31 to 34, the signal bitmaps, are documented as obsolete because they do not describe real-time signals. Use /proc/<pid>/status for signal information. Fields nswap and cnswap are not maintained. A field being present in this ABI does not mean it is a useful current metric.

Never write to /proc/<pid>/stat. This guide only reads it. There is no configuration change to undo, no service restart, and no elevated command required.

6. Verify a result against the live process

Run the parser twice if you need to see whether counters are moving, then compare the process identity and state with ps:

$ python3 - "$PID" <<'PY'
import sys
from pathlib import Path

pid = sys.argv[1]
line = Path(f"/proc/{pid}/stat").read_text(encoding="ascii").rstrip("\n")
head, tail = line.rsplit(") ", 1)
record_pid, comm = head.split(" (", 1)
fields = tail.split()
print(f"pid={record_pid} comm={comm} state={fields[0]} fields={len(fields) + 2}")
PY
pid=24731 comm=bash state=S fields=52
$ ps -p "$PID" -o pid=,stat=,comm=
24731 Ss   bash

Expect fields=52 on a current Linux system. If the process exits, the read can report "No such file or directory"; capture a new PID and rerun. If comm or the state disagrees, assume the process changed between samples before assuming the parser is wrong.

Done means

  • You selected a live PID and read its /proc/<pid>/stat record without root.
  • You parsed the parenthesised comm field before interpreting later fields.
  • You converted clock ticks with sysconf(_SC_CLK_TCK) instead of assuming a fixed rate.
  • You treated rss as resident pages and remembered the manpage's accuracy warning.
  • You recognised protected, obsolete and unmaintained fields instead of inventing meaning for zeroes.
  • You cross-checked the PID, command and state with ps.