Process Monitor (htop-lite)
The brief
Write a tiny clone of htop — a terminal UI that lists every running
process on Linux, sorted by CPU usage, refreshed once a second. The
deliverable is a single binary, phtop, that draws a process table
until the user presses q.
This capstone is “systems C++” in miniature. You’ll parse /proc, wrap
file handles in RAII types, shape data with ranges, and talk to
ncurses (or a tiny ANSI-escape layer) through a thin C++ interface.
The whole thing is also a great test of std::expected — every read
from /proc can fail (the process exits between you opening the
directory and reading the file).
Scope (1 weekend)
The deliverable has four parts:
- Process snapshot. Walk
/proc, identify directories whose name is numeric (PIDs), read/proc/<pid>/statand/proc/<pid>/statusfor each. - Process model. A value type
Process { pid, name, state, cpu_percent, rss_kb, …}. - CPU-percent calculation. Snapshot at T0 and T1, compute the
delta of
utime + stimeper process against the delta of total jiffies. (Seeman 5 procfor the columns.) - TUI loop. Clear screen, render a sortable table, sleep, repeat.
The full tool should refresh in under 100 ms on a system with 200 processes.
The shape
A ProcessReader owning a directory handle:
#include <expected>
#include <filesystem>
#include <string>
#include <vector>
struct Process {
int pid;
std::string comm; // executable name, in parens in /proc
char state; // R, S, D, Z, …
long utime, stime; // jiffies of user+kernel time
long rss_pages; // resident set size
std::string cmdline;
};
class ProcessReader {
public:
std::expected<Process, std::error_code> read(int pid);
std::vector<Process> snapshot(); // all PIDs
};
The std::expected (C++23) is the modern alternative to throwing — the
“process died mid-read” case is expected, not exceptional.
If you can’t use C++23, use std::optional<Process> plus a logged
error, or define your own Result<T>.
File-handle RAII
The lesson 05 pattern, applied:
class ScopedFile {
std::FILE* f_;
public:
explicit ScopedFile(const char* path, const char* mode)
: f_(std::fopen(path, mode)) {}
~ScopedFile() { if (f_) std::fclose(f_); }
ScopedFile(const ScopedFile&) = delete;
ScopedFile& operator=(const ScopedFile&) = delete;
ScopedFile(ScopedFile&& o) noexcept : f_(o.f_) { o.f_ = nullptr; }
ScopedFile& operator=(ScopedFile&&) noexcept = delete; // simplicity
std::FILE* get() const { return f_; }
explicit operator bool() const { return f_ != nullptr; }
};
Use it for /proc/<pid>/stat reads. The destructor handles the close —
no leaks even if the parsing function throws.
Parsing /proc/<pid>/stat
The format is documented in proc(5). The interesting columns:
| Column | Field |
|---|---|
| 1 | pid |
| 2 | comm (in parens) |
| 3 | state |
| 14 | utime (user time, jiffies) |
| 15 | stime (kernel time, jiffies) |
| 24 | rss (resident pages) |
The trap: comm can contain spaces and parentheses, so you can’t just
scanf("%d %s %c …"). Parse comm by finding the last ) in the
line and splitting on it. Everything before is pid (comm; everything
after is the rest of the columns space-separated.
CPU-percent calculation
CPU percent isn’t a single number you can read — it’s a rate. You need two snapshots:
total_jiffies_at_T1 - total_jiffies_at_T0 = Δ_total
process_jiffies_at_T1 - process_jiffies_at_T0 = Δ_process
cpu_percent = 100 * Δ_process / Δ_total * num_cpus
total_jiffies comes from /proc/stat’s first cpu line. The
num_cpus factor scales the result so 100% means “one CPU fully
saturated.”
Don’t try to compute CPU % from a single snapshot. The number htop
shows on startup is from a 50ms-or-so pre-warmup interval — that’s why
the first frame’s CPU column reads close to zero.
TUI options
Three reasonable choices, in increasing order of “extra-dependency”:
- Raw ANSI escapes.
\033[H(home),\033[2J(clear),\033[3;1H(move to row 3 col 1). Works on any terminal. Roll your own row writer. ncurses. Standard on every Linux distro. C API. Has a million subtle behaviors. Wrap it in a smallWindowRAII class.ftxui(header-only, modern C++). Pleasant, declarative, somewhat heavy. Worth it if you’d be tempted to grow the UI.
For a 1-week capstone, option 1 (raw ANSI) keeps the dependency surface
minimal and forces you to think about terminal control. Most professional
tools (htop itself, btop) use ncurses.
Implementation rubric
Your code should hit these targets:
Processis a value type. Default-constructible, copyable, movable, equality-comparable. No raw pointers.ScopedFile(orstd::unique_ptr<FILE, ...>) for every file handle.snapshot()uses ranges:std::filesystem::directory_iteratorfiltered to numeric names, transformed intoProcessobjects,std::ranges::sortby CPU percent descending.- Tests for the
/proc/<pid>/statparser using fixture files. Don’t hit the real/procin unit tests. - Refresh loop uses
std::this_thread::sleep_untilwith a fixed tick, notsleep_for— keeps drift bounded.
Stretch goals
In order of difficulty:
- Per-CPU bars at the top, like
htop. Read/proc/stat’s per-CPU lines, normalize by jiffies, render a Unicode block-element bar per core. - Memory bars.
/proc/meminfogivesMemTotal,MemAvailable,SwapTotal,SwapFree. Render a colored bar. - Filter and sort interactively. Press
/to enter a filter string; pressc/m/pto sort by CPU/Memory/PID. - Tree view. Read
/proc/<pid>/status’sPPid:field, build a parent-child tree, render with indentation. - Kill processes. Press
kover a process; sendSIGTERM. - Cross-platform. Add a
MacProcessReaderthat usesproc_listpidsandproc_pidinfoon macOS. Same interface; sameProcessshape.
What you’ll have learned
By the end:
- Wrapped a real OS resource (file handle) in RAII and felt how much it reduces cleanup code.
- Used
std::expected<T, E>as the alternative to exceptions for “this can fail, here’s the error.” - Composed ranges pipelines for filtering and transforming a collection of processes.
- Implemented a small TUI loop and learned how terminals work at the byte level.
- Read several
/procfiles and understood what’s actually behindhtop,top,ps, and friends.
Reference reading: man 5 proc, the htop source (a model of
careful C-style code with deep terminal handling), and Bryant &
O’Hallaron’s Computer Systems: A Programmer’s Perspective for the
jiffies/scheduling background.