Frontend Labs
⋯

42. Reading /proc: build your own ps

/proc exposes kernel process state as text files (OS Ch 23). Parsing it correctly takes care: the command name in /proc/PID/stat is wrapped in parentheses and can itself contain spaces and ). A process named a) b breaks naive parsers, and attackers have used that to make monitoring tools misread process data.

// linux-only
use std::fs;

#[derive(Debug)]
struct Proc { pid: u32, ppid: u32, state: char, comm: String, rss_kib: u64, threads: u32, cmdline: String }

fn read_proc(pid: u32) -> Option<Proc> {
    let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
    // Format: "PID (COMM) STATE PPID ...". Split at the LAST ')' because COMM may contain ')'.
    let open = stat.find('(')?;
    let close = stat.rfind(')')?;
    let comm = stat[open + 1..close].to_string();
    let fields: Vec<&str> = stat[close + 2..].split(' ').collect(); // fields[0] = state
    let state = fields.first()?.chars().next()?;
    let ppid = fields.get(1)?.parse().ok()?;
    let threads = fields.get(17)?.parse().ok()?;                   // field 20 in `man 5 proc`

    let status = fs::read_to_string(format!("/proc/{pid}/status")).ok()?;
    let rss_kib = status.lines()
        .find_map(|l| l.strip_prefix("VmRSS:"))
        .and_then(|v| v.split_whitespace().next()?.parse().ok())
        .unwrap_or(0);                                             // kernel threads have no VmRSS

    let raw = fs::read(format!("/proc/{pid}/cmdline")).unwrap_or_default();
    let cmdline = raw.split(|&b| b == 0).filter(|a| !a.is_empty())
        .map(|a| String::from_utf8_lossy(a).replace('\n', " ")).collect::<Vec<_>>().join(" ");

    Some(Proc { pid, ppid, state, comm, rss_kib, threads, cmdline })
}

fn main() {
    let mut procs: Vec<Proc> = fs::read_dir("/proc").expect("Linux only")
        .filter_map(|e| e.ok()?.file_name().to_str()?.parse().ok())
        .filter_map(read_proc)                 // processes may exit while we scan: skip them
        .collect();
    procs.sort_by(|a, b| b.rss_kib.cmp(&a.rss_kib));

    println!("{:>7} {:>7} {} {:>9} {:>4}  COMMAND", "PID", "PPID", "S", "RSS(KiB)", "THR");
    for p in procs.iter().take(15) {
        let cmd = if p.cmdline.is_empty() { format!("[{}]", p.comm) } else { p.cmdline.clone() };
        println!("{:>7} {:>7} {} {:>9} {:>4}  {}", p.pid, p.ppid, p.state, p.rss_kib, p.threads,
            cmd.chars().take(60).collect::<String>());
    }
}

Compare this with the Go lab in OS Ch 79. The filter_map chain handles the race where a process exits between read_dir and read_to_string. Any of the three reads can fail, and skipping that process is the correct response.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36