page, kernel: the clock calls never take the kernel lock

docs/kernel-locking.md §5.2 step 1, clocks: lock deleted. clock_gettime,
clock_getres, gettimeofday, getrusage and sched_yield are answered by
clock_syscall, always, with no locked fallback. 3d9814ec's fast path stood
down whenever a signal sequence had moved, on every 64th call, under
tracing, and whenever a provider had registered a thread -- the last for
good in every W3 process, whose native Wine DLLs register: Undertale's game
process still took the kernel lock for about 5,500 clock_gettime, 700
getrusage and 300 sched_yield calls a second (diag/klock-6e census,
worldmachine).

- The ABI and the process's start are process constants (clock_constants),
  set at load, exec and a child's or thread's adoption, before the first
  syscall; no syscall refreshes them. Kernel::started is set there too, so
  the processor-time clocks agree whichever path reads them.
- gex_kernel::syscalls::clock_call answers every clock number, EFAULT
  included, and clock_getres; it declines only other numbers.
- Pending signals are delivered at the next safepoint, which checks for
  them on its own; a provider collection parks the thread on its way back
  (libprov::park_if_stopping, libprov's own lock).
- A clock call that reaches the locked path anyway says so once, as an
  error every page test fails on.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-12 12:54:13 -04:00
1 parent 6cf33b9c4e
commit 66d8484ed0
2 files changed
+122 -77

No files matched your search

+34 -24
View File
@@ -823,14 +823,15 @@ fn monotonic_from((secs, nanos): (u64, u32)) -> (u64, u32) {
/// needs the kernel: the answer is the clock, and `started` (the process's
/// [`Kernel::started`], which the processor-time clocks count from).
///
/// Returns false, writing nothing that matters, for any other call, for a
/// processor-time question before `started` is known (0), and for a buffer
/// the guest cannot be written at: the host then makes the call as usual, so
/// the answer and any `EFAULT` are the kernel's. `compat` is
/// [`Kernel::compat`] (the i386 numbering, number in `eax`, 32-bit layouts).
/// The result goes where [`set_result`] puts it. Signal delivery, which the
/// kernel does at every syscall boundary, is the host's to arrange: it falls
/// back to the full call whenever a signal may be pending.
/// Returns false, writing nothing, for any other call. A clock call is
/// always answered here, `EFAULT` included (a buffer the guest cannot be
/// written at, as the kernel's own calls answer), so a host that routes these
/// numbers here never takes its kernel for them (docs/kernel-locking.md §5.2
/// step 1). `compat` is [`Kernel::compat`] (the i386 numbering, number in
/// `eax`, 32-bit layouts); `started` the process's start, which the
/// processor-time clocks count from. The result goes where [`set_result`]
/// puts it. Signal delivery, which the kernel does at every syscall
/// boundary, is the host's to arrange (the page's safepoints).
pub fn clock_call(
m: &mut GuestMem<'_>,
ctx_addr: u64,
@@ -855,6 +856,8 @@ pub fn clock_call(
enum Call {
// (clock id, buffer, 32-bit timespec)
Clock(u64, u64, bool),
// (buffer, 32-bit timespec)
Res(u64, bool),
// (buffer, 32-bit timeval)
Tod(u64, bool),
Rusage(u64),
@@ -865,6 +868,8 @@ pub fn clock_call(
match raw {
x::CLOCK_GETTIME => Call::Clock(a0, a1, true),
x::CLOCK_GETTIME64 => Call::Clock(a0, a1, false),
x::CLOCK_GETRES => Call::Res(a1, true),
x::CLOCK_GETRES_TIME64 => Call::Res(a1, false),
x::GETTIMEOFDAY => Call::Tod(a0, true),
x::GETRUSAGE => Call::Rusage(a1),
x::SCHED_YIELD => Call::Yield,
@@ -875,6 +880,7 @@ pub fn clock_call(
// unchanged but for the number.
match raw {
228 => Call::Clock(a0, a1, false),
229 => Call::Res(a1, false),
96 => Call::Tod(a0, false),
98 => Call::Rusage(a1),
24 => Call::Yield,
@@ -883,15 +889,17 @@ pub fn clock_call(
} else {
match raw {
nr::CLOCK_GETTIME => Call::Clock(a0, a1, false),
nr::CLOCK_GETRES => Call::Res(a1, false),
nr::GETTIMEOFDAY => Call::Tod(a0, false),
nr::GETRUSAGE => Call::Rusage(a1),
nr::SCHED_YIELD => Call::Yield,
_ => return false,
}
};
// Before the host has said when the process started (0): no time used.
let cpu = || {
let (s, n) = clock();
(s as u128 * 1_000_000_000 + n as u128).saturating_sub(started)
if started == 0 { 0 } else { (s as u128 * 1_000_000_000 + n as u128).saturating_sub(started) }
};
let pair = |m: &mut GuestMem<'_>, at: u64, hi: u64, lo: u64, w32: bool| {
if w32 {
@@ -903,9 +911,6 @@ pub fn clock_call(
let ok = match call {
Call::Clock(clk, buf, w32) => {
// As `sys_clock_gettime`, which a null buffer faults in.
if buf == 0 || (matches!(clk, 2 | 3) && started == 0) {
return false;
}
let (secs, nanos) = match clk {
2 | 3 => {
let t = cpu();
@@ -914,8 +919,9 @@ pub fn clock_call(
c if is_monotonic_clock(c) => monotonic_from(clock()),
_ => clock(),
};
pair(m, buf, secs, nanos as u64, w32).is_ok()
buf != 0 && pair(m, buf, secs, nanos as u64, w32).is_ok()
}
Call::Res(buf, w32) => buf == 0 || pair(m, buf, 0, 1, w32).is_ok(),
Call::Tod(buf, w32) => {
if buf == 0 {
true
@@ -925,9 +931,6 @@ pub fn clock_call(
}
}
Call::Rusage(buf) => {
if started == 0 {
return false;
}
if buf == 0 {
true
} else {
@@ -941,13 +944,12 @@ pub fn clock_call(
// browser schedules those without being asked.
Call::Yield => true,
};
if !ok {
return false;
}
if gex_abi::X86 {
let _ = m.write_u64(ctx_addr + ctx::LAST_SYSCALL as u64, raw);
}
set_result(m, ctx_addr, 0).is_ok()
let ret = if ok { 0 } else { (Errno::EFAULT.0 as i64).wrapping_neg() as u64 };
let _ = set_result(m, ctx_addr, ret);
true
}
/// What a process and thread are called, as a host last saw them under its
@@ -8036,15 +8038,23 @@ mod tests {
assert_eq!(m.read(buf, 160).unwrap(), &want[..], "call {n} {args:?}");
assert_eq!(ret(&m, at), want_ret, "call {n}");
}
// Declined: another call, a null clock buffer, the processor clock
// before the process's start is known.
// Declined: another call. Answered in place: a null clock buffer
// (EFAULT, as the kernel's), the processor clock before the
// process's start is known (no time used), and the resolution.
let mut m = GuestMem::new(&mut mem);
set_args(&mut m, at, nr::GETPID, &[]);
assert!(!clock_call(&mut m, at, false, fixed, k.started));
set_args(&mut m, at, nr::CLOCK_GETTIME, &[0, 0]);
assert!(!clock_call(&mut m, at, false, fixed, k.started));
assert!(clock_call(&mut m, at, false, fixed, k.started));
assert_eq!(ret(&m, at) as i64, -(Errno::EFAULT.0 as i64));
m.write_u64(buf, !0).unwrap();
set_args(&mut m, at, nr::GETRUSAGE, &[0, buf]);
assert!(!clock_call(&mut m, at, false, fixed, 0));
assert!(clock_call(&mut m, at, false, fixed, 0));
assert_eq!(m.read_u64(buf).unwrap(), 0);
assert_eq!(ret(&m, at), 0);
set_args(&mut m, at, nr::CLOCK_GETRES, &[1, buf]);
assert!(clock_call(&mut m, at, false, fixed, k.started));
assert_eq!((m.read_u64(buf).unwrap(), m.read_u64(buf + 8).unwrap()), (0, 1));
// i386: the number in eax, arguments in ebx/ecx, 32-bit layouts.
let set32 = |m: &mut GuestMem<'_>, n: u64, a: [u64; 2]| {
m.write_u64(at + ctx::x(0) as u64, n).unwrap();
+88 -53
View File
@@ -4147,6 +4147,10 @@ pub extern "C" fn gex_load(ptr: u32, len: u32, argv: u32, argv_len: u32) -> u64
kernel.compat = program.compat();
gex_xlate::set_compat(program.compat());
PROCESS_STARTED.store(now_ns(), std::sync::atomic::Ordering::Relaxed);
// The clock calls' process constants (`clock_syscall`), before the
// process's first syscall.
kernel.started = now_ns() as u128;
clock_constants(program.compat(), kernel.started);
// And the same for the first process, which opens its console before it
// makes a syscall. See the note beside the other one.
MY_PID.store(kernel.pid, std::sync::atomic::Ordering::Relaxed);
@@ -6481,8 +6485,8 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
}
}
}
// The clock calls, answered without the kernel lock (see `clock_fast_path`).
if !IN_PROVIDER.with(|c| c.get()) && clock_fast_path(ctx) {
// The clock calls, which never take the kernel lock (`clock_syscall`).
if !IN_PROVIDER.with(|c| c.get()) && clock_syscall(ctx) {
return;
}
fjit::watch_check(pc as u64, "syscall");
@@ -6503,7 +6507,7 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
let base = ctx as u32 as u64;
let n = gm.read_u64(base + ctx::SYSCALL_NR as u64).unwrap_or(u64::MAX);
// An i386-ABI process (W3's Wine): its number, in `x(0)`, is i386's.
let compat = FAST_ABI.load(std::sync::atomic::Ordering::Relaxed) == 1;
let compat = CLOCK_ABI.load(std::sync::atomic::Ordering::Relaxed) == 1;
let n = if compat && !gex_abi::PPC {
let raw = gm.read_u64(base + ctx::x(0) as u64).unwrap_or(u64::MAX);
gex_kernel::i386::canonical_number(raw)
@@ -6808,17 +6812,15 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
}
}
let slot = sys_note(nr, a0, a1);
// The signals this call will see, read before it takes them: a fast
// clock call after it is safe only while no newer one was sent.
let sig_seen = if unsafe { k_present() } != 0 { unsafe { k_signal_seq() } } else { 0 };
let outcome = h.kernel.syscall(&mut gm, pc as u64);
FAST_ABI.store(
if h.kernel.compat { 1 } else { 2 },
std::sync::atomic::Ordering::Relaxed,
);
FAST_STARTED.store(h.kernel.started as u64, std::sync::atomic::Ordering::Relaxed);
FAST_SIG.with(|c| c.set(sig_seen));
FAST_RUN.with(|c| c.set(0));
// The clocks never come here (`clock_syscall`); one that does is a
// regression, said once, as an error every page test fails on.
if is_clock_nr(h.kernel.syscall_nr) && !IN_PROVIDER.with(|c| c.get()) && !EXITED.load(std::sync::atomic::Ordering::Relaxed) {
static SAID: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !SAID.swap(true, std::sync::atomic::Ordering::Relaxed) {
warn_line(&format!("[gex] pid {}: clock call {} took the kernel lock\n", h.kernel.pid, h.kernel.syscall_nr));
}
}
CANON_NR.with(|c| c.set(h.kernel.syscall_nr));
FD_ACTIVITY.with(|c| c.set(h.kernel.take_fd_activity()));
let ret = gm.read_u64(kstate.ctx_addr + ctx::x(0) as u64).unwrap_or(0);
@@ -7563,19 +7565,29 @@ fn wakes_nobody(nr: u64) -> bool {
)
}
/// What `clock_fast_path` needs to know about this process without taking
/// the kernel: its ABI (0 not yet known, 1 i386, 2 native) and when it
/// started (`Kernel::started`), both refreshed by every full syscall.
static FAST_ABI: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
static FAST_STARTED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// Clock calls answered by `clock_fast_path`.
static FAST_CALLS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// The clock calls' process constants (`clock_syscall`): the ABI (0 before
/// the process is loaded, 1 i386, 2 native) and when the process started
/// (`Kernel::started`, ns), which the processor-time clocks count from. Set
/// at load, exec and a child's or thread's adoption (`clock_constants`),
/// before the process's first syscall, and not changed by any syscall after.
static CLOCK_ABI: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
static CLOCK_STARTED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// Clock calls answered by `clock_syscall`.
static CLOCK_CALLS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// The clock-family numbers (canonical), which `clock_syscall` answers.
fn is_clock_nr(n: u64) -> bool {
use gex_kernel::syscalls::nr;
matches!(n, nr::CLOCK_GETTIME | nr::CLOCK_GETRES | nr::GETTIMEOFDAY | nr::GETRUSAGE | nr::SCHED_YIELD)
}
fn clock_constants(compat: bool, started: u128) {
use std::sync::atomic::Ordering::Release;
CLOCK_STARTED.store(started as u64, Release);
CLOCK_ABI.store(if compat { 1 } else { 2 }, Release);
}
thread_local! {
/// `k_signal_seq` as this thread's last full syscall read it.
static FAST_SIG: Cell<i32> = const { Cell::new(i32::MIN) };
/// Fast calls since this thread's last full syscall.
static FAST_RUN: Cell<u32> = const { Cell::new(0) };
/// This thread's ids as its last full syscall saw them (`ident_call`).
static IDS: Cell<gex_kernel::syscalls::Idents> = const {
Cell::new(gex_kernel::syscalls::Idents { pid: 0, ppid: 0, tid: 0, uid: 0, gid: 0 })
@@ -7588,43 +7600,54 @@ thread_local! {
static FD_ACTIVITY: Cell<bool> = const { Cell::new(false) };
}
/// `clock_gettime`, `gettimeofday`, `getrusage` and `sched_yield` served
/// lock-free (`gex_kernel::syscalls::clock_call`), with no proc snapshot and
/// no wake. Every other syscall takes the host lock several times
/// (`with_host`), and a W3 process's ~25 idle Wine Workers contend for it:
/// Undertale's game thread asks the time ~2,000 times a second (Wine's
/// `NtDelayExecution` calls `NtYieldExecution`, which is getrusage,
/// sched_yield, getrusage) and spent ~25 ms of an 87 ms frame in `KLock::take`
/// for these alone.
/// The clocks, with no kernel lock (docs/kernel-locking.md §5.2 step 1):
/// `clock_gettime`, `clock_getres`, `gettimeofday`, `getrusage` and
/// `sched_yield` are answered here, always, from the clock and the process's
/// constants (`gex_kernel::syscalls::clock_call`, `EFAULT` included). They
/// read nothing the kernel guards: the time base is fixed at boot
/// (`k_boot_secs`), the ABI and start at load. Undertale's game process makes
/// about 7,000 of them a second (Wine's `NtDelayExecution` is getrusage,
/// sched_yield, getrusage, and its timers read the clock).
///
/// The kernel delivers pending signals at every syscall boundary, which this
/// skips; so it stands down -- the full call runs -- while a signal may be
/// waiting: whenever `k_signal_seq` has moved since this thread's last full
/// call, and on every 64th call in a row (a signal raised inside the process
/// does not move the sequence).
fn clock_fast_path(ctx: i32) -> bool {
use std::sync::atomic::Ordering::Relaxed;
let abi = FAST_ABI.load(Relaxed);
if abi == 0 || tracing() || EXITED.load(Relaxed) || cgen::libprov::any_registered() {
return false;
}
if unsafe { k_present() } != 0 && FAST_SIG.with(|c| c.get()) != unsafe { k_signal_seq() } {
return false;
}
if FAST_RUN.with(|c| c.get()) >= 63 {
/// Not done here, as the kernel would at a syscall boundary: delivering a
/// pending signal, which the next safepoint does (`gex_safepoint`'s own
/// check); and a provider collection's stop (cgen/libprov.rs), which parks
/// the thread on its way back to guest code, as after any syscall.
///
/// The getpid family is answered here too, from this thread's ids as its
/// last full syscall left them (`ident_call`, step 6a).
fn clock_syscall(ctx: i32) -> bool {
use std::sync::atomic::Ordering::{Acquire, Relaxed};
let abi = CLOCK_ABI.load(Acquire);
// Before the process is loaded there is no guest to make a call; once it
// has exited, the full path ends the thread.
if abi == 0 || EXITED.load(Relaxed) {
return false;
}
let mut gm = GuestMem::new(guest_bytes());
let started = FAST_STARTED.load(Relaxed) as u128;
if !gex_kernel::syscalls::clock_call(&mut gm, ctx as u32 as u64, abi == 1, browser_clock, started)
&& !IDS.with(|c| gex_kernel::syscalls::ident_call(&mut gm, ctx as u32 as u64, abi == 1, &c.get()))
let started = CLOCK_STARTED.load(Acquire) as u128;
let at = ctx as u32 as u64;
if !gex_kernel::syscalls::clock_call(&mut gm, at, abi == 1, browser_clock, started)
&& !IDS.with(|c| gex_kernel::syscalls::ident_call(&mut gm, at, abi == 1, &c.get()))
{
return false;
}
FAST_RUN.with(|c| c.set(c.get() + 1));
SYS_CALLS.fetch_add(1, Relaxed);
if tracing() {
let nr = gm.read_u64(at + if abi == 1 { ctx::x(0) } else { ctx::SYSCALL_NR } as u64).unwrap_or(0);
let ret = gm.read_u64(at + ctx::x(0) as u64).unwrap_or(0);
trace_line(&format!("[sys] {nr} (no kernel lock) = {ret:#x}\n"));
}
// A provider's collection running: park here, as the full path does on
// its way back to guest code.
if cgen::libprov::stop_active() {
let tid = IDS.with(|c| c.get().tid);
if tid != 0 {
cgen::libprov::park_if_stopping(tid as u32, false);
}
}
// Narration, not a warning: a page test fails on any console error.
if FAST_CALLS.fetch_add(1, Relaxed) == 0 {
if CLOCK_CALLS.fetch_add(1, Relaxed) == 0 {
trace_line(&format!(
"[gex] pid {}: clock calls served without the kernel lock\n",
MY_PID.load(Relaxed)
@@ -8849,6 +8872,11 @@ fn do_exec(h: &mut Host, path: &str, argv: &[String], envp: &[String]) -> u64 {
h.kernel.compat = p.compat();
gex_xlate::set_compat(p.compat());
}
// An exec keeps the process's start (its processor time goes on).
if h.kernel.started == 0 {
h.kernel.started = now_ns() as u128;
}
clock_constants(h.kernel.compat, h.kernel.started);
// Set again when a wasm32 program is published (cgen/wasmprog.rs).
h.kernel.wasm32 = false;
cgen::wasmprog::stage(program.as_ref());
@@ -11323,7 +11351,14 @@ pub extern "C" fn gex_compat() -> i32 {
/// state has been adopted, before any block is translated there.
#[no_mangle]
pub extern "C" fn gex_set_compat(on: i32) {
with_host(|h| h.kernel.compat = on != 0);
with_host(|h| {
h.kernel.compat = on != 0;
// A child's process starts now; a thread's is its process's, already set.
if h.kernel.started == 0 {
h.kernel.started = now_ns() as u128;
}
clock_constants(h.kernel.compat, h.kernel.started);
});
// An adopted child's stack (`gex_adopt_child`, before this) went to
// `ctx::SP`; an i386-ABI process's is `esp` (`ctx::sp_slot`), which on
// an AArch64 build is another slot (its i386-ABI processes are wasm32