feat(times): times (L2 XSI) + CLK_TCK sysconf

This commit is contained in:
2026-09-05 23:17:25 -04:00
parent 3bf875030a
commit fefd17241f
3 changed files with 318 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
#ifndef VLIBC_SYS_TIMES_H
#define VLIBC_SYS_TIMES_H
/*
* vlibc — <sys/times.h>.
*
* 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 <sys/types.h> (typedef long
* clock_t; <time.h> 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 <sys/time.h>).
*/
#include <vlibc/features.h>
#include <sys/types.h> /* 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 */
+60
View File
@@ -0,0 +1,60 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <sys/times.h>
#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 <sys/times.h> (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 <time.h>'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) */
+186
View File
@@ -0,0 +1,186 @@
/*
* vlibc — sys/times.h test (todo 35).
*
* Exercises the whole <sys/times.h> 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: <sys/times.h> is entirely level 2, so at
* level 1 this TU compiles to a no-op runner.
*/
#include <sys/times.h>
#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 <sys/times.h> 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: <sys/times.h> is level 2, not available here\n");
return 0;
}
#endif /* VLIBC_LEVEL_GE(2) */