mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
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:
1 parent
6cf33b9c4e
commit
66d8484ed0
2 files changed
+122
-77
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user