Skip to content

Repository files navigation

psutilport

z/OS port of psutil — a cross-platform library for retrieving information on running processes and system utilization (CPU, memory, disks, network, sensors) in Python.

Status

Early / experimental. This port adds a brand-new z/OS backend to psutil (there is no upstream z/OS support at all). Built and tested against Python 3.12, 3.13, and 3.14.

What works

System-wide:

API Notes
psutil.pids(), pid_exists()
psutil.virtual_memory() real storage, read from the CVT/RCE control blocks
psutil.boot_time() from utmpx's BOOT_TIME record
psutil.cpu_count() logical CPUs; cpu_count(logical=False) returns None
psutil.getloadavg() via zoslib; all three values are the same, see below
psutil.disk_usage(path) statvfs; cross-checks against df

Per process:

API Notes
.pid, .ppid(), .name(), .exe(), .cmdline() name() is the executable, see below
.username(), .uids(), .gids(), .status(), .nice()
.num_threads(), .create_time(), .wait()
.cpu_times() user/system, from BPX4GTH
.cpu_percent() computed from cpu_times() deltas
.terminal() controlling tty, or None when there is none
.num_fds() open descriptors, from BPX4GTH's file area
.memory_info(), .memory_full_info(), .memory_percent() see below

What is not implemented

System-wide cpu_times(), cpu_percent(), swap_memory(), disk_partitions(), net_*(), users(); and per process .cwd(), .open_files(), .threads(), .environ(), .io_counters(), .net_connections().

Calling one of these raises AttributeError rather than returning something made up. Contributions welcome — see patches/README.md for the full list and rationale.

Two of these were investigated and are blocked rather than merely unwritten:

  • users() — utmpx here carries no USER_PROCESS records, so the function could only ever return an empty list.
  • open_files() — BPX4GTH's file area gives a descriptor count and 12 bytes per descriptor, and none of those 12 bytes is a path. The count is what num_fds() reports; the names are not in there.

Blocked by the platform, not by this port

These were investigated and cannot be implemented as things stand. Recorded so the next person does not repeat the search.

API why
Process.nice(value) setpriority() returns ENOSYS — "Function not implemented" — from C and from os.setpriority alike. Reading nice() works, because getpriority() does.
Process.cpu_affinity() No affinity API exists: nothing in the system headers, nothing in zoslib, and os.sched_getaffinity/sched_setaffinity are both absent.
memory_full_info().uss Nothing reports a shared/private split. BPX4GTH's process area carries a region size, a memory limit and a usage figure, none of which distinguishes unique from shared pages, and there is no smaps equivalent. AIX and Solaris are in the same position.

Running pyperf

pyperf installs and imports, but does not run out of the box — and psutil is not the reason. It collects mem_max_rss from resource.getrusage(RUSAGE_SELF).ru_maxrss, which returns 0 on z/OS, and then rejects 0 as invalid metadata, so every worker dies before any measurement happens. (ru_utime and ru_stime are fine; it is specifically ru_maxrss.) psutil reports the same process's RSS correctly.

--track-memory additionally needs memory_full_info().uss, which is in the table above.

What this port does supply to pyperf: cpu_count(), boot_time(), memory_info() and memory_full_info(), plus nice() for reading. pyperf guards every psutil call in a try/except ImportError, so the missing pieces degrade rather than crash.

getloadavg() returns the same number three times

A Unix load average is sampled over 1, 5 and 15 minutes. z/OS keeps no such samples. zoslib's getloadavg() reads the CCT's current CPU-utilisation figure and returns it for all three intervals, so what you get is utilisation now, repeated — not three windows.

It is a real reading and it moves: 0.94 idle, 1.0 with a busy loop saturating the single CPU. The three values being identical is the tell that they are not averages, which is a good deal better than three plausible-looking numbers that were never sampled.

  • cpu_count(logical=False) — returns None, psutil's documented value for "cannot be determined". z/OS runs in an LPAR over shared physical processors, so the number of logical CPUs online says nothing about how many cores are behind them. This is not a stub standing in for a real number; it is the answer.

A note on benchmarking

The usual loop — run a workload, sample Process.cpu_percent() and Process.memory_info().rss, normalise by psutil.cpu_count() — works. What is missing for benchmarking is I/O accounting (io_counters(), disk_io_counters(), net_io_counters()) and system-wide CPU (psutil.cpu_percent(), psutil.cpu_times()).

Those are absent rather than stubbed, and deliberately so. A harness that records read_bytes=0 as a measurement publishes a wrong number and nobody notices; an AttributeError stops the run. hasattr(p, "io_counters") is the portable way to check, and it is what psutil's own backends expect, since each of them simply omits what its platform cannot provide.

Two things worth knowing

name() is the executable, not the job name. BPX4GTH reports a job name (DEVUSER8, SSHD7) that identifies the address space rather than the program. psutil's contract is the executable's name, so name() returns basename(argv[0])python, sshd, java — and falls back to the job name only for address spaces that expose no argv. Code doing p.name() == "python3" works as it would anywhere else.

swap_memory() is deliberately absent. z/OS auxiliary storage is not the same concept as swap, and a plausible-looking wrong number is worse than a missing one.

Installation

zopen install psutil

Or from the wheel index:

export PIP_EXTRA_INDEX_URL="https://repo.zopen.community/pypi/wheels/simple/"
export PIP_CONSTRAINT="https://repo.zopen.community/pulp/content/constraints/zopen-constraints.txt"

python3 -m venv --system-site-packages .venv
. .venv/bin/activate

pip install psutil

Example

import psutil

MB, GB = 1024**2, 1024**3

mem = psutil.virtual_memory()
print("total %.1f GB, used %.1f GB (%.0f%%), available %.1f GB"
      % (mem.total / GB, mem.used / GB, mem.percent, mem.available / GB))

# processes by memory. Guard the loop: processes exit while you iterate, and
# on a shared LPAR you will not have rights to all of them.
procs = []
for p in psutil.process_iter(["pid", "name", "username"]):
    try:
        procs.append((p.memory_info().rss, p.info["pid"], p.info["name"],
                      p.info["username"]))
    except (psutil.NoSuchProcess, psutil.AccessDenied):
        pass

for rss, pid, name, user in sorted(procs, reverse=True)[:5]:
    print("%-10s %-14s %-10s %7.1f MB" % (pid, name, user, rss / MB))

me = psutil.Process()
print(me.name(), me.cmdline(), me.status(), me.num_threads())
print("cpu %.3fs user, memory %.1f MB rss (%.2f%%)"
      % (me.cpu_times().user, me.memory_info().rss / MB, me.memory_percent()))

On a 16 GB LPAR that prints:

total 16.0 GB, used 5.0 GB (32%), available 11.0 GB
67174412   java           DEVUSER      613.0 MB
83952092   python         DEVUSER       30.0 MB
66115      gpg-agent      DEVUSER       28.0 MB
83952069   sshd           DEVUSER       13.0 MB
67174644   sleep          DEVUSER       13.0 MB
python ['python', 'demo.py'] running 1
cpu 0.032s user, memory 30.0 MB rss (0.18%)

Build details

  • Build System: setuptools (native compile, no cross-compilation needed)
  • Compiler: ibm-clang / ibm-clang++ with -m64 -fzos-le-char-mode=ascii — this flag is mandatory: without it, Open XL C/C++ compiles char/string literals (including the Python module's own PyInit_<name> symbol name) as EBCDIC even in ASCII-tagged source files, which breaks module import.
  • Linker: Links against the CPython side-deck (libpython3.1x.x) and zoslibport staticallylibzoslib.a, celquopt.s.o, libzoslib-supp.a. The side deck alone is not enough: zoslib's headers redirect libc calls to ASCII variants such as __strerror_ascii and __getutxent_ascii, which live in libzoslib-supp.a. Static linking is also what a loadable extension needs here, since a module bound against libzoslib.so is one the loader will not accept.
  • Dependencies: zoslibport

How it works

z/OS has no /proc-style process table comparable to Linux's (z/OS 3.1 has an early, partial Linux-compatible /proc, but it's missing too much — no environ, no per-thread info, no system-wide cpu/meminfo — to be a solid foundation). Instead, this port uses IBM's BPX4GTH ("get thread") assembler-linkage callable service (documented as BPXYPGTH), the same primitive htop's z/OS backend uses in production, accessed here through zoslibport's Apache 2.0-licensed __bpx4gth() wrapper.

The struct layout, decoding logic, and Python bindings in this port were independently authored and empirically verified against live ground truth (not transcribed from any GPL-licensed codebase). See patches/DESIGN.md for the full verification methodology, including some interesting surprises (e.g. a timing field that looked STCK-sized turned out to already be a plain Unix timestamp).

Test coverage

The test suite validates, against a real running interpreter process:

  • pids() / pid_exists() correctness
  • name(), ppid() (cross-checked against os.getppid())
  • create_time() (cross-checked as "recent")
  • status(), num_threads(), username()
  • uids().effective (cross-checked against os.geteuid())
  • cpu_times() sanity (non-negative)
  • cmdline() non-empty

About

z/OS port of psutil - cross-platform library for process and system utilization monitoring

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages