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.
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.
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 |
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 noUSER_PROCESSrecords, 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 whatnum_fds()reports; the names are not in there.
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. |
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.
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)— returnsNone, 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.
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.
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.
zopen install psutilOr 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 psutilimport 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 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 ownPyInit_<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) andzoslibportstatically —libzoslib.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_asciiand__getutxent_ascii, which live inlibzoslib-supp.a. Static linking is also what a loadable extension needs here, since a module bound againstlibzoslib.sois one the loader will not accept. - Dependencies:
zoslibport
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).
The test suite validates, against a real running interpreter process:
pids()/pid_exists()correctnessname(),ppid()(cross-checked againstos.getppid())create_time()(cross-checked as "recent")status(),num_threads(),username()uids().effective(cross-checked againstos.geteuid())cpu_times()sanity (non-negative)cmdline()non-empty