diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-03-01 18:24:13 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-03-01 18:24:13 +0300 |
| commit | 364bfbfb9b3bd9e4c28308edfa665727016f149e (patch) | |
| tree | 08faa5368649833fe5cb9047989fdc75ea5a0637 | |
| download | Project-Tick-364bfbfb9b3bd9e4c28308edfa665727016f149e.tar.gz Project-Tick-364bfbfb9b3bd9e4c28308edfa665727016f149e.zip | |
init Standalone Linux-native port of FreeBSD `ps` for Project Tick BSD/Linux Distribution.
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | GNUmakefile | 35 | ||||
| -rw-r--r-- | README.md | 71 | ||||
| -rw-r--r-- | extern.h | 124 | ||||
| -rw-r--r-- | fmt.c | 48 | ||||
| -rw-r--r-- | keyword.c | 249 | ||||
| -rw-r--r-- | nlist.c | 76 | ||||
| -rw-r--r-- | print.c | 596 | ||||
| -rw-r--r-- | ps.1 | 1020 | ||||
| -rw-r--r-- | ps.c | 638 | ||||
| -rw-r--r-- | ps.h | 205 | ||||
| -rw-r--r-- | tests/spin_helper.c | 45 | ||||
| -rw-r--r-- | tests/test.sh | 147 |
13 files changed, 3257 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..3193332093 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +out/ +*.o diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000000..eb27646501 --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,35 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CPPFLAGS ?= +CPPFLAGS += -D_GNU_SOURCE +CFLAGS ?= -O2 +CFLAGS += -std=c17 -g -Wall -Wextra -Werror -Wno-unused-parameter +LDFLAGS ?= +LDLIBS ?= -lm + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/ps + +SRCS := ps.c keyword.c print.c fmt.c nlist.c +OBJS := $(SRCS:%.c=$(OBJDIR)/%.o) + +.PHONY: all clean dirs test + +all: $(TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(OBJDIR)/%.o: $(CURDIR)/%.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$<" -o "$@" + +test: $(TARGET) + CC="$(CC)" PS_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh" + +clean: + @rm -rf "$(OBJDIR)" "$(OUTDIR)" diff --git a/README.md b/README.md new file mode 100644 index 0000000000..4d69c1bc68 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# ps + +Standalone Linux-native port of FreeBSD `ps` for Project Tick BSD/Linux Distribution. + +## Build + +```sh +gmake -f GNUmakefile +gmake -f GNUmakefile CC=musl-gcc +``` + +## Test + +```sh +gmake -f GNUmakefile test +gmake -f GNUmakefile test CC=musl-gcc +``` + +## Port Strategy + +| BSD mechanism | Linux replacement | +|---|---| +| `kvm_openfiles(3)` / `kvm_getprocs(3)` | `/proc` directory enumeration | +| `struct kinfo_proc` | `/proc/[pid]/stat` + `/proc/[pid]/status` + `/proc/[pid]/cmdline` + `/proc/[pid]/wchan` | +| `sysctl(3)` / `nlist(3)` | `/proc/meminfo` (for physical memory stats) | +| `libxo` | Standard `printf` output (libxo not available on target Linux-musl) | +| `vis(3)` / `strvis(3)` | Simplified visual encoding for non-printable characters | +| `SLIST_*` / `STAILQ_*` (BSD queue macros) | Kept or replaced with standard C logic | + +### /proc field mapping + +- `pid`, `ppid`, `pgrp`, `sid`, `tdev`: parsed from `/proc/[pid]/stat` fields +- `ruid`, `euid`, `rgid`, `egid`: parsed from `/proc/[pid]/status` (`Uid:`/`Gid:` lines) +- `cpu-time`: `utime` + `stime` from `/proc/[pid]/stat` (fields 14 and 15) +- `percent-cpu`: calculated from `cpu-time` and process uptime (from jiffies) +- `virtual-size`: field 23 of `/proc/[pid]/stat` (in bytes) +- `rss`: field 24 of `/proc/[pid]/stat` (in pages) * pagesize +- `state`: field 3 of `/proc/[pid]/stat` (mapped from Linux to BSD-like characters) +- `wchan`: read from `/proc/[pid]/wchan` + +## Supported Semantics (subset) + +- Default output: `pid,tt,state,time,command` +- Common formats: `-j`, `-l`, `-u`, `-v` (BSD-style formats) +- Selection: `-A` (all), `-a` (all with tty), `-p pid`, `-t tty`, `-U uid`, `-G gid` +- Sorting: `-m` (by memory), `-r` (by CPU) +- Output control: `-O`, `-o`, `-w` (width control), `-h` (repeat headers) + +## Unsupported / Not Available on Linux + +| Option | Reason | +|---|---| +| `-j jail` | FreeBSD jail IDs have no Linux equivalent | +| `-class class` | FreeBSD login class has no Linux equivalent | +| `-M core` | Requires `kvm(3)` for core file analysis | +| `-N system` | Requires `kvm(3)` for name list extraction | +| `-Z` | FreeBSD MAC labels don't map to a portable Linux interface | + +## Known Differences + +- `state` flags: Mapping is approximate (e.g., BSD `P_CONTROLT` has no exact flag in Linux `/proc/[pid]/stat`). +- Memory stats: `tsiz`, `dsiz`, `ssiz` are hard to extract precisely on Linux via `/proc` compared to `struct kinfo_proc`. `vsz` and `rss` are accurate. +- CPU % calculation: BSD-style decaying average (`ki_pctcpu`) is hard to replicate exactly; a life-time average is used instead. + +## Test Categories + +- CLI parsing and usage +- Process selection: `-p`, `-A`, `-t` +- Format selection: `-j`, `-l`, `-u`, `-o` +- Sorting: `-m`, `-r` +- Negative tests: invalid flags, non-existent pids. diff --git a/extern.h b/extern.h new file mode 100644 index 0000000000..b0b18240da --- /dev/null +++ b/extern.h @@ -0,0 +1,124 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1991, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * Copyright (c) 2026 Project Tick. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * Linux-native port: replaces kvm(3) prototypes with Linux /proc equivalents. + */ + +#ifndef _EXTERN_H_ +#define _EXTERN_H_ + +#ifndef nitems +#define nitems(x) (sizeof(x) / sizeof((x)[0])) +#endif + +#ifdef __cplusplus +#define __BEGIN_DECLS extern "C" { +#define __END_DECLS } +#else +#define __BEGIN_DECLS +#define __END_DECLS +#endif + +struct kinfo; +struct var; +struct varent; +typedef struct kinfo KINFO; +typedef struct varent VARENT; + +extern fixpt_t ccpu; +extern int cflag, eval, fscale, nlistread, rawcpu; +extern unsigned long mem_total_kb; +extern time_t now; +extern int showthreads, sumrusage, termwidth; +extern struct velisthead varlist; +extern const size_t known_keywords_nb; + +__BEGIN_DECLS +char *arguments(KINFO *, VARENT *); +void check_keywords(void); +char *command(KINFO *, VARENT *); +char *cputime(KINFO *, VARENT *); +char *cpunum(KINFO *, VARENT *); +int donlist(void); +char *elapsed(KINFO *, VARENT *); +char *elapseds(KINFO *, VARENT *); +char *emulname(KINFO *, VARENT *); +VARENT *find_varentry(const char *); +char *fmt_argv(char **, char *, char *, size_t); +double getpcpu(const KINFO *); +char *jailname(KINFO *, VARENT *); +size_t aliased_keyword_index(const VAR *); +char *kvar(KINFO *, VARENT *); +char *label(KINFO *, VARENT *); +char *loginclass(KINFO *, VARENT *); +char *logname(KINFO *, VARENT *); +char *longtname(KINFO *, VARENT *); +char *lstarted(KINFO *, VARENT *); +char *maxrss(KINFO *, VARENT *); +char *lockname(KINFO *, VARENT *); +char *mwchan(KINFO *, VARENT *); +char *nwchan(KINFO *, VARENT *); +char *pagein(KINFO *, VARENT *); +void parsefmt(const char *, struct velisthead *, int); +char *pcpu(KINFO *, VARENT *); +char *pmem(KINFO *, VARENT *); +char *pri(KINFO *, VARENT *); +void printheader(void); +char *priorityr(KINFO *, VARENT *); +char *egroupname(KINFO *, VARENT *); +char *rgroupname(KINFO *, VARENT *); +void resolve_aliases(void); +char *runame(KINFO *, VARENT *); +char *rvar(KINFO *, VARENT *); +void showkey(void); +char *started(KINFO *, VARENT *); +char *state(KINFO *, VARENT *); +char *systime(KINFO *, VARENT *); +char *tdev(KINFO *, VARENT *); +char *tdnam(KINFO *, VARENT *); +char *tname(KINFO *, VARENT *); +char *ucomm(KINFO *, VARENT *); +char *username(KINFO *, VARENT *); +char *upr(KINFO *, VARENT *); +char *usertime(KINFO *, VARENT *); +char *vsize(KINFO *, VARENT *); +char *wchan(KINFO *, VARENT *); + +/* Linux only: user_from_uid and group_from_gid helpers if not in system */ +char *user_from_uid(uid_t, int); +char *group_from_gid(gid_t, int); +/* BSD-like devname(3) or equivalent */ +char *devname(dev_t, mode_t); +void free_devnames(void); + +__END_DECLS + +#endif /* _EXTERN_H_ */ @@ -0,0 +1,48 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1992, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * Copyright (c) 2026 Project Tick. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <sys/types.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <err.h> +#include "ps.h" + +/* Simplified fmt_argv for Linux */ +char * +fmt_argv(char **argv, char *cmd, char *thread, size_t maxlen) +{ + char *res = strdup(cmd ? cmd : ""); + if (!res) + err(1, "malloc failed"); + return res; +} diff --git a/keyword.c b/keyword.c new file mode 100644 index 0000000000..e53771201b --- /dev/null +++ b/keyword.c @@ -0,0 +1,249 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * Copyright (c) 2025 The FreeBSD Foundation + * Copyright (c) 2026 Project Tick. All rights reserved. + * + * Portions of this software were developed by Olivier Certner + * <olce@FreeBSD.org> at Kumacom SARL under sponsorship from the FreeBSD + * Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <sys/param.h> +#include <sys/resource.h> +#include <sys/time.h> + +#include <assert.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <err.h> + +#include "ps.h" + +static int vcmp(const void *, const void *); + +#define KOFF(x) offsetof(struct kinfo_proc, x) +#define ROFF(x) offsetof(struct rusage, x) + +#define UIDFMT "u" +#define PIDFMT "d" + +/* Sorted alphabetically by name */ +static VAR keywords[] = { + {"%cpu", {NULL}, "%CPU", NULL, 0, pcpu, 0, UNSPEC, NULL}, + {"%mem", {NULL}, "%MEM", NULL, 0, pmem, 0, UNSPEC, NULL}, + {"args", {NULL}, "COMMAND", NULL, COMM|LJUST|USER, arguments, 0, UNSPEC, NULL}, + {"comm", {NULL}, "COMMAND", NULL, LJUST, ucomm, 0, UNSPEC, NULL}, + {"command", {NULL}, "COMMAND", NULL, COMM|LJUST|USER, command, 0, UNSPEC, NULL}, + {"cpu", {NULL}, "C", NULL, 0, cpunum, 0, UNSPEC, NULL}, + {"cputime", {"time"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL}, + {"dsiz", {NULL}, "DSIZ", NULL, 0, kvar, KOFF(ki_dsize), PGTOK, "ld"}, + {"egid", {"gid"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL}, + {"egroup", {"group"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL}, + {"emul", {NULL}, "EMUL", NULL, LJUST, emulname, 0, UNSPEC, NULL}, + {"etime", {NULL}, "ELAPSED", NULL, USER, elapsed, 0, UNSPEC, NULL}, + {"etimes", {NULL}, "ELAPSED", NULL, USER, elapseds, 0, UNSPEC, NULL}, + {"euid", {"uid"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL}, + {"f", {NULL}, "F", NULL, 0, kvar, KOFF(ki_flag), LONG, "lx"}, + {"flags", {"f"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL}, + {"gid", {NULL}, "GID", NULL, 0, kvar, KOFF(ki_groups), UINT, UIDFMT}, + {"group", {NULL}, "GROUP", NULL, LJUST, egroupname, 0, UNSPEC, NULL}, + {"jail", {NULL}, "JAIL", NULL, LJUST, jailname, 0, UNSPEC, NULL}, + {"jid", {NULL}, "JID", NULL, 0, kvar, KOFF(ki_jid), INT, "d"}, + {"jobc", {NULL}, "JOBC", NULL, 0, kvar, KOFF(ki_sid), INT, "d"}, /* session as jobc proxy */ + {"label", {NULL}, "LABEL", NULL, LJUST, label, 0, UNSPEC, NULL}, + {"lim", {NULL}, "LIM", NULL, 0, maxrss, 0, UNSPEC, NULL}, + {"login", {NULL}, "LOGIN", NULL, LJUST, logname, 0, UNSPEC, NULL}, + {"logname", {"login"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL}, + {"lstart", {NULL}, "STARTED", NULL, LJUST|USER, lstarted, 0, UNSPEC, NULL}, + {"lwp", {NULL}, "LWP", NULL, 0, kvar, KOFF(ki_pid), UINT, PIDFMT}, + {"mwchan", {NULL}, "MWCHAN", NULL, LJUST, mwchan, 0, UNSPEC, NULL}, + {"ni", {"nice"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL}, + {"nice", {NULL}, "NI", NULL, 0, kvar, KOFF(ki_nice), CHAR, "d"}, + {"nlwp", {NULL}, "NLWP", NULL, 0, kvar, KOFF(ki_numthreads), UINT, "d"}, + {"nwchan", {NULL}, "NWCHAN", NULL, LJUST, nwchan, 0, UNSPEC, NULL}, + {"pagein", {NULL}, "PAGEIN", NULL, USER, pagein, 0, UNSPEC, NULL}, + {"pcpu", {"%cpu"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL}, + {"pgid", {NULL}, "PGID", NULL, 0, kvar, KOFF(ki_pgid), UINT, PIDFMT}, + {"pid", {NULL}, "PID", NULL, 0, kvar, KOFF(ki_pid), UINT, PIDFMT}, + {"pmem", {"%mem"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL}, + {"ppid", {NULL}, "PPID", NULL, 0, kvar, KOFF(ki_ppid), UINT, PIDFMT}, + {"pri", {NULL}, "PRI", NULL, 0, pri, 0, UNSPEC, NULL}, + {"rgid", {NULL}, "RGID", NULL, 0, kvar, KOFF(ki_rgid), UINT, UIDFMT}, + {"rgroup", {NULL}, "RGROUP", NULL, LJUST, rgroupname, 0, UNSPEC, NULL}, + {"rss", {NULL}, "RSS", NULL, 0, kvar, KOFF(ki_rssize), PGTOK, "ld"}, + {"ruid", {NULL}, "RUID", NULL, 0, kvar, KOFF(ki_ruid), UINT, UIDFMT}, + {"ruser", {NULL}, "RUSER", NULL, LJUST, runame, 0, UNSPEC, NULL}, + {"sid", {NULL}, "SID", NULL, 0, kvar, KOFF(ki_sid), UINT, PIDFMT}, + {"sl", {NULL}, "SL", NULL, INF127, kvar, KOFF(ki_slptime), UINT, "d"}, + {"ssiz", {NULL}, "SSIZ", NULL, 0, kvar, KOFF(ki_ssize), PGTOK, "ld"}, + {"start", {NULL}, "STARTED", NULL, LJUST|USER, started, 0, UNSPEC, NULL}, + {"stat", {"state"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL}, + {"state", {NULL}, "STAT", NULL, LJUST, state, 0, UNSPEC, NULL}, + {"svgid", {NULL}, "SVGID", NULL, 0, kvar, KOFF(ki_svgid), UINT, UIDFMT}, + {"svuid", {NULL}, "SVUID", NULL, 0, kvar, KOFF(ki_svuid), UINT, UIDFMT}, + {"systime", {NULL}, "SYSTIME", NULL, USER, systime, 0, UNSPEC, NULL}, + {"tdev", {NULL}, "TDEV", NULL, 0, tdev, 0, UNSPEC, NULL}, + {"tid", {"lwp"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL}, + {"time", {NULL}, "TIME", NULL, USER, cputime, 0, UNSPEC, NULL}, + {"tpgid", {NULL}, "TPGID", NULL, 0, kvar, KOFF(ki_tpgid), UINT, PIDFMT}, + {"tsiz", {NULL}, "TSIZ", NULL, 0, kvar, KOFF(ki_tsize), PGTOK, "ld"}, + {"tt", {NULL}, "TT ", NULL, 0, tname, 0, UNSPEC, NULL}, + {"tty", {NULL}, "TTY", NULL, LJUST, longtname, 0, UNSPEC, NULL}, + {"ucomm", {NULL}, "UCOMM", NULL, LJUST, ucomm, 0, UNSPEC, NULL}, + {"uid", {NULL}, "UID", NULL, 0, kvar, KOFF(ki_uid), UINT, UIDFMT}, + {"upr", {NULL}, "UPR", NULL, 0, upr, 0, UNSPEC, NULL}, + {"user", {NULL}, "USER", NULL, LJUST, username, 0, UNSPEC, NULL}, + {"usertime", {NULL}, "USERTIME", NULL, USER, usertime, 0, UNSPEC, NULL}, + {"vsize", {"vsz"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL}, + {"vsz", {NULL}, "VSZ", NULL, 0, vsize, 0, UNSPEC, NULL}, + {"wchan", {NULL}, "WCHAN", NULL, LJUST, wchan, 0, UNSPEC, NULL}, +}; + +const size_t known_keywords_nb = nitems(keywords); + +size_t +aliased_keyword_index(const VAR *const v) +{ + const VAR *const fv = (v->flag & RESOLVED_ALIAS) == 0 ? v : v->final_kw; + const size_t idx = fv - keywords; + assert(idx < known_keywords_nb); + return (idx); +} + +void +check_keywords(void) +{ + const VAR *k, *next_k; + bool order_violated = false; + for (size_t i = 0; i < known_keywords_nb - 1; ++i) { + k = &keywords[i]; + next_k = &keywords[i + 1]; + if (strcmp(k->name, next_k->name) >= 0) { + warnx("keywords bad order: '%s' followed by '%s'", k->name, next_k->name); + order_violated = true; + } + } + if (order_violated) errx(2, "keywords not in order"); +} + +static void +merge_alias(VAR *const k, VAR *const tgt) +{ + if ((tgt->flag & RESOLVED_ALIAS) != 0) + k->final_kw = tgt->final_kw; + else + k->final_kw = tgt; + + if (k->header == NULL) k->header = tgt->header; + if (k->field == NULL) k->field = tgt->field; + if (k->flag == 0) k->flag = tgt->flag; + + k->oproc = tgt->oproc; + k->off = tgt->off; + k->type = tgt->type; + k->fmt = tgt->fmt; +} + +static void +resolve_alias(VAR *const k) +{ + VAR *t, key; + if ((k->flag & RESOLVED_ALIAS) != 0 || k->aliased == NULL) return; + if ((k->flag & RESOLVING_ALIAS) != 0) errx(2, "cycle in alias '%s'", k->name); + k->flag |= RESOLVING_ALIAS; + key.name = k->aliased; + t = bsearch(&key, keywords, known_keywords_nb, sizeof(VAR), vcmp); + if (t == NULL) errx(2, "unknown alias target '%s'", k->aliased); + resolve_alias(t); + merge_alias(k, t); + k->flag &= ~RESOLVING_ALIAS; + k->flag |= RESOLVED_ALIAS; +} + +void +resolve_aliases(void) +{ + for (size_t i = 0; i < known_keywords_nb; ++i) + resolve_alias(&keywords[i]); +} + +void +showkey(void) +{ + const VAR *v; + int i = 0; + printf("Keywords:\n"); + for (v = keywords; v < keywords + known_keywords_nb; ++v) { + printf("%-10s%s", v->name, (++i % 7 == 0) ? "\n" : " "); + } + printf("\n"); +} + +void +parsefmt(const char *p, struct velisthead *const var_list, const int user) +{ + char *copy = strdup(p); + char *cp = copy; + char *token; + VAR *v, key; + struct varent *vent; + + while ((token = strsep(&cp, " \t,\n")) != NULL) { + if (*token == '\0') continue; + char *hdr = strchr(token, '='); + if (hdr) *hdr++ = '\0'; + + key.name = token; + v = bsearch(&key, keywords, known_keywords_nb, sizeof(VAR), vcmp); + if (v == NULL) { + warnx("%s: keyword not found", token); + eval = 1; + continue; + } + resolve_alias(v); + vent = malloc(sizeof(struct varent)); + if (!vent) err(1, "malloc"); + vent->header = hdr ? strdup(hdr) : v->header; + vent->width = strlen(vent->header); + vent->var = v; + vent->flags = user ? VE_KEEP : 0; + STAILQ_INSERT_TAIL(var_list, vent, next_ve); + } + free(copy); +} + +static int +vcmp(const void *a, const void *b) +{ + return strcmp(((const VAR *)a)->name, ((const VAR *)b)->name); +} diff --git a/nlist.c b/nlist.c new file mode 100644 index 0000000000..ae16581981 --- /dev/null +++ b/nlist.c @@ -0,0 +1,76 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * Copyright (c) 2026 Project Tick. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include "ps.h" + +fixpt_t ccpu = 0; +int nlistread = 0; +unsigned long mem_total_kb = 0; +int fscale = 100; + +/* + * On Linux, we read memory stats from /proc/meminfo. + * ccpu and fscale are used for CPU calculations; we'll use a simplified model. + */ +int +donlist(void) +{ + FILE *fp; + char line[256]; + + if (nlistread) + return (0); + + fp = fopen("/proc/meminfo", "r"); + if (fp) { + while (fgets(line, sizeof(line), fp)) { + if (sscanf(line, "MemTotal: %lu", &mem_total_kb) == 1) { + break; + } + } + fclose(fp); + } + + if (mem_total_kb == 0) + mem_total_kb = 1024 * 256; /* fallback */ + + /* + * Linux fixed-point scale for load averages is usually 1 << 8, + * but for ps pct calculation we just need a baseline. + */ + fscale = 100; + nlistread = 1; + return (0); +} diff --git a/print.c b/print.c new file mode 100644 index 0000000000..b9fd40d300 --- /dev/null +++ b/print.c @@ -0,0 +1,596 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * Copyright (c) 2026 Project Tick. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <sys/param.h> +#include <sys/time.h> +#include <sys/resource.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/sysmacros.h> + +#include <ctype.h> +#include <dirent.h> +#include <errno.h> +#include <err.h> +#include <grp.h> +#include <langinfo.h> +#include <locale.h> +#include <math.h> +#include <pwd.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <time.h> + +#include "ps.h" + +#define COMMAND_WIDTH 16 +#define ARGUMENTS_WIDTH 16 + +#define SAFE_ASPRINTF(ptr, fmt, ...) do { \ + if (asprintf(ptr, fmt, __VA_ARGS__) == -1) \ + err(1, "asprintf"); \ +} while (0) + +/* Simple strvis-like implementation */ +static char * +simple_strvis(const char *src) +{ + if (!src) return NULL; + size_t len = strlen(src); + char *dst = malloc(len * 4 + 1); + if (!dst) return NULL; + char *d = dst; + for (const char *s = src; *s; s++) { + if (isprint((unsigned char)*s) || *s == ' ') { + *d++ = *s; + } else { + d += sprintf(d, "\\%03o", (unsigned char)*s); + } + } + *d = '\0'; + return dst; +} + +void +printheader(void) +{ + struct varent *vent; + + STAILQ_FOREACH(vent, &varlist, next_ve) + if (*vent->header != '\0') + break; + if (!vent) + return; + + STAILQ_FOREACH(vent, &varlist, next_ve) { + const VAR *v = vent->var; + if (v->flag & LJUST) { + if (STAILQ_NEXT(vent, next_ve) == NULL) /* last one */ + printf("%s", vent->header); + else + printf("%-*s", vent->width, vent->header); + } else + printf("%*s", vent->width, vent->header); + if (STAILQ_NEXT(vent, next_ve) != NULL) + printf(" "); + } + printf("\n"); +} + +char * +arguments(KINFO *k, VARENT *ve) +{ + char *vis_args = simple_strvis(k->ki_args); + if (!vis_args) return NULL; + + if (STAILQ_NEXT(ve, next_ve) != NULL && strlen(vis_args) > ARGUMENTS_WIDTH) + vis_args[ARGUMENTS_WIDTH] = '\0'; + + return (vis_args); +} + +char * +command(KINFO *k, VARENT *ve) +{ + char *str = NULL; + + if (cflag) { + if (STAILQ_NEXT(ve, next_ve) == NULL) { + SAFE_ASPRINTF(&str, "%s%s", + k->ki_d.prefix ? k->ki_d.prefix : "", + k->ki_p->ki_comm); + } else { + str = strdup(k->ki_p->ki_comm); + if (!str) err(1, "strdup"); + } + return (str); + } + + char *vis_args = simple_strvis(k->ki_args); + if (!vis_args) return strdup("-"); + + if (STAILQ_NEXT(ve, next_ve) == NULL) { + /* last field */ + char *vis_env = k->ki_env ? simple_strvis(k->ki_env) : NULL; + asprintf(&str, "%s%s%s%s", + k->ki_d.prefix ? k->ki_d.prefix : "", + vis_env ? vis_env : "", + vis_env ? " " : "", + vis_args); + free(vis_env); + free(vis_args); + } else { + str = vis_args; + if (strlen(str) > COMMAND_WIDTH) + str[COMMAND_WIDTH] = '\0'; + } + return (str); +} + +char * +ucomm(KINFO *k, VARENT *ve) +{ + char *str; + if (STAILQ_NEXT(ve, next_ve) == NULL) { + asprintf(&str, "%s%s", + k->ki_d.prefix ? k->ki_d.prefix : "", + k->ki_p->ki_comm); + } else { + str = strdup(k->ki_p->ki_comm); + } + return (str); +} + +char * +tdnam(KINFO *k, VARENT *ve __unused) +{ + if (k->ki_p->ki_numthreads > 1) + return strdup(k->ki_p->ki_tdname); + return strdup(" "); +} + +char * +logname(KINFO *k, VARENT *ve __unused) +{ + if (*k->ki_p->ki_login == '\0') + return NULL; + return strdup(k->ki_p->ki_login); +} + +char * +state(KINFO *k, VARENT *ve __unused) +{ + char *buf = malloc(16); + if (!buf) return NULL; + char *cp = buf; + + *cp++ = k->ki_p->ki_stat; + + if (k->ki_p->ki_nice < 0) *cp++ = '<'; + else if (k->ki_p->ki_nice > 0) *cp++ = 'N'; + + /* Approximate flags */ + if (k->ki_p->ki_sid == k->ki_p->ki_pid) *cp++ = 's'; + if (k->ki_p->ki_tdev != (dev_t)-1 && k->ki_p->ki_pgid == k->ki_p->ki_tpgid) *cp++ = '+'; + + *cp = '\0'; + return buf; +} + +char * +pri(KINFO *k, VARENT *ve __unused) +{ + char *str; + SAFE_ASPRINTF(&str, "%d", k->ki_p->ki_pri); + return str; +} + +char * +upr(KINFO *k, VARENT *ve __unused) +{ + char *str; + SAFE_ASPRINTF(&str, "%d", k->ki_p->ki_pri); + return str; +} + +char * +username(KINFO *k, VARENT *ve __unused) +{ + return strdup(user_from_uid(k->ki_p->ki_uid, 0)); +} + +char * +egroupname(KINFO *k, VARENT *ve __unused) +{ + return strdup(group_from_gid(k->ki_p->ki_groups[0], 0)); +} + +char * +rgroupname(KINFO *k, VARENT *ve __unused) +{ + return strdup(group_from_gid(k->ki_p->ki_rgid, 0)); +} + +char * +runame(KINFO *k, VARENT *ve __unused) +{ + return strdup(user_from_uid(k->ki_p->ki_ruid, 0)); +} + +char * +tdev(KINFO *k, VARENT *ve __unused) +{ + char *str; + if (k->ki_p->ki_tdev == (dev_t)-1) + str = strdup("-"); + else + asprintf(&str, "%#jx", (uintmax_t)k->ki_p->ki_tdev); + return str; +} + +char * +tname(KINFO *k, VARENT *ve __unused) +{ + char *name = devname(k->ki_p->ki_tdev, S_IFCHR); + if (!name) return strdup("- "); + + char *str; + if (strncmp(name, "tty", 3) == 0) name += 3; + if (strncmp(name, "pts/", 4) == 0) name += 4; + + asprintf(&str, "%s ", name); + return str; +} + +char * +longtname(KINFO *k, VARENT *ve __unused) +{ + char *name = devname(k->ki_p->ki_tdev, S_IFCHR); + return strdup(name ? name : "-"); +} + +char * +started(KINFO *k, VARENT *ve __unused) +{ + if (!k->ki_valid) return NULL; + char *buf = malloc(100); + if (!buf) return NULL; + time_t then = k->ki_p->ki_start.tv_sec; + struct tm *tp = localtime(&then); + if (now - then < 24 * 3600) + strftime(buf, 100, "%H:%M ", tp); + else if (now - then < 7 * 86400) + strftime(buf, 100, "%a%H ", tp); + else + strftime(buf, 100, "%e%b%y", tp); + return buf; +} + +char * +lstarted(KINFO *k, VARENT *ve __unused) +{ + if (!k->ki_valid) return NULL; + char *buf = malloc(100); + if (!buf) return NULL; + time_t then = k->ki_p->ki_start.tv_sec; + strftime(buf, 100, "%c", localtime(&then)); + return buf; +} + +char * +lockname(KINFO *k, VARENT *ve __unused) +{ + return NULL; +} + +char * +wchan(KINFO *k, VARENT *ve __unused) +{ + if (k->ki_p->ki_wmesg[0]) + return strdup(k->ki_p->ki_wmesg); + return NULL; +} + +char * +nwchan(KINFO *k, VARENT *ve __unused) +{ + return NULL; +} + +char * +mwchan(KINFO *k, VARENT *ve __unused) +{ + if (k->ki_p->ki_wmesg[0]) + return strdup(k->ki_p->ki_wmesg); + return NULL; +} + +char * +vsize(KINFO *k, VARENT *ve __unused) +{ + char *str; + asprintf(&str, "%lu", (unsigned long)(k->ki_p->ki_size / 1024)); + return str; +} + +static char * +printtime(KINFO *k, VARENT *ve __unused, long secs, long psecs) +{ + char *str; + if (!k->ki_valid) { secs = 0; psecs = 0; } + asprintf(&str, "%ld:%02ld.%02ld", secs / 60, secs % 60, psecs / 10000); + return str; +} + +char * +cputime(KINFO *k, VARENT *ve) +{ + long secs = k->ki_p->ki_runtime / 1000000; + long psecs = k->ki_p->ki_runtime % 1000000; + return printtime(k, ve, secs, psecs); +} + +char * +cpunum(KINFO *k, VARENT *ve __unused) +{ + char *str; + asprintf(&str, "%d", k->ki_p->ki_lastcpu); + return str; +} + +char * +systime(KINFO *k, VARENT *ve) +{ + return strdup("0:00.00"); +} + +char * +usertime(KINFO *k, VARENT *ve) +{ + return strdup("0:00.00"); +} + +char * +elapsed(KINFO *k, VARENT *ve __unused) +{ + if (!k->ki_valid) return NULL; + time_t val = now - k->ki_p->ki_start.tv_sec; + int days = val / 86400; val %= 86400; + int hours = val / 3600; val %= 3600; + int mins = val / 60; + int secs = val % 60; + char *str; + if (days != 0) asprintf(&str, "%3d-%02d:%02d:%02d", days, hours, mins, secs); + else if (hours != 0) asprintf(&str, "%02d:%02d:%02d", hours, mins, secs); + else asprintf(&str, "%02d:%02d", mins, secs); + return str; +} + +char * +elapseds(KINFO *k, VARENT *ve __unused) +{ + if (!k->ki_valid) return NULL; + char *str; + asprintf(&str, "%jd", (intmax_t)(now - k->ki_p->ki_start.tv_sec)); + return str; +} + +double +getpcpu(const KINFO *k) +{ + if (!k->ki_valid) return 0.0; + time_t uptime = now - k->ki_p->ki_start.tv_sec; + if (uptime <= 0) uptime = 1; + return (100.0 * (k->ki_p->ki_runtime / 1000000.0)) / uptime; +} + +char * +pcpu(KINFO *k, VARENT *ve __unused) +{ + char *str; + SAFE_ASPRINTF(&str, "%.1f", k->ki_pcpu); + return str; +} + +char * +pmem(KINFO *k, VARENT *ve __unused) +{ + if (mem_total_kb == 0) donlist(); + char *str; + double pct = (100.0 * k->ki_p->ki_rssize) / mem_total_kb; + asprintf(&str, "%.1f", pct); + return str; +} + +char * +pagein(KINFO *k, VARENT *ve __unused) +{ + return strdup("0"); +} + +char * +maxrss(KINFO *k, VARENT *ve __unused) +{ + return NULL; +} + +char * +priorityr(KINFO *k, VARENT *ve __unused) +{ + char *str; + SAFE_ASPRINTF(&str, "%d", k->ki_p->ki_pri); + return str; +} + +char * +kvar(KINFO *k, VARENT *ve) +{ + const VAR *v = ve->var; + char *bp = (char *)k->ki_p + v->off; + char *str; + char fmt[32]; + snprintf(fmt, sizeof(fmt), "%%%s", v->fmt ? v->fmt : "s"); + switch (v->type) { + case INT: asprintf(&str, fmt, *(int *)bp); break; + case UINT: asprintf(&str, fmt, *(unsigned int *)bp); break; + case LONG: asprintf(&str, fmt, *(long *)bp); break; + case ULONG: asprintf(&str, fmt, *(unsigned long *)bp); break; + case SHORT: asprintf(&str, fmt, *(short *)bp); break; + case USHORT: asprintf(&str, fmt, *(unsigned short *)bp); break; + case CHAR: asprintf(&str, fmt, *(char *)bp); break; + case UCHAR: asprintf(&str, fmt, *(unsigned char *)bp); break; + case PGTOK: + { + unsigned long val = *(unsigned long *)bp; + asprintf(&str, fmt, (unsigned long)(val * getpagesize() / 1024)); + break; + } + default: asprintf(&str, "?"); break; + } + return str; +} + +char * +rvar(KINFO *k, VARENT *ve) +{ + return strdup("-"); +} + +char * +emulname(KINFO *k, VARENT *ve) +{ + return strdup("Linux"); +} + +char * +jailname(KINFO *k, VARENT *ve) +{ + return strdup("-"); +} + +char * +label(KINFO *k, VARENT *ve) +{ + return strdup("-"); +} + +char * +loginclass(KINFO *k, VARENT *ve) +{ + return strdup("-"); +} + +/* Linux-specific helpers */ + +char * +user_from_uid(uid_t uid, int nouser) +{ + static char buf[32]; + struct passwd *pw = getpwuid(uid); + if (!pw || nouser) { + snprintf(buf, sizeof(buf), "%u", uid); + return buf; + } + return pw->pw_name; +} + +char * +group_from_gid(gid_t gid, int nogroup) +{ + static char buf[32]; + struct group *gr = getgrgid(gid); + if (!gr || nogroup) { + snprintf(buf, sizeof(buf), "%u", gid); + return buf; + } + return gr->gr_name; +} + +struct dev_cache { + dev_t dev; + char name[PATH_MAX]; +}; + +static struct dev_cache *dcache = NULL; +static int dcache_size = 0; + +static void +cache_dir(const char *path, const char *prefix) +{ + DIR *dir = opendir(path); + struct dirent *ent; + struct stat st; + char buf[PATH_MAX]; + + if (!dir) return; + while ((ent = readdir(dir))) { + if (ent->d_name[0] == '.') continue; + snprintf(buf, sizeof(buf), "%s/%s", path, ent->d_name); + if (stat(buf, &st) == 0 && S_ISCHR(st.st_mode)) { + struct dev_cache *tmp; + tmp = realloc(dcache, (dcache_size + 1) * sizeof(struct dev_cache)); + if (!tmp) err(1, "realloc"); + dcache = tmp; + dcache[dcache_size].dev = st.st_rdev; + snprintf(dcache[dcache_size].name, PATH_MAX, "%s%s", prefix, ent->d_name); + dcache_size++; + } + } + closedir(dir); +} + +char * +devname(dev_t dev, mode_t type) +{ + if (dev == (dev_t)-1) return NULL; + if (dcache == NULL) { + cache_dir("/dev/pts", "pts/"); + cache_dir("/dev", ""); + } + for (int i = 0; i < dcache_size; i++) { + if (dcache[i].dev == dev) + return dcache[i].name; + } + return NULL; +} + +void +free_devnames(void) +{ + free(dcache); + dcache = NULL; + dcache_size = 0; +} @@ -0,0 +1,1020 @@ +.\"- +.\" SPDX-License-Identifier: BSD-3-Clause +.\" +.\" Copyright (c) 1980, 1990, 1991, 1993, 1994 +.\" The Regents of the University of California. All rights reserved. +.\" Copyright (c) 2025 The FreeBSD Foundation +.\" Copyright (c) 2026 Project Tick. +.\" +.\" Portions of this documentation were written by Olivier Certner +.\" <olce@FreeBSD.org> at Kumacom SARL under sponsorship from the FreeBSD + \" Foundation. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" 3. Neither the name of the University nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd July 16, 2025 +.Dt PS 1 +.Os +.Sh NAME +.Nm ps +.Nd process status +.Sh SYNOPSIS +.Nm +.Op Fl -libxo +.Op Fl AaCcdefHhjlmrSTuvwXxZ +.Op Fl O Ar fmt +.Op Fl o Ar fmt +.Op Fl D Ar up | down | both +.Op Fl G Ar gid Ns Op , Ns Ar gid Ns Ar ... +.Op Fl J Ar jid Ns Op , Ns Ar jid Ns Ar ... +.Op Fl M Ar core +.Op Fl N Ar system +.Op Fl p Ar pid Ns Op , Ns Ar pid Ns Ar ... +.Op Fl t Ar tty Ns Op , Ns Ar tty Ns Ar ... +.Op Fl U Ar user Ns Op , Ns Ar user Ns Ar ... +.Nm +.Op Fl -libxo +.Fl L +.Sh DESCRIPTION +The +.Nm +utility displays information about a selection of processes. +Its traditional text style output consists of a header line followed by one line +of information per selected process, or possibly multiple ones if using +.Fl H +.Pq one per lightweight-process . +Other output styles can be requested via +.Fl -libxo . +.Pp +By default, only the processes of the calling user, determined by matching their +effective user ID with that of the +.Nm +process, that have controlling terminals are shown. +A different set of processes can be selected for display by using combinations +of the +.Fl A , a , D , G , J , p , T , t , U , X , +and +.Fl x +options. +Except for options +.Fl X +and +.Fl x , +as soon as one of them appears, it inhibits the default process selection, i.e., +the calling user's processes are shown only on request. +If more than one of these +.Pq with same exceptions +appear, +.Nm +will select processes as soon as they are matched by at least one of them +.Pq inclusive OR . +The +.Fl X +option can be independently used to further filter the listed processes to only +those that have a controlling terminal +.Po +except for those selected by +.Fl p +.Pc . +Its opposite, +.Fl x , +forcefully removes that filter. +If none of +.Fl X +and +.Fl x +is specified, the implied default behavior is that of +.Fl X +unless using another option whose description explicitly says that +.Fl x +is implied. +.Pp +For each selected process, the default displayed information consists of the +process' ID, controlling terminal, state, CPU time +.Pq including both user and system time +and associated command +.Po +see the documentation for the +.Cm command +keyword below +.Pc . +This information can be tweaked using two groups of options which can be +combined as needed. +First, options +.Fl o +and +.Fl O +add columns with data corresponding to the explicitly passed keywords. +Available keywords are documented in the +.Sx KEYWORDS +section below. +They can be listed using option +.Fl L . +Second, options +.Fl j , l , u , +and +.Fl v +designate specific predefined groups of columns, also called canned displays. +Appearance of any of these options inhibits the default display, replacing it +all with the requested columns, and in the order options are passed. +The individual columns requested via a canned display option that have the same +keyword or an alias to that of some column added by an earlier canned display +option, or by an explicit +.Fl O +or +.Fl o +option anywhere on the command line, are suppressed. +This automatic removal of duplicate data in canned displays is useful for +slightly tweaking these displays and/or combining multiple ones without having +to rebuild variants from scratch, e.g., using only +.Fl o +options. +.Pp +Output information lines are by default sorted first by controlling terminal, +then by process ID, and then, if +.Fl H +has been specified, by lightweight-process (thread) ID. +The +.Fl m , r , u , +and +.Fl v +options will change the sort order. +If more than one sorting option was given, then the selected processes +will be sorted by the last sorting option which was specified. +.Pp +If the traditional text output (the default) is used, the default output width is that requested by the +.Ev COLUMNS +environment variable if present, else the line width of the terminal associated +to the +.Nm +process, if any. +In all other situations, the output width is unlimited. +See also the +.Fl w +option and the +.Sx BUGS +section. +.Pp +For backwards compatibility, +.Nm +attempts to interpret any positional argument as a process ID, as if specified +by the +.Fl p +option. +Failure to do so will trigger an error. +.Nm +also accepts the old-style BSD options, whose format and effect are left +undocumented on purpose. +.Pp +The options are as follows: +.Bl -tag -width indent +.It Fl -libxo +Generate output via +.Xr libxo 3 +in a selection of different human and machine readable formats. +See +.Xr xo_options 7 +for details on command line arguments. +The default is the traditional text style output. +.It Fl A +Display information about all processes in the system. +Using this option is strictly equivalent to specifying both +.Fl a +and +.Fl x . +Please see their description for more information. +.It Fl a +Display information about all users' processes. +It does not, however, list all processes +.Po +see +.Fl A +and +.Fl x +.Pc . +If the +.Va security.bsd.see_other_uids +sysctl is set to zero, this option is honored only if the real user ID of the +.Nm +process is 0. +.It Fl C +Change the way the CPU percentage is calculated by using a +.Dq raw +CPU calculation that ignores +.Dq resident +time (this normally has +no effect). +.It Fl c +Change the +.Dq command +column output to just contain the executable name, +rather than the full command line. +.It Fl D +Expand the list of selected processes based on the process tree. +.Dq UP +will add the ancestor processes, +.Dq DOWN +will add the descendant processes, and +.Dq BOTH +will add both the ancestor and the descendant processes. +.Fl D +does not imply +.Fl d , +but works well with it. +.It Fl d +Arrange processes into descendancy order and prefix each command with +indentation text showing sibling and parent/child relationships as a tree. +If either of the +.Fl m +and +.Fl r +options are also used, they control how sibling processes are sorted +relative to each other. +Note that this option has no effect if the last column does not have +.Cm comm , +.Cm command +or +.Cm ucomm +as its keyword. +.It Fl e +Display the environment as well. +.It Fl f +Indicates to print the full command and arguments in +.Cm command +columns. +This is the default behavior on +.Fx . +See +.Fl c +to turn it off. +.It Fl G +Display information about processes whose real group ID matches the specified +group IDs or names. +Implies +.Fl x +by default. +.It Fl H +Show all of the threads associated with each process. +.It Fl h +Repeat the information header as often as necessary to guarantee one +header per page of information. +.It Fl J +Display information about processes which match the specified jail IDs. +This may be either the +.Cm jid +or +.Cm name +of the jail. +Use +.Fl J +.Sy 0 +to request display of host processes. +Implies +.Fl x +by default. +.It Fl j +Print information associated with the following keywords: +.Cm user , pid , ppid , pgid , sid , jobc , state , tt , time , +and +.Cm command . +.It Fl L +List the set of keywords available for the +.Fl O +and +.Fl o +options. +.It Fl l +Display information associated with the following keywords: +.Cm uid , pid , ppid , cpu , pri , nice , vsz , rss , mwchan , state , +.Cm tt , time , +and +.Cm command . +.It Fl M +Extract values associated with the name list from the specified core +instead of the currently running system. +.It Fl m +Sort by memory usage, instead of the combination of controlling +terminal and process ID. +.It Fl N +Extract the name list from the specified system instead of the default, +which is the kernel image the system has booted from. +.It Fl O +Save passed columns in a separate list that in the end is grafted just after the +display's first occurence of the process ID column as specified by other +options, or the default display if there is none. +If the display prepared by other options does not include a process ID column, +the list is inserted at start of the display. +Further occurences of +.Fl O +append to the to-be-grafted list of columns. +This option takes a space- or comma-separated list of keywords. +The last keyword in the list may be appended with an equals sign +.Pq Ql = +as explained for option +.Fl o +and with the same effect. +.It Fl o +Display information associated with the space- or comma-separated list of +keywords specified. +The last keyword in the list may be appended with an equals sign +.Pq Ql = +and a string that spans the rest of the argument, and can contain +space and comma characters. +This causes the printed header to use the specified string instead of +the standard header. +Multiple keywords may also be given in the form of more than one +.Fl o +option. +So the header texts for multiple keywords can be changed. +If all keywords have empty header texts, no header line is written. +.It Fl p +Display information about processes which match the specified process IDs. +Processes selected by this option are not subject to being filtered by +.Fl X . +.It Fl r +Sort by current CPU usage, instead of the combination of controlling +terminal and process ID. +.It Fl S +Change the way the process times, namely cputime, systime, and usertime, +are calculated by summing all exited children to their parent process. +.It Fl T +Display information about processes attached to the device associated +with the standard input. +.It Fl t +Display information about processes attached to the specified terminal +devices. +Full pathnames, as well as abbreviations (see explanation of the +.Cm tt +keyword) can be specified. +Implies +.Fl x +by default. +.It Fl U +Display information about processes whose real user ID matches the specified +user IDs or names. +Implies +.Fl x +by default. +.It Fl u +Display information associated with the following keywords: +.Cm user , pid , %cpu , %mem , vsz , rss , tt , state , start , time , +and +.Cm command . +The +.Fl u +option implies the +.Fl r +option. +.It Fl v +Display information associated with the following keywords: +.Cm pid , state , time , sl , re , pagein , vsz , rss , lim , tsiz , +.Cm %cpu , %mem , +and +.Cm command . +The +.Fl v +option implies the +.Fl m +option. +.It Fl w +Use at least 131 columns to display information. +If +.Fl w +is specified more than once, +.Nm +will use as many columns as necessary. +Please see the preamble of this manual page for how the output width is +initially determined. +In particular, if the initial output width is unlimited, specifying +.Fl w +has no effect. +Please also consult the +.Sx BUGS +section. +.It Fl X +When displaying processes selected by other options, skip any processes which do +not have a controlling terminal, except for those selected through +.Fl p . +This is the default behaviour, unless using another option whose description +explicitly says that +.Fl x +is implied. +.It Fl x +When displaying processes selected by other options, include processes which do +not have a controlling terminal. +This option has the opposite behavior to that of +.Fl X . +If both +.Fl X +and +.Fl x +are specified, +.Nm +will obey the last occurence. +.It Fl Z +Add +.Xr mac 4 +label to the list of keywords for which +.Nm +will display information. +.El +.Sh KEYWORDS +The following is a complete list of the available keywords and their meanings. +Several of them have aliases (keywords which are synonyms). +Detailed descriptions for some of them can be found after this list. +.Pp +.Bl -tag -width ".Cm sigignore" -compact +.It Cm %cpu +percentage CPU usage (alias +.Cm pcpu ) +.It Cm %mem +percentage memory usage (alias +.Cm pmem ) +.It Cm acflag +accounting flag (alias +.Cm acflg ) +.It Cm args +command and arguments +.It Cm class +login class +.It Cm comm +command +.It Cm command +command and arguments +.It Cm cow +number of copy-on-write faults +.It Cm cpu +The processor number on which the process is executing (visible only on SMP +systems). +.It Cm dsiz +data size in KiB +.It Cm emul +system-call emulation environment (ABI) +.It Cm etime +elapsed running time, format +.Do +.Op days- Ns +.Op hours\&: Ns +minutes:seconds +.Dc +.It Cm etimes +elapsed running time, in decimal integer seconds +.It Cm fib +default FIB number, see +.Xr setfib 1 +.It Cm flags +the process flags, in hexadecimal (alias +.Cm f ) +.It Cm flags2 +the additional set of process flags, in hexadecimal (alias +.Cm f2 ) +.It Cm gid +effective group ID (alias +.Cm egid ) +.It Cm group +group name (from egid) (alias +.Cm egroup ) +.It Cm inblk +total blocks read (alias +.Cm inblock ) +.It Cm jail +jail name +.It Cm jid +jail ID +.It Cm jobc +job control count +.It Cm ktrace +tracing flags +.It Cm label +MAC label +.It Cm lim +memoryuse limit +.It Cm lockname +lock currently blocked on (as a symbolic name) +.It Cm logname +login name of user who started the session +.It Cm lstart +time started +.It Cm lwp +thread (light-weight process) ID (alias +.Cm tid ) +.It Cm majflt +total page faults +.It Cm minflt +total page reclaims +.It Cm msgrcv +total messages received (reads from pipes/sockets) +.It Cm msgsnd +total messages sent (writes on pipes/sockets) +.It Cm mwchan +wait channel or lock currently blocked on +.It Cm nice +nice value (alias +.Cm ni ) +.It Cm nivcsw +total involuntary context switches +.It Cm nlwp +number of threads (light-weight processes) tied to a process +.It Cm nsigs +total signals taken (alias +.Cm nsignals ) +.It Cm nswap +total swaps in/out +.It Cm nvcsw +total voluntary context switches +.It Cm nwchan +wait channel (as an address) +.It Cm oublk +total blocks written (alias +.Cm oublock ) +.It Cm paddr +process pointer +.It Cm pagein +pageins (same as majflt) +.It Cm pgid +process group number +.It Cm pid +process ID +.It Cm ppid +parent process ID +.It Cm pri +scheduling priority +.It Cm re +core residency time (in seconds; 127 = infinity) +.It Cm rgid +real group ID +.It Cm rgroup +group name (from rgid) +.It Cm rss +resident set size in KiB +.It Cm rtprio +realtime priority (see +.Xr rtprio 1) +.It Cm ruid +real user ID +.It Cm ruser +user name (from ruid) +.It Cm sid +session ID +.It Cm sig +pending signals (alias +.Cm pending ) +.It Cm sigcatch +caught signals (alias +.Cm caught ) +.It Cm sigignore +ignored signals (alias +.Cm ignored ) +.It Cm sigmask +blocked signals (alias +.Cm blocked ) +.It Cm sl +sleep time (in seconds; 127 = infinity) +.It Cm ssiz +stack size in KiB +.It Cm start +time started +.It Cm state +symbolic process state (alias +.Cm stat ) +.It Cm svgid +saved gid from a setgid executable +.It Cm svuid +saved UID from a setuid executable +.It Cm systime +accumulated system CPU time +.It Cm tdaddr +thread address +.It Cm tdname +thread name +.It Cm tdev +control terminal device number +.It Cm time +accumulated CPU time, user + system (alias +.Cm cputime ) +.It Cm tpgid +control terminal process group ID +.It Cm tracer +tracer process ID +.\".It Cm trss +.\"text resident set size in KiB +.It Cm tsid +control terminal session ID +.It Cm tsiz +text size in KiB +.It Cm tt +control terminal name (two letter abbreviation) +.It Cm tty +full name of control terminal +.It Cm ucomm +process name used for accounting +.It Cm uid +effective user ID (alias +.Cm euid ) +.It Cm upr +scheduling priority on return from system call (alias +.Cm usrpri ) +.It Cm uprocp +process pointer +.It Cm user +user name (from UID) +.It Cm usertime +accumulated user CPU time +.It Cm vmaddr +vmspace pointer +.It Cm vsz +virtual size in KiB (alias +.Cm vsize ) +.It Cm wchan +wait channel (as a symbolic name) +.It Cm xstat +exit or stop status (valid only for stopped or zombie process) +.El +.Pp +Some of these keywords are further specified as follows: +.Bl -tag -width lockname +.It Cm %cpu +The CPU utilization of the process; this is a decaying average over up to +a minute of previous (real) time. +Since the time base over which this is computed varies (since processes may +be very young) it is possible for the sum of all +.Cm %cpu +fields to exceed 100%. +.It Cm %mem +The percentage of real memory used by this process. +.It Cm class +Login class associated with the process. +.It Cm command +The printed command and arguments are determined as follows. +A process that has exited and has a parent that has not yet waited for the +process (in other words, a zombie) is listed as +.Dq Li <defunct>. +If the arguments cannot be located +.Po +usually because they have not been set, as is the case for system processes +and/or kernel threads +.Pc , +the command name is printed within square brackets. +The +.Nm +utility first tries to obtain the arguments cached by the kernel +.Po +if they were shorter than the value of the +.Va kern.ps_arg_cache_limit +sysctl +.Pc . +The process can change the arguments shown with +.Xr setproctitle 3 . +Otherwise, +.Nm +makes an educated guess as to the file name and arguments given when the +process was created by examining memory or the swap area. +The method is inherently somewhat unreliable and in any event a process +is entitled to destroy this information. +The +.Cm ucomm +keyword +.Pq accounting +can, however, be depended on. +If the arguments are unavailable or do not agree with the +.Cm ucomm +keyword, the value for the +.Cm ucomm +keyword is appended to the arguments in parentheses. +.It Cm flags +The flags associated with the process as in +the include file +.In sys/proc.h : +.Bl -column P_SINGLE_BOUNDARY 0x40000000 +.It Dv "P_ADVLOCK" Ta No "0x00000001" Ta "Process may hold a POSIX advisory lock" +.It Dv "P_CONTROLT" Ta No "0x00000002" Ta "Has a controlling terminal" +.It Dv "P_KPROC" Ta No "0x00000004" Ta "Kernel process" +.It Dv "P_PPWAIT" Ta No "0x00000010" Ta "Parent is waiting for child to exec/exit" +.It Dv "P_PROFIL" Ta No "0x00000020" Ta "Has started profiling" +.It Dv "P_STOPPROF" Ta No "0x00000040" Ta "Has thread in requesting to stop prof" +.It Dv "P_HADTHREADS" Ta No "0x00000080" Ta "Has had threads (no cleanup shortcuts)" +.It Dv "P_SUGID" Ta No "0x00000100" Ta "Had set id privileges since last exec" +.It Dv "P_SYSTEM" Ta No "0x00000200" Ta "System proc: no sigs, stats or swapping" +.It Dv "P_SINGLE_EXIT" Ta No "0x00000400" Ta "Threads suspending should exit, not wait" +.It Dv "P_TRACED" Ta No "0x00000800" Ta "Debugged process being traced" +.It Dv "P_WAITED" Ta No "0x00001000" Ta "Someone is waiting for us" +.It Dv "P_WEXIT" Ta No "0x00002000" Ta "Working on exiting" +.It Dv "P_EXEC" Ta No "0x00004000" Ta "Process called exec" +.It Dv "P_WKILLED" Ta No "0x00008000" Ta "Killed, shall go to kernel/user boundary ASAP" +.It Dv "P_CONTINUED" Ta No "0x00010000" Ta "Proc has continued from a stopped state" +.It Dv "P_STOPPED_SIG" Ta No "0x00020000" Ta "Stopped due to SIGSTOP/SIGTSTP" +.It Dv "P_STOPPED_TRACE" Ta No "0x00040000" Ta "Stopped because of tracing" +.It Dv "P_STOPPED_SINGLE" Ta No "0x00080000" Ta "Only one thread can continue" +.It Dv "P_PROTECTED" Ta No "0x00100000" Ta "Do not kill on memory overcommit" +.It Dv "P_SIGEVENT" Ta No "0x00200000" Ta "Process pending signals changed" +.It Dv "P_SINGLE_BOUNDARY" Ta No "0x00400000" Ta "Threads should suspend at user boundary" +.It Dv "P_HWPMC" Ta No "0x00800000" Ta "Process is using HWPMCs" +.It Dv "P_JAILED" Ta No "0x01000000" Ta "Process is in jail" +.It Dv "P_TOTAL_STOP" Ta No "0x02000000" Ta "Stopped for system suspend" +.It Dv "P_INEXEC" Ta No "0x04000000" Ta Process is in Xr execve 2 +.It Dv "P_STATCHILD" Ta No "0x08000000" Ta "Child process stopped or exited" +.It Dv "P_INMEM" Ta No "0x10000000" Ta "Always set, unused" +.It Dv "P_PPTRACE" Ta No "0x80000000" Ta "Vforked child issued ptrace(PT_TRACEME)" +.El +.It Cm flags2 +The flags kept in +.Va p_flag2 +associated with the process as in +the include file +.In sys/proc.h : +.Bl -column P2_INHERIT_PROTECTED 0x00000001 +.It Dv "P2_INHERIT_PROTECTED" Ta No "0x00000001" Ta "New children get P_PROTECTED" +.It Dv "P2_NOTRACE" Ta No "0x00000002" Ta "No" Xr ptrace 2 attach or coredumps +.It Dv "P2_NOTRACE_EXEC" Ta No "0x00000004" Ta Keep P2_NOPTRACE on Xr execve 2 +.It Dv "P2_AST_SU" Ta No "0x00000008" Ta "Handles SU ast for kthreads" +.It Dv "P2_PTRACE_FSTP" Ta No "0x00000010" Ta "SIGSTOP from PT_ATTACH not yet handled" +.It Dv "P2_TRAPCAP" Ta No "0x00000020" Ta "SIGTRAP on ENOTCAPABLE" +.It Dv "P2_ASLR_ENABLE" Ta No "0x00000040" Ta "Force enable ASLR" +.It Dv "P2_ASLR_DISABLE" Ta No "0x00000080" Ta "Force disable ASLR" +.It Dv "P2_ASLR_IGNSTART" Ta No "0x00000100" Ta "Enable ASLR to consume sbrk area" +.It Dv "P2_PROTMAX_ENABLE" Ta No "0x00000200" Ta "Force enable implied PROT_MAX" +.It Dv "P2_PROTMAX_DISABLE" Ta No "0x00000400" Ta "Force disable implied PROT_MAX" +.It Dv "P2_STKGAP_DISABLE" Ta No "0x00000800" Ta "Disable stack gap for MAP_STACK" +.It Dv "P2_STKGAP_DISABLE_EXEC" Ta No "0x00001000" Ta "Stack gap disabled after exec" +.It Dv "P2_ITSTOPPED" Ta No "0x00002000" Ta "itimers stopped (as part of process stop)" +.It Dv "P2_PTRACEREQ" Ta No "0x00004000" Ta "Active ptrace req" +.It Dv "P2_NO_NEW_PRIVS" Ta No "0x00008000" Ta "Ignore setuid on exec" +.It Dv "P2_WXORX_DISABLE" Ta No "0x00010000" Ta "WX mappings enabled" +.It Dv "P2_WXORX_ENABLE_EXEC" Ta No "0x00020000" Ta "WxorX enabled after exec" +.It Dv "P2_WEXIT" Ta No "0x00040000" Ta "Internal exit early state" +.It Dv "P2_REAPKILLED" Ta No "0x00080000" Ta "REAP_KILL pass handled the process" +.It Dv "P2_MEMBAR_PRIVE" Ta No "0x00100000" Ta "membarrier private expedited registered" +.It Dv "P2_MEMBAR_PRIVE_SYNCORE" Ta No "0x00200000" Ta "membarrier private expedited sync core registered" +.It Dv "P2_MEMBAR_GLOBE" Ta No "0x00400000" Ta "membar global expedited registered" +.El +.It Cm label +The MAC label of the process. +.It Cm lim +The soft limit on memory used, specified via a call to +.Xr setrlimit 2 . +.It Cm lstart +The exact time the command started, using the +.Ql %c +format described in +.Xr strftime 3 . +.It Cm lockname +The name of the lock that the process is currently blocked on. +If the name is invalid or unknown, then +.Dq ???\& +is displayed. +.It Cm logname +The login name associated with the session the process is in (see +.Xr getlogin 2 ) . +.It Cm mwchan +The event name if the process is blocked normally, or the lock name if +the process is blocked on a lock. +See the wchan and lockname keywords +for details. +.It Cm nice +The process scheduling increment (see +.Xr setpriority 2 ) . +.It Cm rss +the real memory (resident set) size of the process in KiB. +.It Cm start +The time the command started. +If the command started less than 24 hours ago, the start time is +displayed using the +.Dq Li %H:%M +format described in +.Xr strftime 3 . +If the command started less than 7 days ago, the start time is +displayed using the +.Dq Li %a%H +format. +Otherwise, the start time is displayed using the +.Dq Li %e%b%y +format. +.It Cm sig +The bitmask of signals pending in the process queue if the +.Fl H +option has not been specified, else the per-thread queue of pending signals. +.It Cm state +The state is given by a sequence of characters, for example, +.Dq Li RWNA . +The first character indicates the run state of the process: +.Pp +.Bl -tag -width indent -compact +.It Li D +Marks a process in disk (or other short term, uninterruptible) wait. +.It Li I +Marks a process that is idle (sleeping for longer than about 20 seconds). +.It Li L +Marks a process that is waiting to acquire a lock. +.It Li R +Marks a runnable process. +.It Li S +Marks a process that is sleeping for less than about 20 seconds. +.It Li T +Marks a stopped process. +.It Li W +Marks an idle interrupt thread. +.It Li Z +Marks a dead process (a +.Dq zombie ) . +.El +.Pp +Additional characters after these, if any, indicate additional state +information: +.Pp +.Bl -tag -width indent -compact +.It Li + +The process is in the foreground process group of its control terminal. +.It Li < +The process has raised CPU scheduling priority. +.It Li C +The process is in +.Xr capsicum 4 +capability mode. +.It Li E +The process is trying to exit. +.It Li J +Marks a process which is in +.Xr jail 2 . +The hostname of the prison can be found in +.Pa /proc/ Ns Ao Ar pid Ac Ns Pa /status . +.It Li L +The process has pages locked in core (for example, for raw I/O). +.It Li N +The process has reduced CPU scheduling priority (see +.Xr setpriority 2 ) . +.It Li s +The process is a session leader. +.It Li V +The process' parent is suspended during a +.Xr vfork 2 , +waiting for the process to exec or exit. +.It Li X +The process is being traced or debugged. +.El +.It Cm tt +An abbreviation for the pathname of the controlling terminal, if any. +The abbreviation consists of the three letters following +.Pa /dev/tty , +or, for pseudo-terminals, the corresponding entry in +.Pa /dev/pts . +This is followed by a +.Ql - +if the process can no longer reach that +controlling terminal (i.e., it has been revoked). +A +.Ql - +without a preceding two letter abbreviation or pseudo-terminal device number +indicates a process which never had a controlling terminal. +The full pathname of the controlling terminal is available via the +.Cm tty +keyword. +.It Cm wchan +The event (an address in the system) on which a process waits. +When printed numerically, the initial part of the address is +trimmed off and the result is printed in hex, for example, 0x80324000 prints +as 324000. +.El +.Sh ENVIRONMENT +The following environment variables affect the execution of +.Nm : +.Bl -tag -width ".Ev COLUMNS" +.It Ev COLUMNS +If set, specifies the user's preferred output width in column positions. +Only affects the traditional text style output. +Please see the preamble of this manual page on how the final output width is +determined. +.El +.Sh FILES +.Bl -tag -width ".Pa /boot/kernel/kernel" -compact +.It Pa /boot/kernel/kernel +default system namelist +.El +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +Display information on all system processes: +.Pp +.Dl $ ps -auxw +.Sh SEE ALSO +.Xr kill 1 , +.Xr pgrep 1 , +.Xr pkill 1 , +.Xr procstat 1 , +.Xr w 1 , +.Xr kvm 3 , +.Xr libxo 3 , +.Xr strftime 3 , +.Xr xo_options 7 , +.Xr mac 4 , +.Xr procfs 4 , +.Xr pstat 8 , +.Xr sysctl 8 , +.Xr mutex 9 +.Sh STANDARDS +For historical reasons, the +.Nm +utility under +.Fx +supports a different set of options from what is described by +.St -p1003.1-2024 +and what is supported on +.No non- Ns Bx +operating systems. +.Pp +In particular, and contrary to this implementation, POSIX specifies that option +.Fl d +should serve to select all processes except session leaders, option +.Fl e +to select all processes +.Po +equivalently to +.Fl A +.Pc , +and option +.Fl u +to select processes by effective user ID. +.Pp +However, options +.Fl A , a , G , l , o , p , U , +and +.Fl t +behave as prescribed by +.St -p1003.1-2024 . +Options +.Fl f +and +.Fl w +currently do not, but may be changed to in the future. +.Pp +POSIX's option +.Fl g , +to select processes having the specified processes as their session leader, is +not implemented. +However, other UNIX systems that provide this functionality do so via option +.Fl s +instead, reserving +.Fl g +to query by group leaders. +.Sh HISTORY +The +.Nm +command appeared in +.At v3 +in section 8 of the manual. +.Sh BUGS +Since +.Nm +cannot run faster than the system and is run as any other scheduled +process, the information it displays can never be exact. +.Pp +.Nm ps +currently does not correctly limit the ouput width, and in most cases does not +limit it at all when it should. +Regardless of the target width, requested columns are always all printed and +with widths allowing to entirely print their longest values, except for columns +with keyword +.Cm command +or +.Cm args +that are not last in the display +.Pq they are truncated to 16 bytes , +and for the last column in the display if its keyword requests textual +information of variable length, such as the +.Cm command , jail , +and +.Cm user +keywords do. +This considerably limits the effects and usefulness of the terminal width on the +output, and consequently that of the +.Ev COLUMNS +environment variable and the +.Fl w +option +.Pq if specified only once . +.Pp +The +.Nm +utility does not correctly display argument lists containing multibyte +characters. @@ -0,0 +1,638 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * Copyright (c) 2025 The FreeBSD Foundation + * Copyright (c) 2026 Project Tick. All rights reserved. + * + * Portions of this software were developed by Olivier Certner + * <olce@FreeBSD.org> at Kumacom SARL under sponsorship from the FreeBSD + * Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ------+---------+---------+-------- + --------+---------+---------+---------* + * Copyright (c) 2004 - Garance Alistair Drosehn <gad@FreeBSD.org>. + * All rights reserved. + * + * Significant modifications made to bring `ps' options somewhat closer + * to the standard for `ps' as described in SingleUnixSpec-v3. + * ------+---------+---------+-------- + --------+---------+---------+---------* + */ + +#include <sys/param.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/time.h> +#include <sys/resource.h> +#include <sys/sysmacros.h> + +#include <ctype.h> +#include <dirent.h> +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <grp.h> +#include <limits.h> +#include <locale.h> +#include <paths.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <time.h> + +#include "ps.h" + +/* Globals */ +int cflag; /* -c */ +int eval; /* Exit value */ +time_t now; /* Current time(3) value */ +int rawcpu; /* -C */ +int sumrusage; /* -S */ +int termwidth; /* Width of the screen (0 == infinity). */ +int showthreads; /* will threads be shown? */ +int hlines; /* repeat headers every N lines */ + +struct velisthead varlist = STAILQ_HEAD_INITIALIZER(varlist); +struct velisthead Ovarlist = STAILQ_HEAD_INITIALIZER(Ovarlist); + +static int needcomm, needenv, needuser, optfatal; +static enum sort { DEFAULT, SORTMEM, SORTCPU } sortby = DEFAULT; + +static long clk_tck; +static double system_uptime; + +struct listinfo { + int count; + int maxcount; + int elemsize; + int (*addelem)(struct listinfo *, const char *); + const char *lname; + void *l; +}; + +/* Forward declarations */ +static int addelem_gid(struct listinfo *, const char *); +static int addelem_pid(struct listinfo *, const char *); +static int addelem_tty(struct listinfo *, const char *); +static int addelem_uid(struct listinfo *, const char *); +static void add_list(struct listinfo *, const char *); +static int pscomp(const void *, const void *); +static void scan_vars(void); +static void usage(void) __attribute__((__noreturn__)); +static int scan_processes(KINFO **kinfop, int *nentries); +static double get_system_uptime(void); +static char *kludge_oldps_options(const char *, char *, const char *); + +static const char dfmt[] = "pid,tt,state,time,command"; +static const char jfmt[] = "user,pid,ppid,pgid,sid,jobc,state,tt,time,command"; +static const char lfmt[] = "uid,pid,ppid,cpu,pri,nice,vsz,rss,mwchan,state,tt,time,command"; +static const char ufmt[] = "user,pid,%cpu,%mem,vsz,rss,tt,state,start,time,command"; +static const char vfmt[] = "pid,state,time,sl,vsz,rss,lim,tsiz,%cpu,%mem,command"; + +#define PS_ARGS "AaCcD:defG:gHhjJ:LlM:mN:O:o:p:rSTt:U:uvwXxZ" + +int +main(int argc, char *argv[]) +{ + struct listinfo gidlist, pidlist, ruidlist, sesslist, ttylist, uidlist, pgrplist; + KINFO *kinfo = NULL; + struct varent *vent; + struct winsize ws; + char *cols; + int all, ch, i, nentries, nkept, nselectors, wflag, xkeep, xkeep_implied; + + setlocale(LC_ALL, ""); + time(&now); + + clk_tck = sysconf(_SC_CLK_TCK); + system_uptime = get_system_uptime(); + + if ((cols = getenv("COLUMNS")) != NULL && *cols != '\0') + termwidth = atoi(cols); + else if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) { + termwidth = ws.ws_col - 1; + if (ws.ws_row > 0) hlines = ws.ws_row - 1; + } else { + termwidth = 79; + hlines = 22; + } + /* -h default is active only if -h is passed, so we use hlines as the value if hflag set */ + int hflag = 0; + + if (argc > 1) + argv[1] = kludge_oldps_options(PS_ARGS, argv[1], argv[2]); + + all = nselectors = optfatal = wflag = xkeep_implied = 0; + xkeep = -1; + + memset(&gidlist, 0, sizeof(gidlist)); gidlist.addelem = addelem_gid; gidlist.elemsize = sizeof(gid_t); + memset(&pidlist, 0, sizeof(pidlist)); pidlist.addelem = addelem_pid; pidlist.elemsize = sizeof(pid_t); + memset(&ruidlist, 0, sizeof(ruidlist)); ruidlist.addelem = addelem_uid; ruidlist.elemsize = sizeof(uid_t); + memset(&sesslist, 0, sizeof(sesslist)); sesslist.addelem = addelem_pid; sesslist.elemsize = sizeof(pid_t); + memset(&ttylist, 0, sizeof(ttylist)); ttylist.addelem = addelem_tty; ttylist.elemsize = sizeof(dev_t); + memset(&uidlist, 0, sizeof(uidlist)); uidlist.addelem = addelem_uid; uidlist.elemsize = sizeof(uid_t); + memset(&pgrplist, 0, sizeof(pgrplist)); pgrplist.addelem = addelem_pid; pgrplist.elemsize = sizeof(pid_t); + + int _fmt = 0; + while ((ch = getopt(argc, argv, PS_ARGS)) != -1) + switch (ch) { + case 'A': all = xkeep = 1; break; + case 'a': all = 1; break; + case 'C': rawcpu = 1; break; + case 'c': cflag = 1; break; + case 'e': needenv = 1; break; + case 'f': /* default in FreeBSD */ break; + case 'G': add_list(&gidlist, optarg); xkeep_implied = 1; nselectors++; break; + case 'g': /* BSD compat: leaders (no-op on Linux for now) */ break; + case 'H': showthreads = KERN_PROC_INC_THREAD; break; + case 'h': hflag = 1; break; + case 'j': parsefmt(jfmt, &varlist, 0); _fmt = 1; break; + case 'L': showkey(); exit(0); + case 'l': parsefmt(lfmt, &varlist, 0); _fmt = 1; break; + case 'M': case 'N': warnx("-%c not supported on Linux", ch); break; + case 'm': sortby = SORTMEM; break; + case 'O': parsefmt(optarg, &Ovarlist, 1); break; + case 'o': parsefmt(optarg, &varlist, 1); _fmt = 1; break; + case 'p': add_list(&pidlist, optarg); nselectors++; break; + case 'r': sortby = SORTCPU; break; + case 'S': sumrusage = 1; break; + case 'T': add_list(&ttylist, ttyname(STDIN_FILENO)); xkeep_implied = 1; nselectors++; break; + case 't': add_list(&ttylist, optarg); xkeep_implied = 1; nselectors++; break; + case 'U': add_list(&ruidlist, optarg); xkeep_implied = 1; nselectors++; break; + case 'u': parsefmt(ufmt, &varlist, 0); sortby = SORTCPU; _fmt = 1; break; + case 'v': parsefmt(vfmt, &varlist, 0); sortby = SORTMEM; _fmt = 1; break; + case 'w': if (wflag) termwidth = 0; else if (termwidth < 131) termwidth = 131; wflag++; break; + case 'X': xkeep = 0; break; + case 'x': xkeep = 1; break; + case 'Z': parsefmt("label", &varlist, 0); break; + default: usage(); + } + + argc -= optind; argv += optind; + while (*argv && isdigit(**argv)) { add_list(&pidlist, *argv); nselectors++; argv++; } + if (*argv) errx(1, "illegal argument: %s", *argv); + if (optfatal) exit(1); + if (xkeep < 0) xkeep = xkeep_implied; + if (!hflag) hlines = 0; + + if (!_fmt) parsefmt(dfmt, &varlist, 0); + if (!STAILQ_EMPTY(&Ovarlist)) { + /* Simple join for now */ + STAILQ_CONCAT(&varlist, &Ovarlist); + } + + if (STAILQ_EMPTY(&varlist)) { + warnx("no keywords specified"); + showkey(); + exit(1); + } + + scan_vars(); + + if (all) nselectors = 0; + else if (nselectors == 0) { + uid_t me = geteuid(); + uidlist.l = realloc(uidlist.l, sizeof(uid_t)); + ((uid_t*)uidlist.l)[0] = me; + uidlist.count = 1; + nselectors = 1; + } + + if (scan_processes(&kinfo, &nentries) < 0) err(1, "scan_processes"); + + for (i = 0; i < nentries; i++) + kinfo[i].ki_pcpu = getpcpu(&kinfo[i]); + + nkept = 0; + KINFO *kept = malloc(nentries * sizeof(KINFO)); + if (!kept) err(1, "malloc"); + + for (i = 0; i < nentries; i++) { + struct kinfo_proc *kp = kinfo[i].ki_p; + int match = 0; + if (pidlist.count > 0) { + for (int j = 0; j < pidlist.count; j++) { + // printf("Checking pid %d against %d\n", kp->ki_pid, ((pid_t*)pidlist.l)[j]); + if (kp->ki_pid == ((pid_t*)pidlist.l)[j]) { match = 1; break; } + } + } + if (!match && xkeep == 0 && (kp->ki_tdev == (dev_t)-1)) continue; + if (!match && nselectors > 0) { + if (gidlist.count > 0) { + for (int j = 0; j < gidlist.count; j++) + if (kp->ki_groups[0] == ((gid_t*)gidlist.l)[j]) { match = 1; break; } + } + if (!match && ruidlist.count > 0) { + for (int j = 0; j < ruidlist.count; j++) + if (kp->ki_ruid == ((uid_t*)ruidlist.l)[j]) { match = 1; break; } + } + if (!match && uidlist.count > 0) { + for (int j = 0; j < uidlist.count; j++) + if (kp->ki_uid == ((uid_t*)uidlist.l)[j]) { match = 1; break; } + } + if (!match && ttylist.count > 0) { + for (int j = 0; j < ttylist.count; j++) + if (kp->ki_tdev == ((dev_t*)ttylist.l)[j]) { match = 1; break; } + } + if (!match) continue; + } else if (!match && xkeep == 0 && (kp->ki_tdev == (dev_t)-1)) continue; + + kept[nkept++] = kinfo[i]; + } + + if (nkept == 0) { + printheader(); + exit(1); + } + + qsort(kept, nkept, sizeof(KINFO), pscomp); + + printheader(); + for (i = 0; i < nkept; i++) { + if (hlines > 0 && i > 0 && i % hlines == 0) printheader(); + int linelen = 0; + STAILQ_FOREACH(vent, &varlist, next_ve) { + char *str = vent->var->oproc(&kept[i], vent); + if (!str) str = strdup("-"); + int width = vent->width; + if (STAILQ_NEXT(vent, next_ve) == NULL) { + if (termwidth > 0 && linelen + (int)strlen(str) > termwidth) { + int avail = termwidth - linelen; + if (avail > 0) str[avail] = '\0'; + } + printf("%s", str); + } else { + if (vent->var->flag & LJUST) printf("%-*s ", width, str); + else printf("%*s ", width, str); + linelen += width + 1; + } + free(str); + } + printf("\n"); + } + + for (i = 0; i < nentries; i++) { + KINFO_STR *ks; + while (!STAILQ_EMPTY(&kinfo[i].ki_ks)) { + ks = STAILQ_FIRST(&kinfo[i].ki_ks); + STAILQ_REMOVE_HEAD(&kinfo[i].ki_ks, ks_next); + free(ks->ks_str); + free(ks); + } + free(kinfo[i].ki_p); + free(kinfo[i].ki_args); + free(kinfo[i].ki_env); + } + free(kinfo); + free(kept); + free_devnames(); + + return 0; +} + +static void +scan_vars(void) +{ + struct varent *vent; + STAILQ_FOREACH(vent, &varlist, next_ve) { + if (vent->var->flag & COMM) needcomm = 1; + if (vent->var->flag & USER) needuser = 1; + } +} + +static int +addelem_pid(struct listinfo *inf, const char *arg) +{ + long v = strtol(arg, NULL, 10); + inf->l = realloc(inf->l, (inf->count + 1) * sizeof(pid_t)); + ((pid_t*)inf->l)[inf->count++] = (pid_t)v; + return 0; +} + +static int +addelem_uid(struct listinfo *inf, const char *arg) +{ + struct passwd *pw = getpwnam(arg); + uid_t uid = pw ? pw->pw_uid : (uid_t)atoi(arg); + inf->l = realloc(inf->l, (inf->count + 1) * sizeof(uid_t)); + ((uid_t*)inf->l)[inf->count++] = uid; + return 0; +} + +static int +addelem_gid(struct listinfo *inf, const char *arg) +{ + struct group *gr = getgrnam(arg); + gid_t gid = gr ? gr->gr_gid : (gid_t)atoi(arg); + inf->l = realloc(inf->l, (inf->count + 1) * sizeof(gid_t)); + ((gid_t*)inf->l)[inf->count++] = gid; + return 0; +} + +static int +addelem_tty(struct listinfo *inf, const char *arg) +{ + char path[PATH_MAX]; + struct stat st; + if (arg[0] != '/') snprintf(path, sizeof(path), "/dev/%s", arg); + else snprintf(path, sizeof(path), "%s", arg); + if (stat(path, &st) == 0 && S_ISCHR(st.st_mode)) { + inf->l = realloc(inf->l, (inf->count + 1) * sizeof(dev_t)); + ((dev_t*)inf->l)[inf->count++] = st.st_rdev; + return 0; + } + return -1; +} + +static void +add_list(struct listinfo *inf, const char *arg) +{ + char *copy = strdup(arg); + char *p = copy, *token; + while ((token = strsep(&p, " \t,")) != NULL) { + if (*token == '\0') continue; + if (inf->addelem(inf, token) < 0) optfatal = 1; + } + free(copy); +} + +static int +pscomp(const void *a, const void *b) +{ + const KINFO *ka = a, *kb = b; + if (sortby == SORTMEM) { + if (ka->ki_p->ki_rssize < kb->ki_p->ki_rssize) return 1; + if (ka->ki_p->ki_rssize > kb->ki_p->ki_rssize) return -1; + return 0; + } + if (sortby == SORTCPU) { + if (ka->ki_pcpu < kb->ki_pcpu) return 1; + if (ka->ki_pcpu > kb->ki_pcpu) return -1; + return 0; + } + if (ka->ki_p->ki_pid < kb->ki_p->ki_pid) return -1; + if (ka->ki_p->ki_pid > kb->ki_p->ki_pid) return 1; + return 0; +} + +static double +get_system_uptime(void) +{ + FILE *fp = fopen("/proc/uptime", "r"); + double uptime = 0.0; + if (fp) { + if (fscanf(fp, "%lf", &uptime) != 1) uptime = 0.0; + fclose(fp); + } + return uptime; +} + +static long long +safe_strtoll(const char *s) +{ + char *end; + if (s == NULL || *s == '\0') return 0; + long long val = strtoll(s, &end, 10); + if (end == s) return 0; + return val; +} + +static unsigned long +safe_strtoul(const char *s) +{ + char *end; + if (s == NULL || *s == '\0') return 0; + unsigned long val = strtoul(s, &end, 10); + if (end == s) return 0; + return val; +} + +/* Linux-specific /proc scanning */ + +static char* +read_file(const char *path) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) return NULL; + char *buf = malloc(8192); + if (!buf) { close(fd); return NULL; } + ssize_t n = read(fd, buf, 8191); + close(fd); + if (n <= 0) { free(buf); return NULL; } + buf[n] = '\0'; + return buf; +} + +static int +read_proc_stat(pid_t pid, struct kinfo_proc *ki) +{ + char path[64], *buf; + snprintf(path, sizeof(path), "/proc/%d/stat", pid); + buf = read_file(path); + if (!buf) return -1; + + char *p = strrchr(buf, ')'); + if (!p) { free(buf); return -1; } + + /* Field 1: pid (already known) */ + /* Field 2: comm (already got p) */ + char *comm_start = strchr(buf, '('); + if (comm_start) { + size_t len = p - (comm_start + 1); + if (len >= sizeof(ki->ki_comm)) len = sizeof(ki->ki_comm) - 1; + memcpy(ki->ki_comm, comm_start + 1, len); + ki->ki_comm[len] = '\0'; + } + + p += 2; /* Skip ") " */ + char *tokens[50]; + int ntok = 0; + char *sp; + while ((sp = strsep(&p, " ")) != NULL && ntok < 50) { + tokens[ntok++] = sp; + } + + if (ntok < 20) { free(buf); return -1; } + + ki->ki_stat = (tokens[0] && tokens[0][0]) ? tokens[0][0] : '?'; + ki->ki_ppid = (int)safe_strtoll(tokens[1]); + ki->ki_pgid = (int)safe_strtoll(tokens[2]); + ki->ki_sid = (int)safe_strtoll(tokens[3]); + + unsigned int tty_nr = (unsigned int)safe_strtoul(tokens[4]); + if (tty_nr == 0) { + ki->ki_tdev = (dev_t)-1; + } else { + unsigned int maj = (tty_nr >> 8) & 0xFFF; + unsigned int min = (tty_nr & 0xFF) | ((tty_nr >> 12) & 0xFFF00); + ki->ki_tdev = makedev(maj, min); + } + + ki->ki_tpgid = tokens[5] ? (int)safe_strtoll(tokens[5]) : -1; + ki->ki_flag = (long)safe_strtoll(tokens[6]); + + long long utime = safe_strtoll(tokens[11]); + long long stime = safe_strtoll(tokens[12]); + ki->ki_runtime = (uint64_t)((utime + stime) * 1000000 / clk_tck); + + ki->ki_pri = (int)safe_strtoll(tokens[15]); + ki->ki_nice = (int)safe_strtoll(tokens[16]); + ki->ki_numthreads = (ntok > 17) ? (int)safe_strtoll(tokens[17]) : 1; + if (ki->ki_numthreads <= 0) ki->ki_numthreads = 1; + + if (ntok > 19 && tokens[19]) { + double boot_time = (double)now - system_uptime; + double start_ticks = (double)safe_strtoll(tokens[19]); + ki->ki_start.tv_sec = (time_t)(boot_time + (start_ticks / clk_tck)); + ki->ki_start.tv_usec = 0; + } else { + ki->ki_start.tv_sec = now; + ki->ki_start.tv_usec = 0; + } + + ki->ki_size = (ntok > 20) ? (uint64_t)safe_strtoll(tokens[20]) : 0; + ki->ki_rssize = (ntok > 21) ? (uint64_t)safe_strtoll(tokens[21]) * (getpagesize() / 1024) : 0; + + free(buf); + return 0; +} + +static int +read_proc_status(pid_t pid, struct kinfo_proc *ki) +{ + char path[64]; + snprintf(path, sizeof(path), "/proc/%d/status", pid); + FILE *fp = fopen(path, "r"); + if (!fp) return -1; + char line[256]; + while (fgets(line, sizeof(line), fp)) { + if (strncmp(line, "Uid:", 4) == 0) { + sscanf(line + 4, "%u %u %u", &ki->ki_ruid, &ki->ki_uid, &ki->ki_svuid); + } else if (strncmp(line, "Gid:", 4) == 0) { + sscanf(line + 4, "%u %u %u", &ki->ki_rgid, &ki->ki_groups[0], &ki->ki_svgid); + } + } + fclose(fp); + return 0; +} + +static char* +read_proc_cmdline(pid_t pid) +{ + char path[64]; + snprintf(path, sizeof(path), "/proc/%d/cmdline", pid); + int fd = open(path, O_RDONLY); + if (fd < 0) return NULL; + char *buf = malloc(4096); + if (!buf) { close(fd); return NULL; } + ssize_t n = read(fd, buf, 4095); + close(fd); + if (n <= 0) { free(buf); return NULL; } + for (int i = 0; i < n - 1; i++) if (buf[i] == '\0') buf[i] = ' '; + buf[n] = '\0'; + return buf; +} + +static char* +read_proc_environ(pid_t pid) +{ + char path[64]; + snprintf(path, sizeof(path), "/proc/%d/environ", pid); + int fd = open(path, O_RDONLY); + if (fd < 0) return NULL; + char *buf = malloc(4096); + if (!buf) { close(fd); return NULL; } + ssize_t n = read(fd, buf, 4095); + close(fd); + if (n <= 0) { free(buf); return NULL; } + for (int i = 0; i < n - 1; i++) if (buf[i] == '\0') buf[i] = ' '; + buf[n] = '\0'; + return buf; +} + +static int +scan_processes(KINFO **kinfop, int *nentries) +{ + DIR *dir = opendir("/proc"); + if (!dir) return -1; + struct dirent *ent; + int count = 0, cap = 128; + KINFO *k = malloc(cap * sizeof(KINFO)); + + while ((ent = readdir(dir))) { + if (!isdigit(ent->d_name[0])) continue; + pid_t pid = atoi(ent->d_name); + struct kinfo_proc *kp = calloc(1, sizeof(struct kinfo_proc)); + kp->ki_pid = pid; + if (read_proc_stat(pid, kp) < 0 || read_proc_status(pid, kp) < 0) { + free(kp); + continue; + } + // printf("Scanned pid %d: comm=%s ppid=%d\n", kp->ki_pid, kp->ki_comm, kp->ki_ppid); + + if (count >= cap) { + cap *= 2; + k = realloc(k, cap * sizeof(KINFO)); + } + k[count].ki_p = kp; + k[count].ki_valid = 1; + k[count].ki_args = needcomm ? read_proc_cmdline(pid) : NULL; + if (!k[count].ki_args) k[count].ki_args = strdup(kp->ki_comm); + k[count].ki_env = (needenv) ? read_proc_environ(pid) : NULL; + k[count].ki_pcpu = 0; /* filled later if needed */ + k[count].ki_memsize = kp->ki_rssize * getpagesize(); + k[count].ki_d.prefix = NULL; + STAILQ_INIT(&k[count].ki_ks); + count++; + } + closedir(dir); + *kinfop = k; + *nentries = count; + return 0; +} + +static void +usage(void) +{ + fprintf(stderr, "usage: ps [%s]\n", PS_ARGS); + exit(1); +} + +static char * +kludge_oldps_options(const char *optstring, char *arg, const char *nextarg) +{ + /* Simple version of BSD kludge: if first arg doesn't start with '-', prepend one */ + if (arg && arg[0] != '-') { + char *newarg = malloc(strlen(arg) + 2); + newarg[0] = '-'; + strcpy(newarg + 1, arg); + return newarg; + } + return arg; +} @@ -0,0 +1,205 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1990, 1993 + * The Regents of the University of California. All rights reserved. + * Copyright (c) 2026 Project Tick. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * Linux-native port: replaces BSD kinfo_proc with a Linux-compatible equivalent + * that can be filled from /proc. + */ + +#ifndef _PS_H_ +#define _PS_H_ + +#include <sys/types.h> +#include <sys/time.h> +#include <sys/resource.h> +#include <sys/stat.h> +#include <stdint.h> +#include <stdbool.h> + +/* Minimal STAILQ for musl-gcc compatibility */ +#define STAILQ_HEAD(name, type) \ +struct name { struct type *stqh_first; struct type **stqh_last; } +#define STAILQ_HEAD_INITIALIZER(head) { NULL, &(head).stqh_first } +#define STAILQ_ENTRY(type) \ +struct { struct type *stqe_next; } +#define STAILQ_FIRST(head) ((head)->stqh_first) +#define STAILQ_NEXT(elm, field) ((elm)->field.stqe_next) +#define STAILQ_EMPTY(head) ((head)->stqh_first == NULL) +#define STAILQ_INIT(head) do { (head)->stqh_first = NULL; (head)->stqh_last = &(head)->stqh_first; } while (0) +#define STAILQ_INSERT_TAIL(head, elm, field) do { \ + (elm)->field.stqe_next = NULL; \ + *(head)->stqh_last = (elm); \ + (head)->stqh_last = &(elm)->field.stqe_next; \ +} while (0) +#define STAILQ_FOREACH(var, head, field) \ + for ((var) = STAILQ_FIRST(head); (var); (var) = STAILQ_NEXT(var, field)) +#define STAILQ_CONCAT(head1, head2) do { \ + if (!STAILQ_EMPTY(head2)) { \ + *(head1)->stqh_last = (head2)->stqh_first; \ + (head1)->stqh_last = (head2)->stqh_last; \ + STAILQ_INIT(head2); \ + } \ +} while (0) +#define STAILQ_REMOVE_HEAD(head, field) do { \ + if (((head)->stqh_first = (head)->stqh_first->field.stqe_next) == NULL) \ + (head)->stqh_last = &(head)->stqh_first; \ +} while (0) + +#define UNLIMITED 0 + +#ifndef __unused +#define __unused __attribute__((__unused__)) +#endif + +#define COMMLEN 256 +#define WMESGLEN 64 +#define KI_NGROUPS 16 + +/* Linux-port version of kinfo_proc to store data from /proc */ +struct kinfo_proc { + pid_t ki_pid; + pid_t ki_ppid; + pid_t ki_pgid; + pid_t ki_sid; + dev_t ki_tdev; + pid_t ki_tpgid; + uid_t ki_uid; + uid_t ki_ruid; + uid_t ki_svuid; + gid_t ki_groups[KI_NGROUPS]; + gid_t ki_rgid; + gid_t ki_svgid; + char ki_comm[COMMLEN]; + char ki_tdname[64]; + char ki_moretdname[64]; + struct timeval ki_start; + uint64_t ki_runtime; // in microseconds + uint64_t ki_size; // VSZ in bytes + uint64_t ki_rssize; // RSS in pages + uint64_t ki_tsize; // Text size in pages (hard to get precise on Linux) + uint64_t ki_dsize; // Data size in pages + uint64_t ki_ssize; // Stack size in pages + int ki_nice; + int ki_pri_level; + int ki_pri_class; + char ki_stat; // BSD-like state character (S, R, T, Z, D) + long ki_flag; // Linux task flags + char ki_wmesg[WMESGLEN]; + int ki_numthreads; + long ki_slptime; // time since last running (approximate) + int ki_oncpu; + int ki_lastcpu; + int ki_jid; // Always 0 on Linux + char ki_login[64]; // placeholder for logname + struct rusage ki_rusage; // approximate from /proc/pid/stat + struct timeval ki_childtime; // placeholder for sumrusage + struct timeval ki_childstime; + struct timeval ki_childutime; +}; + +/* Compatibility aliases for FreeBSD KVM-based fields used in print.c */ +#define ki_pri ki_pri_level + +typedef long fixpt_t; +typedef uint64_t segsz_t; + +enum type { UNSPEC, CHAR, UCHAR, SHORT, USHORT, INT, UINT, LONG, ULONG, KPTR, PGTOK }; + +typedef struct kinfo_str { + STAILQ_ENTRY(kinfo_str) ks_next; + char *ks_str; /* formatted string */ +} KINFO_STR; + +typedef struct kinfo { + struct kinfo_proc *ki_p; /* kinfo_proc structure */ + char *ki_args; /* exec args (heap) */ + char *ki_env; /* environment (heap) */ + int ki_valid; /* 1 => data valid */ + double ki_pcpu; /* calculated in main() */ + segsz_t ki_memsize; /* calculated in main() */ + union { + int level; /* used in descendant_sort() */ + char *prefix; /* prefix string for tree-view */ + } ki_d; + STAILQ_HEAD(, kinfo_str) ki_ks; +} KINFO; + +struct var; +typedef struct var VAR; + +typedef struct varent { + STAILQ_ENTRY(varent) next_ve; + const char *header; + const struct var *var; + u_int width; +#define VE_KEEP (1 << 0) + uint16_t flags; +} VARENT; + +STAILQ_HEAD(velisthead, varent); + +struct var { + const char *name; + union { + const char *aliased; + const VAR *final_kw; + }; + const char *header; + const char *field; +#define COMM 0x01 +#define LJUST 0x02 +#define USER 0x04 +#define INF127 0x10 +#define NOINHERIT 0x1000 +#define RESOLVING_ALIAS 0x10000 +#define RESOLVED_ALIAS 0x20000 + u_int flag; + char *(*oproc)(struct kinfo *, struct varent *); + size_t off; + enum type type; + const char *fmt; +}; + +#define KERN_PROC_PROC 0 +#define KERN_PROC_THREAD 1 +#define KERN_PROC_INC_THREAD 2 +#define KERN_PROC_ALL 3 + +/* Don't define these if they conflict with system headers */ +#ifndef NZERO +#define NZERO 20 +#endif + +/* Linux PRI mapping */ +#define PUSER 0 + +#include "extern.h" + +#endif /* _PS_H_ */ diff --git a/tests/spin_helper.c b/tests/spin_helper.c new file mode 100644 index 0000000000..eef1d0e3b7 --- /dev/null +++ b/tests/spin_helper.c @@ -0,0 +1,45 @@ +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <signal.h> +#include <string.h> +#include <sys/prctl.h> + +/* + * Small helper to create processes with predictable names/properties for ps tests. + */ + +static void +handle_sigterm(int sig) +{ + (void)sig; + exit(0); +} + +int +main(int argc, char **argv) +{ + if (argc < 2) { + fprintf(stderr, "usage: %s <name> [arg...]\n", argv[0]); + return (1); + } + + /* Set name for /proc/self/comm (visible in ps -c) */ + prctl(PR_SET_NAME, argv[1], 0, 0, 0); + + signal(SIGTERM, handle_sigterm); + + /* Busy wait in background to consume some CPU time if needed */ + if (argc > 2 && strcmp(argv[2], "busy") == 0) { + while (1) { + /* Do nothing, just spin */ + } + } + + /* Normal idle wait */ + while (1) { + pause(); + } + + return (0); +} diff --git a/tests/test.sh b/tests/test.sh new file mode 100644 index 0000000000..9bfc009a99 --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,147 @@ +#!/bin/sh + +# ps tests +# +# Assumptions: +# - PS_BIN points to the newly built ps +# - TEST_HELPER points to tests/spin_helper.c compiled into out/tests/spin_helper + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +fail() { + printf "${RED}FAIL: %s${NC}\n" "$1" >&2 + exit 1 +} + +pass() { + printf "${GREEN}PASS: %s${NC}\n" "$1" +} + +# Compile helper if not already there +HELPER_DIR="$(dirname "$0")" +OUT_DIR="$(dirname "${PS_BIN}")" +HELPER_BIN="${OUT_DIR}/spin_helper" + +if [ ! -f "${HELPER_BIN}" ]; then + cc -O2 "${HELPER_DIR}/spin_helper.c" -o "${HELPER_BIN}" || fail "failed to compile spin_helper" +fi + +# 1. Basic sanity: run without arguments +# Should output at least headers and some processes. +${PS_BIN} > /tmp/ps_out || fail "ps failed to run" +grep -q "PID" /tmp/ps_out || fail "ps output lacks PID header" +grep -q "COMMAND" /tmp/ps_out || fail "ps output lacks COMMAND header" +pass "Basic sanity (no args)" + +# 2. Process selection: -p +# Start a helper, then find it with ps. +${HELPER_BIN} test-ps-select & +HELPER_PID=$! +sleep 0.1 +${PS_BIN} -p ${HELPER_PID} > /tmp/ps_p_out || fail "ps -p failed" +grep -q "${HELPER_PID}" /tmp/ps_p_out || fail "ps -p did not find helper pid ${HELPER_PID}" +kill ${HELPER_PID} +pass "Process selection (-p pid)" + +# 3. Format selection: -j +${HELPER_BIN} test-ps-fmt-j & +HELPER_PID=$! +sleep 0.1 +${PS_BIN} -j -p ${HELPER_PID} > /tmp/ps_j_out || fail "ps -j failed" +grep -q "USER" /tmp/ps_j_out || fail "ps -j output lacks USER header" +grep -q "PPID" /tmp/ps_j_out || fail "ps -j output lacks PPID header" +grep -q "SID" /tmp/ps_j_out || fail "ps -j output lacks SID header" +kill ${HELPER_PID} +pass "Format selection (-j)" + +# 4. Format selection: -u +${HELPER_BIN} test-ps-fmt-u & +HELPER_PID=$! +sleep 0.1 +${PS_BIN} -u -p ${HELPER_PID} > /tmp/ps_u_out || fail "ps -u failed" +grep -q "%CPU" /tmp/ps_u_out || fail "ps -u output lacks %CPU header" +grep -q "%MEM" /tmp/ps_u_out || fail "ps -u output lacks %MEM header" +grep -q "VSZ" /tmp/ps_u_out || fail "ps -u output lacks VSZ header" +grep -q "RSS" /tmp/ps_u_out || fail "ps -u output lacks RSS header" +kill ${HELPER_PID} +pass "Format selection (-u)" + +# 5. Format selection: -o (custom) +${HELPER_BIN} test-ps-fmt-o & +HELPER_PID=$! +sleep 0.1 +${PS_BIN} -o pid,comm,nice -p ${HELPER_PID} > /tmp/ps_o_out || fail "ps -o failed" +grep -q "PID" /tmp/ps_o_out || fail "ps -o output lacks PID header" +grep -q "COMMAND" /tmp/ps_o_out || fail "ps -o output lacks COMMAND header" +grep -q "NI" /tmp/ps_o_out || fail "ps -o output lacks NI header" +# First line is header, second line should contain the PID. +grep -q "${HELPER_PID}" /tmp/ps_o_out || fail "ps -o did not find helper pid" +kill ${HELPER_PID} +pass "Format selection (-o pid,comm,nice)" + +# 6. Sorting: -m (by memory) and -r (by cpu) +${PS_BIN} -m -A > /dev/null || fail "ps -m -A failed" +${PS_BIN} -r -A > /dev/null || fail "ps -r -A failed" +pass "Sorting (-m, -r)" + +# 7. Negative test: invalid pid +${PS_BIN} -p 999999 > /tmp/ps_neg_out 2>&1 +# FreeBSD ps should just print header and exits 1 if no matches found? +# Actually, standard behavior varies. Let's check status. +EXIT_CODE=$? +if [ ${EXIT_CODE} -ne 1 ]; then + # In some versions, no match means exit 1. In others, exit 0. + # FreeBSD ps.c: main() calls exit(1) if nkept == 0. + fail "ps -p invalid_pid should exit 1 (got ${EXIT_CODE})" +fi +pass "Negative test (invalid pid)" + +# 8. Negative test: invalid flag +${PS_BIN} -Y > /dev/null 2>&1 +if [ $? -eq 0 ]; then + fail "ps -Y should fail" +fi +pass "Negative test (invalid flag)" + +# 9. Width test: -w +${HELPER_BIN} test-ps-width-very-long-name-to-check-truncation & +HELPER_PID=$! +sleep 0.1 +# Record output without -w (should truncate or at least be limited) +${PS_BIN} -u -p ${HELPER_PID} > /tmp/ps_w1 +# Record output with -ww (no limit) +${PS_BIN} -u -ww -p ${HELPER_PID} > /tmp/ps_w2 +# Compare -w2 should be longer or equal to -w1 in terms of line length +LEN1=$(awk '{ print length }' /tmp/ps_w1 | sort -nr | head -1) +LEN2=$(awk '{ print length }' /tmp/ps_w2 | sort -nr | head -1) +if [ "${LEN2}" -lt "${LEN1}" ]; then + fail "ps -ww output shorter than normal output" +fi +kill ${HELPER_PID} +pass "Width control (-w, -ww)" + +# 10. Headers control: -h +# Start 30 helpers safely. +I=1 +while [ ${I} -le 30 ]; do + ${HELPER_BIN} test-ps-h-${I} & + H_PIDS="${H_PIDS} $!" + I=$((I+1)) +done +sleep 0.1 +# Run ps -h -A (repeat headers every 22 lines or so) +${PS_BIN} -h -A | grep "PID" | wc -l > /tmp/ps_h_count +COUNT=$(cat /tmp/ps_h_count) +if [ "${COUNT}" -lt 2 ]; then + fail "ps -h should repeat headers (got ${COUNT} headers for 30+ processes)" +fi +kill ${H_PIDS} +pass "Header repetition (-h)" + +# Clean up +rm -f /tmp/ps_* +printf "${GREEN}All tests passed!${NC}\n" +exit 0 |
