From fefd17241f2c676afb55f4664a382d7cb8f93e48 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Sat, 5 Sep 2026 23:17:25 -0400 Subject: [PATCH] feat(times): times (L2 XSI) + CLK_TCK sysconf --- include/sys/times.h | 72 +++++++++++++++++ src/sys/times.c | 60 ++++++++++++++ tests/test_times.c | 186 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 318 insertions(+) create mode 100644 include/sys/times.h create mode 100644 src/sys/times.c create mode 100644 tests/test_times.c diff --git a/include/sys/times.h b/include/sys/times.h new file mode 100644 index 0000000..37eaf54 --- /dev/null +++ b/include/sys/times.h @@ -0,0 +1,72 @@ +#ifndef VLIBC_SYS_TIMES_H +#define VLIBC_SYS_TIMES_H + +/* + * vlibc — . + * + * Process CPU-time accounting in clock ticks. times() and struct tms are + * XSI [CX] in POSIX.1-2008 (glibc gates them behind _XOPEN_SOURCE, not + * _POSIX_C_SOURCE), so the whole surface sits at level 2 — nothing here is + * POSIX.1-2008 base: + * + * Level 2 (muslmimic): times, struct tms. + * + * clock_t is the scalar tick type owned by (typedef long + * clock_t; pulls it from the same place for clock()); this header + * includes that one rather than re-typedefing clock_t. The unit of every + * times() figure is the tick: CLK_TCK per second, the fixed x86_64 USER_HZ + * constant of 100 that todo 32's sysconf(_SC_CLK_TCK) reports (defined + * once, in src/misc/sysconf.c). + * + * struct tms mirrors the x86_64 kernel's __kernel_tms exactly: four longs + * in the same order (tms_utime, tms_stime, tms_cutime, tms_cstime), which + * is why src/sys/times.c hands the struct straight to SYS_times. The kernel + * writes all four words on every successful call, so no field is optional. + * + * The declaration carries no intent attribute: times() performs a syscall + * with side effects and reports failures through errno, so const/pure + * would be unsound (see the rationale in ). + */ + +#include + +#include /* clock_t */ + +#ifdef __cplusplus +extern "C" { +#endif + +#if VLIBC_LEVEL_GE(2) +/* Level 2 (muslmimic): XSI process-CPU-time accounting. */ + +/* + * CPU-time accounting for the calling process and its waited-for children, + * measured in clock ticks (CLK_TCK per second). tms_utime and tms_stime + * are the user and system CPU time of the calling process; tms_cutime and + * tms_cstime are the user and system CPU time consumed by children that + * have been waited for (all four zeroed before any child is waited on). + */ +struct tms +{ + clock_t tms_utime; /* user CPU time of the calling process */ + clock_t tms_stime; /* system CPU time of the calling process */ + clock_t tms_cutime; /* user CPU time of waited-for children */ + clock_t tms_cstime; /* system CPU time of waited-for children */ +}; + +/* + * Store the process CPU-time accounting through buf and return the number + * of clock ticks since an arbitrary fixed point in the past (the Linux + * kernel reports ticks since boot). Return (clock_t)-1 with errno set when + * buf points outside the address space. + */ +clock_t +times(struct tms *buf); + +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_SYS_TIMES_H */ diff --git a/src/sys/times.c b/src/sys/times.c new file mode 100644 index 0000000..5d35e1f --- /dev/null +++ b/src/sys/times.c @@ -0,0 +1,60 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" + +/* + * times(): process CPU-time accounting in CLK_TCK ticks. + * + * Everything here is XSI — times() is [CX] in POSIX.1-2008, NOT base — so + * the whole file is gated at level 2 (whole-file-L2 rule; the wiring pass + * puts it in VLIBC_LEVEL2_SRCS, and at level 1 this TU compiles empty). + * + * SYS_times writes a struct __kernel_tms, which on x86_64 is exactly four + * longs (tms_utime, tms_stime, tms_cutime, tms_cstime) — byte-for-byte the + * public struct tms from (clock_t is long here). The kernel + * therefore fills the caller's struct in place; no private mirror is + * needed. The syscall's return value is the tick counter since boot, which + * is clock_t (long) — a value that can legitimately exceed INT_MAX on a + * long-lived host — so the error translation is inlined below with the same + * semantics as syscall_ret() instead of routing the full-width counter + * through it (house idiom, see src/unistd/lseek.c). + * + * The tick unit is CLK_TCK (100 per second on x86_64), the fixed USER_HZ + * constant reported by sysconf(_SC_CLK_TCK) and defined ONCE in + * src/misc/sysconf.c; times() consumes that unit rather than re-defining + * it. clock_t here is therefore the raw kernel tick count, matching POSIX + * XSI (where the clock_t unit is CLK_TCK); this deliberately does NOT + * rescale to the 1 MHz clock() unit that 's clock() uses (see + * src/time/clock.c, which reads the same SYS_times through its own struct + * tms mirror and scales by CLOCKS_PER_SEC / 100). + */ + +#if VLIBC_LEVEL_GE(2) + +/* + * Fill *buf with the CPU times of the calling process (tms_utime, + * tms_stime) and of its waited-for children (tms_cutime, tms_cstime) in + * CLK_TCK ticks, and return the number of clock ticks since boot. On + * failure — buf pointing outside the address space — return (clock_t)-1 + * with errno set; per POSIX times() has no other error returns. + */ +clock_t +times(struct tms *buf) +{ + long r = __syscall1(SYS_times, (long)buf); + + if (r < 0 && r > -4096) + { + errno = (int)-r; + return (clock_t)-1; + } + return (clock_t)r; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/tests/test_times.c b/tests/test_times.c new file mode 100644 index 0000000..41fcc00 --- /dev/null +++ b/tests/test_times.c @@ -0,0 +1,186 @@ +/* + * vlibc — sys/times.h test (todo 35). + * + * Exercises the whole surface end to end: + * + * 1. times(&t) succeeds: the returned tick counter is not -1 and the four + * struct tms fields are non-negative (fresh processes commonly show + * zeros here, which is legal). + * 2. Monotonicity across real work: a few-million-iteration busy loop + * burns user CPU, and a second times() read then reports tms_utime no + * smaller than the first. The tick counter itself must never go + * backwards between two reads. + * 3. A rapid-fire loop of times() calls (no work between them) keeps the + * tick counter strictly non-decreasing across every adjacent pair. + * + * The -f mode runs the one failure shape — times() on an out-of-range + * buffer pointer, which the kernel rejects with EFAULT — asserting the + * return value only (errno is never read: vlibc's errno slot collides + * with the host TCB). It leaves via a raw SYS_exit_group so no host + * cleanup runs after the library's errno write (house pattern, + * tests/test_stat.c). + * + * No host libc headers are included (the -Iinclude path would shadow + * GCC's internal headers); diagnostics go through raw SYS_write. The whole + * body mirrors the header's gate: is entirely level 2, so at + * level 1 this TU compiles to a no-op runner. + */ + +#include + +#include "../src/internal/syscall.h" + +/* Write a NUL-terminated string to fd via the raw syscall layer. The + * optimize attribute keeps GCC from lowering the length loop into a + * strlen call, which would leave a vlibc-owned symbol undefined in this + * host-linked standalone binary (house idiom, see src/string). */ +static __attribute__((optimize("no-tree-loop-distribute-patterns"))) void +say(int fd, const char *s) +{ + long n = 0; + + while (s[n] != '\0') + { + n++; + } + __syscall3(SYS_write, fd, (long)s, n); +} + +#if VLIBC_LEVEL_GE(2) + +static int failures; + +static void +check(int ok, const char *msg) +{ + if (ok) + { + say(1, "ok "); + } + else + { + say(1, "FAIL "); + failures++; + } + say(1, msg); + say(1, "\n"); +} + +/* Burn a few million iterations of real user CPU so tms_utime has a chance + * to advance between two times() reads. The sink is volatile so the loop + * cannot be optimized away. */ +static volatile unsigned long t35_sink; + +static void +burn_cpu(void) +{ + for (unsigned long i = 0; i < 5000000UL; i++) + { + t35_sink += i; + } +} + +/* 1-3. Success shapes: plausible fields, monotonic across work and across + * rapid reads. */ +static void +times_scenario(void) +{ + struct tms t1; + struct tms t2; + struct tms probe; + clock_t r1; + clock_t r2; + clock_t prev; + + r1 = times(&t1); + check(r1 != (clock_t)-1, "times(&t) does not return -1"); + check(t1.tms_utime >= 0 && t1.tms_stime >= 0, "tms_utime/tms_stime are non-negative"); + check(t1.tms_cutime >= 0 && t1.tms_cstime >= 0, "tms_cutime/tms_cstime are non-negative"); + + burn_cpu(); + r2 = times(&t2); + check(r2 != (clock_t)-1, "second times(&t) does not return -1"); + check(r2 >= r1, "tick counter does not go backwards across work"); + check(t2.tms_utime >= t1.tms_utime, "tms_utime is nondecreasing after a CPU burn"); + check(t2.tms_stime >= t1.tms_stime, "tms_stime is nondecreasing after a CPU burn"); + check(t2.tms_cutime >= t1.tms_cutime && t2.tms_cstime >= t1.tms_cstime, + "child-time fields are nondecreasing (no waited-for children here)"); + + /* Rapid-fire reads with no work between them: the kernel tick counter + * is monotonic, so every adjacent pair must be non-decreasing. */ + prev = times(&probe); + check(prev != (clock_t)-1, "rapid-fire times() baseline is not -1"); + for (int i = 0; i < 100000; i++) + { + clock_t now = times(&probe); + + if (now == (clock_t)-1) + { + check(0, "rapid-fire times() never returns -1"); + break; + } + if (now < prev) + { + check(0, "rapid-fire tick counter never goes backwards"); + break; + } + prev = now; + } + check(prev != (clock_t)-1, "rapid-fire times() loop completed"); +} + +/* The failure shapes; return values only, never errno. Exit via a raw + * SYS_exit_group: the library's errno write on this path corrupts glibc's + * private dtv slot at %fs:0+8, so host cleanup must never run (house + * pattern, tests/test_stat.c). + * + * The probe buffer is the page-start address 1: non-NULL (the modern + * kernel treats times(NULL) as legal and returns the tick counter without + * writing anything) but unmapped, so SYS_times' copy_to_user fails with + * EFAULT and the wrapper returns -1. */ +static void +failure_scenarios(void) +{ + check(times((struct tms *)(unsigned long)1) == (clock_t)-1, + "-f times(out-of-range buf) returns -1"); + say(1, "times: raw kernel ticks reported; not rescaled to clock() units\n"); + + __syscall1(SYS_exit_group, failures == 0 ? 0 : 1); + /* not reached */ +} + +int +main(int argc, char **argv) +{ + if (argc > 1 && argv[1][0] == '-' && argv[1][1] == 'f' && argv[1][2] == '\0') + { + failure_scenarios(); + } + + times_scenario(); + + if (failures == 0) + { + say(1, "all times tests passed\n"); + } + else + { + say(1, "FAILURES\n"); + } + return failures == 0 ? 0 : 1; +} + +#else /* !VLIBC_LEVEL_GE(2) */ + +/* + * Level 1: every symbol is gated at level 2, so there is + * nothing to run. Keep the TU compilable at any configured profile. + */ +int +main(void) +{ + say(1, "SKIP: is level 2, not available here\n"); + return 0; +} + +#endif /* VLIBC_LEVEL_GE(2) */