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.