From 7cd2c0e77357b6c2aabd28162aea84e3904f0f9d Mon Sep 17 00:00:00 2001 From: Max042004 Date: Tue, 7 Jul 2026 17:02:42 +0800 Subject: [PATCH] Implement times(2) syscall Guests calling times(2) got -ENOSYS: the syscall was absent from dispatch.tbl and abi.h had no SYS_times entry. Shell timing builtins and build tools that poll times() for elapsed/CPU tick accounting fall over on the error return. sys_times reports tms_utime/tms_stime from host getrusage(RUSAGE_SELF). tms_cutime/tms_cstime cannot come from RUSAGE_CHILDREN: the emulator also waits on helper subprocesses (rosettad translate, sysroot tooling), so that aggregate would masquerade helper CPU as guest child time. Instead the proc layer accumulates each reaped guest child's wait4 rusage -- at sys_wait4, sys_waitid, the table-pressure reaper, and the CLONE_VFORK wait -- and times() reads that sum. Fresh guest children start with zeroed counters for free because fork spawns a new emulator process. POSIX leaves autoreaped-children inclusion unspecified, so counting reaper-collected children is conformant. The return value is host CLOCK_MONOTONIC converted to USER_HZ (100) ticks: Linux returns jiffies-since-boot, and POSIX only requires an arbitrary-but-fixed reference point that successive calls can difference. Tick conversion uses the same 100Hz that core/stack.c advertises via AT_CLKTCK, so libc's sysconf(_SC_CLK_TCK) scaling stays consistent. A NULL buffer is accepted as on Linux, returning only the tick count. test-times covers the tick clock advancing with wall time, self and waited-for-child CPU accumulation against a measured burn, the NULL buffer, and EFAULT on a bad pointer. --- src/runtime/forkipc.c | 13 ++- src/syscall/abi.h | 1 + src/syscall/dispatch.tbl | 1 + src/syscall/proc.c | 67 +++++++++++-- src/syscall/proc.h | 10 ++ src/syscall/syscall.c | 1 + src/syscall/time.c | 76 ++++++++++++++ src/syscall/time.h | 1 + tests/manifest.txt | 1 + tests/test-times.c | 207 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 366 insertions(+), 12 deletions(-) create mode 100644 tests/test-times.c diff --git a/src/runtime/forkipc.c b/src/runtime/forkipc.c index 35e42338..8e64a842 100644 --- a/src/runtime/forkipc.c +++ b/src/runtime/forkipc.c @@ -1812,14 +1812,19 @@ int64_t sys_clone(hv_vcpu_t vcpu, close(vfork_notify_fds[0]); if (nr <= 0) { - int status; - waitpid(child_host_pid, &status, 0); + int status = 0; + struct rusage ru; + if (wait4(child_host_pid, &status, 0, &ru) == child_host_pid) + proc_children_cpu_add(&ru); proc_mark_child_exited(child_host_pid, status); } else { int status; - pid_t waited = waitpid(child_host_pid, &status, WNOHANG); - if (waited == child_host_pid) + struct rusage ru; + pid_t waited = wait4(child_host_pid, &status, WNOHANG, &ru); + if (waited == child_host_pid) { + proc_children_cpu_add(&ru); proc_mark_child_exited(child_host_pid, status); + } } } diff --git a/src/syscall/abi.h b/src/syscall/abi.h index 6dff0693..e4f6b82f 100644 --- a/src/syscall/abi.h +++ b/src/syscall/abi.h @@ -121,6 +121,7 @@ #define SYS_getresgid 150 #define SYS_setfsuid 151 #define SYS_setfsgid 152 +#define SYS_times 153 #define SYS_setpgid 154 #define SYS_getpgid 155 #define SYS_getsid 156 diff --git a/src/syscall/dispatch.tbl b/src/syscall/dispatch.tbl index b6b07678..7962dfa6 100644 --- a/src/syscall/dispatch.tbl +++ b/src/syscall/dispatch.tbl @@ -126,6 +126,7 @@ SYS_clock_gettime sc_clock_gettime 0 SYS_clock_getres sc_clock_getres 0 SYS_clock_nanosleep sc_clock_nanosleep 1 SYS_gettimeofday sc_gettimeofday 0 +SYS_times sc_times 0 SYS_timerfd_create sc_timerfd_create 1 SYS_timerfd_settime sc_timerfd_settime 1 SYS_timerfd_gettime sc_timerfd_gettime 1 diff --git a/src/syscall/proc.c b/src/syscall/proc.c index f6f0055f..36f6ac74 100644 --- a/src/syscall/proc.c +++ b/src/syscall/proc.c @@ -81,6 +81,35 @@ static pthread_mutex_t pid_lock = PTHREAD_MUTEX_INITIALIZER; /* Lock order: 6 */ static pthread_cond_t pid_cond = PTHREAD_COND_INITIALIZER; /* Signaled on child exit */ +/* CPU time of reaped guest children, accumulated at every host reap site and + * reported by times(2) as tms_cutime/tms_cstime. The emulator also waits on + * helper subprocesses (rosettad translate, sysroot tooling) whose CPU shows up + * in the host's RUSAGE_CHILDREN, so times() cannot read that aggregate; only + * reaps of proc_table children may land here. Relaxed atomics suffice: the + * counters are monotonic sums and times() tolerates reading utime/stime one + * reap apart. + */ +static _Atomic uint64_t children_utime_us; +static _Atomic uint64_t children_stime_us; + +void proc_children_cpu_add(const struct rusage *ru) +{ + atomic_fetch_add_explicit(&children_utime_us, + (uint64_t) ru->ru_utime.tv_sec * 1000000 + + (uint64_t) ru->ru_utime.tv_usec, + memory_order_relaxed); + atomic_fetch_add_explicit(&children_stime_us, + (uint64_t) ru->ru_stime.tv_sec * 1000000 + + (uint64_t) ru->ru_stime.tv_usec, + memory_order_relaxed); +} + +void proc_children_cpu_us(uint64_t *utime_us, uint64_t *stime_us) +{ + *utime_us = atomic_load_explicit(&children_utime_us, memory_order_relaxed); + *stime_us = atomic_load_explicit(&children_stime_us, memory_order_relaxed); +} + /* Global flag for exit_group: signals all threads to terminate. Atomic to avoid * undefined behavior under C11 memory model when multiple threads read/write * concurrently. @@ -283,9 +312,11 @@ static int proc_reap_finished(void) continue; } int status; - pid_t ret = waitpid(proc_table[i].host_pid, &status, WNOHANG); + struct rusage ru; + pid_t ret = wait4(proc_table[i].host_pid, &status, WNOHANG, &ru); if (ret > 0) { /* Child exited; free the slot */ + proc_children_cpu_add(&ru); proc_table[i].active = false; reaped++; } @@ -1073,9 +1104,17 @@ int64_t sys_wait4(guest_t *g, int status; struct rusage ru; - pid_t ret = wait4(host_pid, &status, mac_options | WNOHANG, - rusage_gva ? &ru : NULL); + pid_t ret = + wait4(host_pid, &status, mac_options | WNOHANG, &ru); if (ret > 0) { + /* Credit CPU only on a terminal report. mac_options may + * carry WUNTRACED/WCONTINUED, and a stop/continue report + * is a snapshot of a still-running child: crediting it + * here would double- or triple-count the same child + * across its stop, continue, and final exit reports. + */ + if (WIFEXITED(status) || WIFSIGNALED(status)) + proc_children_cpu_add(&ru); if (status_gva) { int32_t linux_status = status; if (guest_write_small(g, status_gva, &linux_status, @@ -1146,8 +1185,7 @@ int64_t sys_wait4(guest_t *g, struct rusage ru; pid_t ret; if (mac_options & WNOHANG) { - ret = wait4(host_pid, &status, mac_options, - rusage_gva ? &ru : NULL); + ret = wait4(host_pid, &status, mac_options, &ru); } else { /* A bare blocking wait4 has no re-check point: a worker * parked here past exit_group is invisible to @@ -1159,8 +1197,7 @@ int64_t sys_wait4(guest_t *g, * pid_cond on every host child exit. */ for (;;) { - ret = wait4(host_pid, &status, mac_options | WNOHANG, - rusage_gva ? &ru : NULL); + ret = wait4(host_pid, &status, mac_options | WNOHANG, &ru); if (ret != 0) break; if (proc_exit_group_requested()) @@ -1173,6 +1210,9 @@ int64_t sys_wait4(guest_t *g, } } if (ret > 0) { + /* Same terminal-report gate as the P_ALL branch above. */ + if (WIFEXITED(status) || WIFSIGNALED(status)) + proc_children_cpu_add(&ru); if (status_gva) { int32_t linux_status = status; if (guest_write_small(g, status_gva, &linux_status, @@ -1296,7 +1336,18 @@ int64_t sys_waitid(guest_t *g, } else { pid_t host_pid = proc_table[i].host_pid; pthread_mutex_unlock(&pid_lock); - ret = waitpid(host_pid, &status, WNOHANG); + struct rusage ru; + ret = wait4(host_pid, &status, WNOHANG, &ru); + /* Credit only a terminal report, and only when this call + * actually consumes the reap: WNOWAIT must leave the child + * waitable with its CPU uncounted until the later consuming + * wait, but this inner wait4() has no WNOWAIT of its own, so + * it consumes the host zombie regardless of the guest's + * WNOWAIT request. + */ + if (ret > 0 && !(options & LINUX_WNOWAIT) && + (WIFEXITED(status) || WIFSIGNALED(status))) + proc_children_cpu_add(&ru); if (ret == 0) { /* This child hasn't exited yet; continue checking others * (P_ALL must scan all children). diff --git a/src/syscall/proc.h b/src/syscall/proc.h index 83526e72..cb6e48ad 100644 --- a/src/syscall/proc.h +++ b/src/syscall/proc.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "core/guest.h" @@ -313,6 +314,15 @@ int proc_register_child(pid_t host_pid, int64_t guest_pid, int64_t pgid); /* Mark a child as exited by host PID (for CLONE_VFORK wait). */ void proc_mark_child_exited(pid_t host_pid, int status); +/* Accumulate a reaped guest child's rusage into the cutime/cstime counters + * that times(2) reports. Call at every host reap of a proc_table child; never + * for emulator helper subprocesses. + */ +void proc_children_cpu_add(const struct rusage *ru); + +/* Read the accumulated guest-children CPU time, in microseconds. */ +void proc_children_cpu_us(uint64_t *utime_us, uint64_t *stime_us); + /* Collect host PIDs of active (non-exited) fork children. Writes up to max_pids * entries into out[]. * diff --git a/src/syscall/syscall.c b/src/syscall/syscall.c index 48840fdf..299435f2 100644 --- a/src/syscall/syscall.c +++ b/src/syscall/syscall.c @@ -345,6 +345,7 @@ SC_FORWARD(sc_clock_nanosleep, sys_clock_nanosleep(g, (int) x0, (int) x1, x2, x SC_FORWARD(sc_gettimeofday, sys_gettimeofday(g, x0, x1)) SC_FORWARD(sc_setitimer, sys_setitimer(g, (int) x0, x1, x2)) SC_FORWARD(sc_getitimer, sys_getitimer(g, (int) x0, x1)) +SC_FORWARD(sc_times, sys_times(g, x0)) /* Signals */ SC_FORWARD(sc_rt_sigaction, signal_rt_sigaction(g, (int) x0, x1, x2, x3)) diff --git a/src/syscall/time.c b/src/syscall/time.c index 44b016b3..747577d3 100644 --- a/src/syscall/time.c +++ b/src/syscall/time.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include "utils.h" @@ -459,6 +460,81 @@ int64_t sys_gettimeofday(guest_t *g, uint64_t tv_gva, uint64_t tz_gva) return 0; } +/* Linux struct tms: four clock_t fields (long on LP64) counting USER_HZ + * ticks. + */ +typedef struct { + int64_t tms_utime; + int64_t tms_stime; + int64_t tms_cutime; + int64_t tms_cstime; +} linux_tms_t; + +/* Kernel USER_HZ, the unit of clock_t values returned by times(). Must match + * the AT_CLKTCK auxv entry in core/stack.c: libc divides these raw tick counts + * by sysconf(_SC_CLK_TCK), so a mismatch silently rescales every value. + */ +#define LINUX_USER_HZ 100 + +/* Shared by both the timeval-based self accounting and the raw-microsecond + * proc-layer accumulator below, so the two paths cannot drift out of step if + * LINUX_USER_HZ ever changes. + */ +static int64_t usec_to_clock_ticks(uint64_t usec) +{ + return (int64_t) (usec / (1000000ULL / LINUX_USER_HZ)); +} + +static int64_t timeval_to_clock_ticks(const struct timeval *tv) +{ + return usec_to_clock_ticks((uint64_t) tv->tv_sec * 1000000ULL + + (uint64_t) tv->tv_usec); +} + +int64_t sys_times(guest_t *g, uint64_t buf_gva) +{ + /* Linux permits a NULL buffer: only the return value (elapsed ticks) is + * wanted. cutime/cstime come from the proc-layer accumulator fed at each + * guest-child reap, not from RUSAGE_CHILDREN: the emulator also waits on + * helper subprocesses (rosettad translate, sysroot tooling) whose CPU + * would otherwise masquerade as guest child time. + */ + if (buf_gva) { + /* getrusage(RUSAGE_SELF) aggregates the whole emulator process -- + * every host thread plus page-table, syscall-servicing, and Rosetta + * translation overhead -- not just the guest task's own execution, so + * utime/stime over-report relative to what a real guest kernel would + * charge. Unavoidable given this design (there is no per-guest-thread + * host accounting to draw from instead). + */ + struct rusage self; + if (getrusage(RUSAGE_SELF, &self) < 0) + return linux_errno(); + + uint64_t cutime_us, cstime_us; + proc_children_cpu_us(&cutime_us, &cstime_us); + + linux_tms_t tms = { + .tms_utime = timeval_to_clock_ticks(&self.ru_utime), + .tms_stime = timeval_to_clock_ticks(&self.ru_stime), + .tms_cutime = usec_to_clock_ticks(cutime_us), + .tms_cstime = usec_to_clock_ticks(cstime_us), + }; + if (guest_write_small(g, buf_gva, &tms, sizeof(tms)) < 0) + return -LINUX_EFAULT; + } + + /* Linux returns jiffies-since-boot converted to clock_t ticks; POSIX only + * requires an arbitrary-but-fixed reference point, so host CLOCK_MONOTONIC + * satisfies callers that difference successive returns. + */ + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) + return linux_errno(); + return (int64_t) ts.tv_sec * LINUX_USER_HZ + + ts.tv_nsec / (NSEC_PER_SEC / LINUX_USER_HZ); +} + int64_t sys_setitimer(guest_t *g, int which, uint64_t new_gva, uint64_t old_gva) { /* Linux reads new_gva before dispatching on which, so an invalid pointer diff --git a/src/syscall/time.h b/src/syscall/time.h index 65f36e0e..f781bcf9 100644 --- a/src/syscall/time.h +++ b/src/syscall/time.h @@ -25,6 +25,7 @@ int64_t sys_clock_nanosleep(guest_t *g, uint64_t req_gva, uint64_t rem_gva); int64_t sys_gettimeofday(guest_t *g, uint64_t tv_gva, uint64_t tz_gva); +int64_t sys_times(guest_t *g, uint64_t buf_gva); int64_t sys_setitimer(guest_t *g, int which, uint64_t new_gva, diff --git a/tests/manifest.txt b/tests/manifest.txt index 8d4382ac..c3710b3d 100644 --- a/tests/manifest.txt +++ b/tests/manifest.txt @@ -48,6 +48,7 @@ test-socket test-file-ops test-flock test-sysinfo +test-times test-io-opt test-oom-proc test-syscall-smoke diff --git a/tests/test-times.c b/tests/test-times.c new file mode 100644 index 00000000..9db564dc --- /dev/null +++ b/tests/test-times.c @@ -0,0 +1,207 @@ +/* + * Test times(2) process CPU-time accounting + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Tests: return-value tick clock, self utime/stime accounting, waited-for + * child cutime/cstime accounting (counted once, not on stop/WNOWAIT + * snapshots), NULL buffer, EFAULT on a bad pointer. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "raw-syscall.h" +#include "test-harness.h" + +/* Spin until this process has accumulated at least @ms more CPU time. + * CLOCK_PROCESS_CPUTIME_ID and times() draw from the same host accounting, so + * the burned amount is a lower bound for what times() must report. + */ +static void burn_cpu_ms(long ms) +{ + struct timespec start, now; + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start); + volatile unsigned long sink = 0; + do { + for (int i = 0; i < 100000; i++) + sink += (unsigned long) i; + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &now); + } while ((now.tv_sec - start.tv_sec) * 1000L + + (now.tv_nsec - start.tv_nsec) / 1000000L < + ms); +} + +/* Like burn_cpu_ms, but spends the time in real open/write/close syscalls + * rather than pure computation, so the host accounts some of it as system + * time (ru_stime) rather than all user time. + */ +static void burn_syscalls_ms(long ms) +{ + struct timespec start, now; + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start); + char buf[64] = {0}; + do { + for (int i = 0; i < 50; i++) { + int fd = open("/dev/null", O_WRONLY); + if (fd >= 0) { + write(fd, buf, sizeof(buf)); + close(fd); + } + } + clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &now); + } while ((now.tv_sec - start.tv_sec) * 1000L + + (now.tv_nsec - start.tv_nsec) / 1000000L < + ms); +} + +int main(void) +{ + int passes = 0, fails = 0; + + printf("test-times: times(2) accounting tests\n"); + + long tick = sysconf(_SC_CLK_TCK); + TEST("sysconf(_SC_CLK_TCK) == 100"); + EXPECT_EQ(tick, 100, "unexpected clock tick rate"); + + TEST("times(&buf) succeeds"); + struct tms t1; + clock_t r1 = times(&t1); + EXPECT_TRUE(r1 != (clock_t) -1, "times failed"); + + TEST("times(NULL) returns ticks"); + { + long r = raw_syscall1(__NR_times, 0); + /* The return value is elapsed ticks, not an errno. */ + EXPECT_TRUE(r >= 0, "NULL buffer rejected"); + } + + TEST("return value advances with wall time"); + { + /* 60ms >= 6 ticks at 100Hz; require strict advance */ + struct timespec ts = {0, 60 * 1000 * 1000}; + nanosleep(&ts, NULL); + clock_t r2 = times(NULL); + EXPECT_TRUE(r2 != (clock_t) -1 && r2 > r1, + "tick clock did not advance"); + } + + TEST("self CPU time accumulates"); + { + burn_cpu_ms(100); /* >= 10 ticks of process CPU */ + struct tms t2; + if (times(&t2) == (clock_t) -1) + FAIL("times failed"); + else + EXPECT_TRUE( + t2.tms_utime + t2.tms_stime >= t1.tms_utime + t1.tms_stime + 5, + "utime+stime did not grow"); + } + + TEST("waited-for child CPU accumulates"); + { + struct tms before, after; + if (times(&before) == (clock_t) -1) { + FAIL("times(before) failed"); + } else { + pid_t pid = fork(); + if (pid == 0) { + burn_cpu_ms(100); /* >= 10 ticks of child CPU */ + _exit(0); + } + if (pid < 0) { + FAIL("fork failed"); + } else { + int status; + if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status) || + WEXITSTATUS(status) != 0) { + FAIL("child did not exit cleanly"); + } else if (times(&after) == (clock_t) -1) { + FAIL("times(after) failed"); + } else { + EXPECT_TRUE(after.tms_cutime + after.tms_cstime >= + before.tms_cutime + before.tms_cstime + 5, + "cutime+cstime did not grow"); + } + } + } + } + + TEST("child cutime and cstime accumulate individually"); + { + struct tms before, after; + if (times(&before) == (clock_t) -1) { + FAIL("times(before) failed"); + } else { + pid_t pid = fork(); + if (pid == 0) { + burn_syscalls_ms(150); /* real syscalls: nonzero cstime too */ + _exit(0); + } + if (pid < 0) { + FAIL("fork failed"); + } else { + int status; + if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status) || + WEXITSTATUS(status) != 0) { + FAIL("child did not exit cleanly"); + } else if (times(&after) == (clock_t) -1) { + FAIL("times(after) failed"); + } else if (after.tms_cutime <= before.tms_cutime) { + FAIL("cutime did not grow individually"); + } else { + /* A regression that always reports cstime==0 must not + * pass here: this child's loop is real host syscalls, not + * pure computation, so cstime alone must also grow. + */ + EXPECT_TRUE(after.tms_cstime > before.tms_cstime, + "cstime did not grow individually"); + } + } + } + } + + TEST("waitid(WNOWAIT) does not credit cutime on the peek"); + { + struct tms before, after; + if (times(&before) == (clock_t) -1) { + FAIL("times(before) failed"); + } else { + pid_t pid = fork(); + if (pid == 0) { + burn_cpu_ms(80); + _exit(0); + } + if (pid < 0) { + FAIL("fork failed"); + } else { + siginfo_t info; + memset(&info, 0, sizeof(info)); + long rc = syscall(SYS_waitid, P_PID, (long) pid, &info, + WEXITED | WNOWAIT, NULL); + if (rc != 0 || info.si_pid != pid) { + FAIL("waitid(WNOWAIT) failed"); + } else if (times(&after) == (clock_t) -1) { + FAIL("times(after) failed"); + } else { + EXPECT_TRUE(after.tms_cutime == before.tms_cutime, + "WNOWAIT peek incorrectly credited cutime"); + } + } + } + } + + TEST("times(bad_ptr) -> EFAULT"); + EXPECT_RAW_ERRNO(raw_syscall1(__NR_times, (long) 0xDEAD000000000000ULL), + -14, "expected -EFAULT (-14)"); + + SUMMARY("test-times"); + return fails > 0 ? 1 : 0; +}