Parsers for the text Linux exports through its pseudo-filesystems
README.md

pseudofs #

Parsers for the text Linux exports through its pseudo-filesystems.

proc(5) and sysfs(5) both begin with the same sentence: the filesystem is "a pseudo-filesystem which provides an interface to kernel data structures". That interface is text in the kernel's own formats, which change with the kernel and are documented by it. This library parses those formats into typed records and cites the kernel documentation for each one. A caller then reads /proc/<pid>/stat correctly in one place, where it would otherwise split it on spaces in five, and learn at 3am that a process called a) b (c) d reports somebody else's CPU time.

Each parser is a pure string -> (t, [> Msg of string]) resultover the bytes of one file. Nothing in the package opens a file, so the tests replay bytes captured from a real kernel and run on any machine. Reading the files is left to the separatepseudofs-eio` package.

Installation #

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam install pseudofs pseudofs-eio

Usage #

The resident memory of a process and the CPU time it has used come from two files:

# let status = "Name:\tcat\nState:\tR (running)\nVmRSS:\t     932 kB\n"
val status : string =
  "Name:\tcat\nState:\tR (running)\nVmRSS:\t     932 kB\n"
# Result.map Pseudofs.Proc.Pid.Status.vm_rss
    (Pseudofs.Proc.Pid.Status.of_string status)
- : (int64 option, [> `Msg of string ]) result = Ok (Some 954368L)

The process controls its own executable name in /proc/<pid>/stat, and the name can contain spaces and parentheses. The fields after it therefore start at the last closing parenthesis of the line, not the first:

# let stat = "18 (a) b (c) d) S 1 1 1 0 -1 4194304 77 0 1 0 3 1 0 0 20 0 1 0 8798194 2293760 221 18446744073709551615 0 0 0 0 0 0 0 6 0 1 0 0 17 11 0 0 0 0 0 0 0 0 0 0 0 0 0" in
  Result.map
    (fun t ->
      (t.Pseudofs.Proc.Pid.Stat.comm, Pseudofs.Proc.Pid.Stat.cpu_seconds t))
    (Pseudofs.Proc.Pid.Stat.of_string stat)
- : (string * float, [> `Msg of string ]) result = Ok ("a) b (c) d", 0.04)

Malformed input gives an error that says what was wrong, never a value that looks like a healthy zero:

# Pseudofs.Cgroup2.Cpu_stat.of_string "usage_usec\n"
- : (Pseudofs.Cgroup2.Cpu_stat.t, [> `Msg of string ]) result =
Error (`Msg "not a flat-keyed line: \"usage_usec\"")

pseudofs-eio runs the same parsers on a real filesystem. Each reader takes the root to read under, so a caller resolves /proc once and passes it down:

let resident_bytes env pid =
  let proc = Eio.Path.(Eio.Stdenv.fs env / "proc") in
  match Pseudofs_eio.Proc.status proc pid with
  | Ok status -> Pseudofs.Proc.Pid.Status.vm_rss status
  | Error (`Msg m) -> failwith m

API #

  • Pseudofs.Attr parses a file holding one value, which is most of sysfs.
  • Pseudofs.Proc parses cmdline, meminfo, loadavg, uptime, and per process stat, status, net/dev and cgroup.
  • Pseudofs.Cgroup2 parses cgroup.controllers, the flat-keyed files (cpu.stat, memory.stat, memory.events), the nested-keyed io.stat and the pressure-stall files.
  • Pseudofs.Sys_block parses the block device attributes, counted in the 512-byte sectors the block layer uses whatever the logical block size of the device.
  • Pseudofs.Kmsg parses the kernel log records /dev/kmsg hands out.
  • Pseudofs_eio reads the same files from a filesystem. It also lists the devices in /sys/block, since that needs code that can read a directory.

Testing #

test/fixtures holds bytes returned by a real Linux kernel, committed and replayed by the tests; test/fixtures/README.md records which kernel they came from and how they were captured. No test reads the machine it runs on. The parsers are what is under test, and a test that read /proc would check nothing on macOS and something different on every Linux host.

janestreet/procfs parses /proc in OCaml and owns the procfs name on opam. It is built on Core and Async. The core of this library depends only on fmt and does no I/O, so that it can link into a unikernel or an init process, and that is why it is separate and not a contribution to procfs. It also covers more than /proc: sysfs and /dev/kmsg too, which the kernel documentation also calls pseudo-filesystems.

The usual alternative is to read /proc by hand, which keeps going wrong in the same four places: the comm field in parentheses in /proc/<pid>/stat, the tabs in /proc/<pid>/status, the unsigned 64-bit fields that no signed decimal reader accepts, and a sysfs size that counts 512-byte sectors, not blocks.

Licence #

ISC, see LICENSE.md.