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; +}