36. Reading /proc: build your own ps
Linux exposes every process as a directory under /proc
(OS Ch 23).
ps, top, and psutil read these files. So can you:
# labs/pyps.py: a minimal `ps aux` from /proc
import os
from dataclasses import dataclass
from pathlib import Path
CLK_TCK = os.sysconf("SC_CLK_TCK")
PAGE = os.sysconf("SC_PAGE_SIZE")
@dataclass(slots=True)
class Proc:
pid: int
user: str
state: str
rss_mb: float
cpu_s: float
threads: int
cmd: str
def users() -> dict[int, str]:
out = {}
for line in Path("/etc/passwd").read_text().splitlines():
parts = line.split(":")
if len(parts) > 2:
out[int(parts[2])] = parts[0]
return out
def read_proc(pid: int, names: dict[int, str]) -> Proc | None:
base = Path("/proc") / str(pid)
try:
stat = (base / "stat").read_text()
# comm is in parentheses and may contain spaces: split around the LAST ')'
rest = stat[stat.rindex(")") + 2 :].split()
state, utime, stime = rest[0], int(rest[11]), int(rest[12])
threads, rss_pages = int(rest[17]), int(rest[21])
uid = int((base / "status").read_text().split("Uid:")[1].split()[0])
cmd = (base / "cmdline").read_bytes().replace(b"\0", b" ").decode(errors="replace").strip()
if not cmd:
cmd = "[" + stat[stat.index("(") + 1 : stat.rindex(")")] + "]" # kernel thread
except (FileNotFoundError, ProcessLookupError, PermissionError):
return None # process exited while we read it
return Proc(pid, names.get(uid, str(uid)), state, rss_pages * PAGE / 2**20,
(utime + stime) / CLK_TCK, threads, cmd)
def main() -> None:
names = users()
procs = [p for d in os.listdir("/proc") if d.isdigit() if (p := read_proc(int(d), names))]
procs.sort(key=lambda p: p.rss_mb, reverse=True)
print(f"{'PID':>7} {'USER':<10} S {'RSS MB':>8} {'CPU s':>8} {'THR':>4} COMMAND")
for p in procs[:15]:
print(f"{p.pid:>7} {p.user:<10} {p.state} {p.rss_mb:8.1f} {p.cpu_s:8.1f} {p.threads:>4} {p.cmd[:60]}")
if __name__ == "__main__":
main()
What to notice:
- Races are normal. Processes exit between
listdirandread_text, so every read handlesFileNotFoundError. This is EAFP (Ch 7.2) for real. - The process name in
/proc/PID/statcan contain spaces and), so parsing must split on the last). Realpsimplementations had bugs here; a malicious process can name itself to break naive parsers. - In practice, use
psutil, which does this portably on Linux, macOS, and Windows. Write it by hand once to know what it reads.
Real-world example: a training-job watchdog that reads
/proc/self/status (VmRSS) and the cgroup's memory.max and
memory.current (/sys/fs/cgroup/) to log how close the job is to an OOM
kill (OS Ch 13),
so the next crash comes with evidence.