All writing

ProcWatch: Reconstructing Linux Process Monitoring from /proc

A C systems project that rebuilds process monitoring by reading Linux /proc directly—enumerating PIDs, parsing stat and cmdline, deriving CPU utilization from samples, and handling races, virtual memory, and kernel interfaces.

Terminal output from ProcWatch listing processes with PID, PPID, state, threads, VSIZE, RSS, CPU%, and command.

The question behind the tool

Most developers experience process monitoring through ps, top, or htop. Those tools feel authoritative, yet they are themselves userspace programs that read interfaces the kernel already exposes. ProcWatch was built to answer a concrete systems question:

What does it actually take to reconstruct process information from the interfaces Linux provides to userspace?

The result is a lightweight C utility that enumerates processes, reads metadata and command lines, derives CPU utilization from successive samples, sorts by CPU or memory, inspects individual processes, file descriptors, and memory mappings, and handles the inevitable races that occur when processes disappear mid-inspection.

ProcWatch does not wrap existing tools. It observes /proc directly.

Architecture

The program is organized as a clear pipeline:

CLI → process collection → sampling → CPU calculation → sorting → display

Source modules:

  • main.c — argument parsing, operation selection, sampling orchestration
  • proc.c / proc.h/proc enumeration, stat and cmdline reading, process table, CPU calculation, sorting
  • display.c / display.h — table, single-process, tree, fds, and maps views
  • util.c / util.h — file reading, PID-directory checks, timing helpers, error reporting

The central structures are struct proc_info (one process) and struct proc_table (the collection). The table starts with capacity 512 and doubles on exhaustion up to a defined maximum. Ownership is explicit: the table owns the array of process records.

Collection, analysis, and presentation remain separated. Display never re-reads /proc; it operates on the in-memory model.

Why `/proc`

Linux exposes process state through a pseudo-filesystem whose contents are generated by the kernel on demand. Relevant paths include:

/proc/<pid>/stat
/proc/<pid>/cmdline
/proc/<pid>/status
/proc/<pid>/fd/
/proc/<pid>/maps

ProcWatch treats these files as the authoritative interface. It does not need internal kernel data structures; it consumes the representation Linux already makes available to userspace.

Process discovery

The program opens /proc with opendir() and iterates with readdir(). Only entries whose names consist entirely of digits are treated as candidate PIDs. Everything else (sys, meminfo, cpuinfo, etc.) is ignored.

A process may exist when its directory is discovered and vanish before /proc/<pid>/stat is opened. ENOENT in this window is treated as a normal race: the entry is silently skipped. Live systems change while they are observed.

Parsing `/proc//stat`

/proc/<pid>/stat is positional and unforgiving. A single skipped field shifts every subsequent value. Relevant fields include:

1  pid
2  (comm)
3  state
4  ppid
14 utime
15 stime
20 num_threads
22 starttime
23 vsize
24 rss

The second field, comm, is enclosed in parentheses and may contain spaces or parentheses itself. Blind whitespace tokenization is unsafe. The parser therefore locates the closing parenthesis with strrchr(), the opening parenthesis with strchr(), extracts the name between them, and only then continues positional parsing of the remaining fields.

Early versions of the parser produced absurd values such as multi-petabyte VSIZE figures. Direct inspection of the raw file and careful field mapping corrected the skip counts. After the fix, ProcWatch’s numbers were consistent with both the raw kernel text and ps.

RSS is reported in pages; conversion by the system page size yields bytes. VSIZE is reported in bytes. These units must be handled explicitly.

Virtual size versus resident size

A process can report a multi-terabyte virtual address space while consuming only a few hundred megabytes of resident memory. VSIZE reflects the size of the virtual mappings; RSS reflects pages currently resident in physical RAM. The large values are not parser errors—they are real virtual-memory accounting. Distinguishing “unexpected” from “incorrect” required comparing ProcWatch output, the raw /proc record, unit conversion, and ps side by side.

Command lines and kernel threads

/proc/<pid>/cmdline stores arguments separated by NUL bytes. A helper converts the NUL-separated sequence into a space-separated string for display. Kernel threads often have an empty cmdline; in those cases ProcWatch falls back to the comm name extracted from stat.

CPU utilization

CPU percentage cannot be read from a single snapshot. The kernel supplies cumulative counters (utime + stime) in clock ticks. ProcWatch therefore:

  1. Takes sample A for every process.
  2. Sleeps for a configurable interval (default 1 s).
  3. Takes sample B.
  4. Computes the delta in ticks, converts to seconds using sysconf(_SC_CLK_TCK), divides by the measured wall-clock interval obtained with clock_gettime(CLOCK_MONOTONIC, ...), and scales to a percentage.

The resulting metric is derived, not supplied. This is the same pattern used for network rates, disk I/O rates, and most observability systems: sample, difference, normalize by time.

Sorting, filtering, and specialized views

Sorting uses qsort() with comparators for CPU percentage and RSS. The CLI supports:

  • default CPU-sorted listing
  • --top cpu|memory
  • --pid PID for single-process inspection
  • --tree for hierarchy
  • --fds PID and --maps PID
  • --interval and --limit

Argument parsing is performed with getopt_long().

Build system and quality constraints

The project is built with GNU Make. Sources compile to object files under obj/ and are linked into a single procwatch binary. Compiler flags are:

-std=c11 -Wall -Wextra -Wpedantic -O2 -g

Development encountered classic Make pitfalls—missing tab separators, malformed pattern rules, and the case sensitivity of makefile versus Makefile—as well as C issues around feature-test macros, missing headers for exit(), and the obsolescence of usleep(). The preferred interval primitive is nanosleep().

Warnings (assignment-suppression length modifiers in sscanf, potential snprintf truncation) remain documented technical debt; they do not prevent a successful build.

Operational behavior and verification

Process disappearance between enumeration and open is expected. Permission and existence failures surface clearly. The validation workflow used throughout development was:

make clean && make
./procwatch
./procwatch --pid <pid>
cat /proc/<pid>/stat
ps -p <pid> -o pid,comm,vsz,rss,nlwp

Agreement among ProcWatch, the raw kernel interface, and ps provided independent confirmation that the parser and unit conversions were correct.

Scope and limitations

ProcWatch is intentionally Linux-specific and observational. It does not ptrace, inject code, or attempt to replace htop or a debugger. Known limitations include remaining format-string warnings, heuristic presentation of very large virtual sizes, sequential matching assumptions in specialized views, and the absence of a comprehensive automated test suite. Future work can tighten parsing, improve human-readable memory units, add continuous refresh, and expand filtering without changing the core mission.

What the project demonstrates

A process is not a single number. It is kernel-maintained state exposed through multiple /proc files, each with its own format, units, and lifetime rules. ProcWatch turns that raw text into a structured model, derives rates from cumulative counters, and surfaces the results in a usable form.

The artifact is small enough to understand completely yet touches kernel interfaces, positional parsing, virtual-memory accounting, sampling, race handling, multi-file C structure, and Make. The same collection → parse → model → derive → present pipeline appears in monitoring agents, container runtimes, and infrastructure observability systems. That combination is the point of the project.