capstone · intermediate · 1 week

Process Monitor (htop-lite)

concepts
RAII over file handles · /proc parsing · ranges · ncurses bridge · value semantics
builds on
RAII + Smart Pointers · Containers, Iterators, and the STL Shape · Algorithms + Ranges

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:

  1. Process snapshot. Walk /proc, identify directories whose name is numeric (PIDs), read /proc/<pid>/stat and /proc/<pid>/status for each.
  2. Process model. A value type Process { pid, name, state, cpu_percent, rss_kb, …}.
  3. CPU-percent calculation. Snapshot at T0 and T1, compute the delta of utime + stime per process against the delta of total jiffies. (See man 5 proc for the columns.)
  4. 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:

ColumnField
1pid
2comm (in parens)
3state
14utime (user time, jiffies)
15stime (kernel time, jiffies)
24rss (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”:

  1. 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.
  2. ncurses. Standard on every Linux distro. C API. Has a million subtle behaviors. Wrap it in a small Window RAII class.
  3. 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:

  1. Process is a value type. Default-constructible, copyable, movable, equality-comparable. No raw pointers.
  2. ScopedFile (or std::unique_ptr<FILE, ...>) for every file handle.
  3. snapshot() uses ranges: std::filesystem::directory_iterator filtered to numeric names, transformed into Process objects, std::ranges::sort by CPU percent descending.
  4. Tests for the /proc/<pid>/stat parser using fixture files. Don’t hit the real /proc in unit tests.
  5. Refresh loop uses std::this_thread::sleep_until with a fixed tick, not sleep_for — keeps drift bounded.

Stretch goals

In order of difficulty:

  1. 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.
  2. Memory bars. /proc/meminfo gives MemTotal, MemAvailable, SwapTotal, SwapFree. Render a colored bar.
  3. Filter and sort interactively. Press / to enter a filter string; press c/m/p to sort by CPU/Memory/PID.
  4. Tree view. Read /proc/<pid>/status’s PPid: field, build a parent-child tree, render with indentation.
  5. Kill processes. Press k over a process; send SIGTERM.
  6. Cross-platform. Add a MacProcessReader that uses proc_listpids and proc_pidinfo on macOS. Same interface; same Process shape.

What you’ll have learned

By the end:

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.