[PATCHv2] procps: new applet: vmstat
Denys Vlasenko
vda.linux at googlemail.com
Sun Jan 18 09:59:52 UTC 2026
Applied, thank you.
On Thu, Nov 13, 2025 at 10:52 AM David Leonard
<d+busybox at adaptive-enterprises.com> wrote:
>
>
> v2
>
> * fixed small bugs in error handling, averaging
> * fixed compiler warning
> * removed some dev/debug code
>
>
> Adds a compact vmstat applet that matches the default behaviour
> of procps's vmstat.
>
> function old new delta
> vmstat_main - 872 +872
> load_row - 852 +852
> print_row - 322 +322
> coldescs - 239 +239
> .rodata 99999 100119 +120
> find_col - 108 +108
> packed_usage 34738 34759 +21
> applet_main 3224 3232 +8
> applet_names 2782 2789 +7
> ------------------------------------------------------------------------------
> (add/remove: 6/0 grow/shrink: 4/0 up/down: 2549/0) Total: 2549 bytes
> text data bss dec hex filename
> 1052603 16691 1664 1070958 10576e busybox_old
> 1055168 16699 1664 1073531 10617b busybox_unstripped
> ---
> procps/vmstat.c | 426 ++++++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 426 insertions(+)
> create mode 100644 procps/vmstat.c
>
> diff --git a/procps/vmstat.c b/procps/vmstat.c
> new file mode 100644
> index 000000000..bccd27d63
> --- /dev/null
> +++ b/procps/vmstat.c
> @@ -0,0 +1,426 @@
> +/* vi: set sw=4 ts=4: */
> +/*
> + * Report virtual memory statistics.
> + *
> + * Copyright (C) 2025 David Leonard
> + *
> + * Licensed under GPLv2, see file LICENSE in this source tree.
> + */
> +//config:config VMSTAT
> +//config: bool "vmstat (3 kb)"
> +//config: default y
> +//config: help
> +//config: Report virtual memory statistics
> +
> +//applet:IF_VMSTAT(APPLET(vmstat, BB_DIR_BIN, BB_SUID_DROP))
> +
> +//kbuild:lib-$(CONFIG_VMSTAT) += vmstat.o
> +
> +#include "libbb.h"
> +#include "common_bufsiz.h"
> +
> +/* Must match option string! */
> +enum {
> + OPT_n = 1 << 0,
> +};
> +
> +#define FROM_PROC_STAT "\1"
> +#define FROM_PROC_STAT_CPU "\2"
> +#define FROM_PROC_VMSTAT "\3"
> +#define FROM_PROC_MEMINFO "\4"
> +#define M_DELTA "\x81" /* differentiate */
> +#define M_DPERCENT "\x83" /* DELTA + sum and scale to 100 */
> +#define M_DECREMENT "\x84" /* decrement (exclude self proc) */
> +#define PSEUDO_SWPD "_1" /* SwapTotal - SwapFree */
> +#define PSEUDO_CACHE "_2" /* Cached + SReclaimable */
> +
> +/* Column descriptors */
> +static const char coldescs[] =
> + /* [grplabel\0] (\width label\0 from [m_mod] fromspec\0)+ */
> + "procs\0" "\2r\0" FROM_PROC_STAT M_DECREMENT "procs_running\0"
> + "\2b\0" FROM_PROC_STAT "procs_blocked\0"
> + "memory\0" "\6swpd\0" FROM_PROC_MEMINFO PSEUDO_SWPD "\0"
> + "\6free\0" FROM_PROC_MEMINFO "MemFree\0"
> + "\6buff\0" FROM_PROC_MEMINFO "Buffers\0"
> + "\6cache\0" FROM_PROC_MEMINFO PSEUDO_CACHE "\0"
> + "swap\0" "\4si\0" FROM_PROC_VMSTAT M_DELTA "pswpin\0"
> + "\4so\0" FROM_PROC_VMSTAT M_DELTA "pswpout\0"
> + "io\0" "\5bi\0" FROM_PROC_VMSTAT M_DELTA "pgpgin\0"
> + "\5bo\0" FROM_PROC_VMSTAT M_DELTA "pgpgout\0"
> + "system\0" "\4in\0" FROM_PROC_STAT M_DELTA "intr\0"
> + "\4cs\0" FROM_PROC_STAT M_DELTA "ctxt\0"
> + "cpu\0" "\2us\0" FROM_PROC_STAT_CPU M_DPERCENT "\x0d" /* user */
> + "\2sy\0" FROM_PROC_STAT_CPU M_DPERCENT "\x0b" /* system */
> + "\2id\0" FROM_PROC_STAT_CPU M_DPERCENT "\x04" /* idle */
> + "\2wa\0" FROM_PROC_STAT_CPU M_DPERCENT "\x05" /* iowait */
> + "\2st\0" FROM_PROC_STAT_CPU M_DPERCENT "\x08" /* steal */
> + "\2gu\0" FROM_PROC_STAT_CPU M_DPERCENT "\x0c" /* guest */
> + ;
> +
> +/* Packed row data from coldescs[] is decoded into this structure */
> +struct col {
> + const char *grplabel;
> + const char *label;
> + const char *fromspec;
> + unsigned char from;
> + unsigned char width;
> + unsigned char mod;
> +#define MOD_DELTA 0x01
> +#define MOD_PERCENT 0x02
> +#define MOD_DECREMENT 0x04
> +};
> +
> +/* Number of columns defined in coldescs[] */
> +#define NCOLS (2+4+2+2+2+6)
> +
> +/* Globals. Sort by size and access frequency. */
> +struct globals {
> + unsigned data1[NCOLS];
> + unsigned data2[NCOLS];
> +};
> +#define G (*(struct globals*)bb_common_bufsiz1)
> +#define INIT_G() do { \
> + /* memset(&G, 0, sizeof G); */ \
> +} while (0)
> +
> +
> +//usage:#define vmstat_trivial_usage
> +//usage: "[-n] [INTERVAL [COUNT]]"
> +//usage:#define vmstat_full_usage "\n\n"
> +//usage: "Report virtual memory statistics\n"
> +//usage: "\n -n Display the header only once"
> +
> +/*
> + * Advance an iterator over the coldescs[] packed descriptors.
> + * col_return - pointer to storage to hold the next unpacked descriptor.
> + * cp - pointer to iterator storage; should be initialised with coldescs.
> + * Returns 1 when *cp has been advanced, and *col_return filled
> + * (i.e. col_return->label will not be NULL).
> + * Returns 0 when coldescs[] has been exhausted, and sets col_return->label
> + * and col_return->grplabel to NULL.
> + */
> +static ALWAYS_INLINE bool next_col(struct col *col_return, const char **cp)
> +{
> + if (!**cp) {
> + col_return->label = NULL;
> + col_return->grplabel = NULL;
> + return 0;
> + }
> + if (**cp > ' ') {
> + /* Only the first column of a group gets the grplabel */
> + col_return->grplabel = *cp;
> + while (*(*cp)++)
> + ; /* Skip over the grplabel */
> + } else
> + col_return->grplabel = NULL;
> + col_return->width = *(*cp)++;
> + col_return->label = *cp;
> + while (*(*cp)++)
> + ; /* Skip over the label */
> + col_return->from = *(*cp)++;
> + if (**cp & 0x80)
> + col_return->mod = *(*cp)++;
> + else
> + col_return->mod = 0;
> + col_return->fromspec = *cp;
> + while (*(*cp)++ >= ' ')
> + ; /* Skip over the fromspec */
> + return 1;
> +}
> +
> +/* Compares two fromspec strings for equality.
> + * A fromspec can be a C string, or be terminated inclusively with
> + * a byte 1..31. */
> +static bool fromspec_equal(const char *fs1, const char *fs2)
> +{
> + while (*fs1 == *fs2) {
> + if (*fs1 < ' ')
> + return 1;
> + fs1++;
> + fs2++;
> + }
> + return 0;
> +
> +}
> +
> +/*
> + * Finds a column in coldescs[] that has the the given from and fromspec.
> + * Returns the index if found, otherwise -1.
> + */
> +static int find_col(unsigned char from, const char *fromspec)
> +{
> + const char *coli;
> + struct col col;
> + int i;
> +
> + for (i = 0, coli = coldescs; next_col(&col, &coli); i++)
> + if (col.from == from &&
> + fromspec_equal(col.fromspec, fromspec))
> + return i;
> + return -1;
> +}
> +
> +/*
> + * Reads current system state into the data array elements corresponding
> + * to coldescs[] columns.
> + */
> +static void load_row(unsigned data[static NCOLS])
> +{
> + FILE *fp;
> + char label[32];
> + char line[256];
> + int colnum;
> + unsigned SwapFree = 0;
> + unsigned SwapTotal = 0;
> + unsigned Cached = 0;
> + unsigned SReclaimable = 0;
> +
> + memset(data, 0, NCOLS * sizeof *data);
> +
> + /*
> + * Open each FROM_* source and read all their items. These are
> + * generally labeled integer items. As we read each item, hunt
> + * through coldescs[] to see if a column uses it and, if one does,
> + * store the item's value in the corresponding data[] element.
> + */
> +
> + /* FROM_PROC_STAT and FROM_PROC_STAT_CPU */
> + fp = xfopen_for_read("/proc/stat");
> + while (fgets(line, sizeof(line), fp)) {
> + enum stat_cpu {
> + STAT_CPU_user = 1,
> + STAT_CPU_nice = 2,
> + STAT_CPU_system = 3,
> + STAT_CPU_idle = 4,
> + STAT_CPU_iowait = 5,
> + STAT_CPU_irq = 6,
> + STAT_CPU_softirq = 7,
> + STAT_CPU_steal = 8,
> + STAT_CPU_guest = 9, /* included in user */
> + STAT_CPU_guest_nice = 10, /* included in nice */
> + /* computed columns */
> + STAT_CPU_pseudo_sy = 0xb, /* system + irq + softirq */
> + STAT_CPU_pseudo_gu = 0xc, /* guest + guest_nice */
> + STAT_CPU_pseudo_us = 0xd, /* user + nice - pseudo_gu */
> + _STAT_CPU_max
> + };
> + unsigned num[_STAT_CPU_max];
> + int n = sscanf(line,
> + "%31s %u %u %u %u %u %u %u %u %u %u",
> + label,
> + &num[STAT_CPU_user],
> +# define _STAT_CPU_first STAT_CPU_user
> + &num[STAT_CPU_nice],
> + &num[STAT_CPU_system],
> + &num[STAT_CPU_idle],
> + &num[STAT_CPU_iowait],
> + &num[STAT_CPU_irq],
> + &num[STAT_CPU_softirq],
> + &num[STAT_CPU_steal],
> + &num[STAT_CPU_guest],
> + &num[STAT_CPU_guest_nice]);
> + if (n == 11 && strcmp(label, "cpu") == 0) {
> + const char *coli;
> + struct col col;
> + int i;
> +
> + num[STAT_CPU_pseudo_sy] = num[STAT_CPU_system]
> + + num[STAT_CPU_irq]
> + + num[STAT_CPU_softirq]
> + ;
> + num[STAT_CPU_pseudo_gu] = num[STAT_CPU_guest]
> + + num[STAT_CPU_guest_nice]
> + ;
> + num[STAT_CPU_pseudo_us] = num[STAT_CPU_user]
> + + num[STAT_CPU_nice]
> + - num[STAT_CPU_pseudo_gu]
> + ;
> + for (i = 0, coli = coldescs; next_col(&col, &coli); i++)
> + if (col.from == *FROM_PROC_STAT_CPU)
> + data[i] = num[(int)*col.fromspec];
> + }
> + else if (n >= 2 &&
> + (colnum = find_col(*FROM_PROC_STAT, label)) != -1)
> + data[colnum] = num[_STAT_CPU_first];
> + }
> + fclose(fp);
> +
> + /* FROM_PROC_VMSTAT */
> + fp = xfopen_for_read("/proc/vmstat");
> + while (fgets(line, sizeof(line), fp)) {
> + unsigned num;
> +
> + if (sscanf(line, "%31s %u", label, &num) == 2 &&
> + (colnum = find_col(*FROM_PROC_VMSTAT, label)) != -1)
> + data[colnum] = num;
> + }
> + fclose(fp);
> +
> + /* FROM_PROC_MEMINFO */
> + fp = xfopen_for_read("/proc/meminfo");
> + while (fgets(line, sizeof(line), fp)) {
> + unsigned num;
> +
> + if (sscanf(line, "%31[^:]: %u", label, &num) == 2) {
> + /* Store some values for computed (pseudo) values */
> + if (strcmp(label, "SwapTotal") == 0)
> + SwapTotal = num;
> + else if (strcmp(label, "SwapFree") == 0)
> + SwapFree = num;
> + else if (strcmp(label, "Cached") == 0)
> + Cached = num;
> + else if (strcmp(label, "SReclaimable") == 0)
> + SReclaimable = num;
> +
> + if ((colnum = find_col(*FROM_PROC_MEMINFO,
> + label)) != -1)
> + data[colnum] = num;
> + }
> + }
> + fclose(fp);
> +
> + /* "Pseudo" items computed from other items */
> + if ((colnum = find_col(*FROM_PROC_MEMINFO, PSEUDO_SWPD)) != -1)
> + data[colnum] = SwapTotal - SwapFree;
> + if ((colnum = find_col(*FROM_PROC_MEMINFO, PSEUDO_CACHE)) != -1)
> + data[colnum] = Cached + SReclaimable;
> +}
> +
> +/* Modify, format and print a row of data values */
> +static void print_row(const unsigned data[static NCOLS],
> + const unsigned old_data[static NCOLS])
> +{
> + const char *coli;
> + struct col col;
> + smalluint i;
> + bool is_first;
> + unsigned percent_sum = 0;
> +
> + for (coli = coldescs, i = 0; next_col(&col, &coli); i++)
> + if (col.mod & MOD_PERCENT) {
> + unsigned value = data[i];
> + if (col.mod & MOD_DELTA)
> + value -= old_data[i];
> + percent_sum += value;
> + }
> +
> + is_first = 1;
> + for (coli = coldescs, i = 0; next_col(&col, &coli); i++) {
> + unsigned value = data[i];
> +
> + if (col.mod & MOD_DELTA)
> + value -= old_data[i];
> + if (col.mod & MOD_PERCENT)
> + value = percent_sum ? 100 * value / percent_sum : 0;
> + if ((col.mod & MOD_DECREMENT) && value)
> + value--;
> + printf(" %*u" + is_first, col.width, value);
> + is_first = 0;
> + }
> + bb_putchar('\n');
> +}
> +
> +/* Print column header rows */
> +static void print_header(void)
> +{
> + const char *coli;
> + struct col col;
> + bool is_first;
> +
> + /* First header row is the group labels */
> + coli = coldescs;
> + next_col(&col, &coli);
> + is_first = 1;
> + while (col.label) {
> + const char *grplabel = col.grplabel;
> + smalluint gllen = strlen(grplabel);
> + smalluint grpwidth = col.width;
> + smalluint nhy = 0;
> +
> + /* Sum the column widths */
> + next_col(&col, &coli);
> + while (col.label && !col.grplabel) {
> + grpwidth += 1 + col.width;
> + next_col(&col, &coli);
> + }
> +
> + if (is_first)
> + is_first = 0;
> + else
> + bb_putchar(' ');
> +
> + /* Centre the grplabel within grpwidth hyphens. */
> + while (gllen < grpwidth) {
> + bb_putchar('-');
> + grpwidth--;
> + if (gllen < grpwidth)
> + grpwidth--, nhy++;
> + }
> + while (*grplabel)
> + bb_putchar(*grplabel++);
> + while (nhy--)
> + bb_putchar('-');
> + }
> + bb_putchar('\n');
> +
> + /* Second header row is right-justified column labels */
> + coli = coldescs;
> + is_first = 1;
> + while (next_col(&col, &coli)) {
> + printf(" %*s" + is_first, col.width, col.label);
> + is_first = 0;
> + }
> + bb_putchar('\n');
> +}
> +
> +int vmstat_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
> +int vmstat_main(int argc UNUSED_PARAM, char **argv)
> +{
> + int opt;
> + unsigned interval = 0;
> + int count = 1;
> + unsigned height = 24;
> + unsigned rows;
> +
> + INIT_G();
> +
> + /* Parse and process arguments */
> + opt = getopt32(argv, "n");
> + argv += optind;
> +
> + if (*argv) {
> + interval = xatoi_positive(*argv);
> + count = (interval != 0 ? -1 : 1);
> + argv++;
> + if (*argv)
> + count = xatoi_positive(*argv);
> + }
> +
> + /* Prepare to re-print the header row after it scrolls off */
> + if (opt & OPT_n)
> + height = 0;
> + else
> + get_terminal_width_height(STDOUT_FILENO, NULL, &height);
> +
> + /* Main loop */
> + for (rows = 0;; rows++) {
> + if (!rows || (height > 5 && (rows % (height - 3)) == 0))
> + print_header();
> +
> + /* Flip between using data1/2 and data2/1 for old/new */
> + if (rows & 1) {
> + load_row(G.data1);
> + print_row(G.data1, G.data2);
> + } else {
> + load_row(G.data2);
> + print_row(G.data2, G.data1);
> + }
> +
> + if (count > 0 && --count == 0)
> + break;
> +
> + sleep(interval);
> + }
> +
> + return EXIT_SUCCESS;
> +}
> --
> 2.43.0
>
> _______________________________________________
> busybox mailing list
> busybox at busybox.net
> https://lists.busybox.net/mailman/listinfo/busybox
More information about the busybox
mailing list