mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
page, kernel: waits on wait queues; the global wake word deleted
docs/kernel-locking.md §5.2 step 2, Linux's wait-queue model. Every waitable object owns a wait queue (gex_kernel::wait::Queue): a pipe, a unix socket (each direction's reader and writer), an eventfd, a timerfd, an epoll set, a pty, the terminal's input, a futex's bucket, a process. The queues live in the global kernel's shared memory (shims/kernel wait.rs): a 64-byte slot bitmap plus inline chains, walkable from JS. Each Worker's thread owns one wait slot (its kernel stack's number, k_ws_self). When the scheduler turn finds its thread waiting, it registers the slot on the queue of every object the wait names -- each descriptor of a poll, the epoll set (whose queue is chained onto every descriptor it watches, rechained as the interest list changes), the futex bucket, and its process's queue (signals, a child's exit, the process ending) -- re-checks readiness, and parks on the slot until a notify or its own deadline (a sleep, a timed poll, a timerfd's or a full /dev/dsp's clock, the process's interval timer). Writers, closers, connects and accepts notify their object's queue, which wakes the registered slots and nobody else. The slot is unregistered when the wait ends. Deleted: the global WAKE word (k_wake, k_park, k_wake_seq, k_wake_addr), k_park_futex, the k_wake after every syscall (wakes_waiters, wakes_nobody, CANON_NR, FD_ACTIVITY), the compositor pump's blanket wake, PARK_PLAN/WAKE_SEEN, and the 20 ms and 100 ms park bounds: a park ends by its notify or its deadline. A turn of a wait already registered asks only its own thread, without the kernel lock (waitq::idle_turn); it takes the lock to finish the call once the wait is over, to pump the compositor when it has work, or when a signal may have arrived. gex_park, park_until_ready and gex_wait_bound_ms take no lock. Converted from polling to waits: FUTEX_LOCK_PI (Waiting::Pi, ready when the owner releases or the generation moves) and sigsuspend (Waiting::Signal, ended by the process queue). The page: the compositor notifies the process's queue after an input event (wl-bridge notifyQueue); the hosted Xwayland server has a slot of its own registered on its sockets and its own queue; the network wait waits on the slot word. The SP build parks fibers on their slot words (Sched.parkSlot) and looks at them after every notify it can see (checkSlots, at each park and yield). Lock-taking sites in shims/browser/src: 214 (219 at66d8484e, 232 at masterf3689d12). 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
66d8484ed0
commit
d26f0d41e4
27 files changed
+1983
-753
No files matched your search
@@ -33,6 +33,8 @@ const MAX: u64 = u64::MAX - 1;
|
||||
pub struct EventFd {
|
||||
count: Mutex<u64>,
|
||||
semaphore: bool,
|
||||
/// Who waits on it (`crate::wait`): a read makes room, a write a value.
|
||||
queue: crate::wait::Queue,
|
||||
}
|
||||
|
||||
impl EventFd {
|
||||
@@ -40,6 +42,7 @@ impl EventFd {
|
||||
EventFd {
|
||||
count: Mutex::new(initval),
|
||||
semaphore,
|
||||
queue: crate::wait::Queue::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +66,8 @@ impl FileHandle for EventFd {
|
||||
} else {
|
||||
std::mem::replace(&mut *c, 0)
|
||||
};
|
||||
drop(c);
|
||||
self.queue.notify();
|
||||
buf[..8].copy_from_slice(&val.to_ge_bytes());
|
||||
Ok(8)
|
||||
}
|
||||
@@ -84,6 +89,8 @@ impl FileHandle for EventFd {
|
||||
return Err(Errno::EAGAIN);
|
||||
}
|
||||
*c += val;
|
||||
drop(c);
|
||||
self.queue.notify();
|
||||
Ok(8)
|
||||
}
|
||||
|
||||
@@ -110,6 +117,10 @@ impl FileHandle for EventFd {
|
||||
*self.count.lock().unwrap() > 0
|
||||
}
|
||||
|
||||
fn wait_queue(&self) -> u32 {
|
||||
self.queue.id()
|
||||
}
|
||||
|
||||
fn poll_writable(&self) -> bool {
|
||||
*self.count.lock().unwrap() < MAX
|
||||
}
|
||||
|
||||
@@ -497,6 +497,10 @@ impl FileHandle for PipeEnd {
|
||||
fn is_stream(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn wait_queue(&self) -> u32 {
|
||||
self.pipe.wait_queue()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PipeEnd {
|
||||
|
||||
@@ -58,6 +58,7 @@ pub mod syscalls;
|
||||
pub mod timerfd;
|
||||
pub mod tty;
|
||||
pub mod vfs;
|
||||
pub mod wait;
|
||||
pub mod unix;
|
||||
/// The x87's extended-precision arithmetic (its own crate, shared with the
|
||||
/// IR interpreter and the code generators).
|
||||
|
||||
@@ -43,7 +43,8 @@ struct State {
|
||||
pub struct Pipe {
|
||||
state: Mutex<State>,
|
||||
ready: Condvar,
|
||||
|
||||
/// Who waits on either end (`crate::wait`): notified wherever `ready` is.
|
||||
queue: crate::wait::Queue,
|
||||
}
|
||||
|
||||
impl Default for Pipe {
|
||||
@@ -67,9 +68,14 @@ impl Pipe {
|
||||
broken: 0,
|
||||
}),
|
||||
ready: Condvar::new(),
|
||||
queue: crate::wait::Queue::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The wait queue both ends' waiters register on.
|
||||
pub fn wait_queue(&self) -> u32 {
|
||||
self.queue.id()
|
||||
}
|
||||
|
||||
|
||||
/// Note that another descriptor now refers to one end.
|
||||
@@ -93,6 +99,7 @@ impl Pipe {
|
||||
// Waking both sides is what turns the last close into an EOF or an
|
||||
// EPIPE rather than a hang.
|
||||
self.ready.notify_all();
|
||||
self.queue.notify();
|
||||
}
|
||||
|
||||
pub fn read(&self, buf: &mut [u8]) -> SysResult {
|
||||
@@ -111,6 +118,7 @@ impl Pipe {
|
||||
buf[k..n].copy_from_slice(&back[..n - k]);
|
||||
s.buf.drain(..n);
|
||||
self.ready.notify_all();
|
||||
self.queue.notify();
|
||||
return Ok(n as u64);
|
||||
}
|
||||
// No data: end of file once no writer can ever supply any.
|
||||
@@ -153,6 +161,7 @@ impl Pipe {
|
||||
s.last.extend(buf[written..written + n].iter().take(160));
|
||||
written += n;
|
||||
self.ready.notify_all();
|
||||
self.queue.notify();
|
||||
if written == buf.len() {
|
||||
return Ok(written as u64);
|
||||
}
|
||||
@@ -357,6 +366,8 @@ pub struct Remote {
|
||||
pub readable: extern "C" fn(id: i32) -> i32,
|
||||
pub writable: extern "C" fn(id: i32) -> i32,
|
||||
pub buffered: extern "C" fn(id: i32) -> i32,
|
||||
/// The pipe's wait queue (`crate::wait`).
|
||||
pub queue: extern "C" fn(id: i32) -> u32,
|
||||
}
|
||||
|
||||
/// One end of a pipe in the global kernel. Dropping closes that end there, so
|
||||
@@ -433,6 +444,10 @@ impl crate::vfs::FileHandle for RemotePipeEnd {
|
||||
!self.write || (self.remote.writable)(self.id) != 0
|
||||
}
|
||||
|
||||
fn wait_queue(&self) -> u32 {
|
||||
(self.remote.queue)(self.id)
|
||||
}
|
||||
|
||||
/// The read end of a pipe with room in it: no writer is parked on it.
|
||||
/// (One that fills it between this question and the read sleeps to its
|
||||
/// park's bound, or the next wake, not for ever.)
|
||||
|
||||
@@ -62,6 +62,8 @@ pub struct Remote {
|
||||
pub wait_ready: extern "C" fn(ppid: i32, want: i32) -> i32,
|
||||
pub request_halt: extern "C" fn(code: i32),
|
||||
pub halt_code: extern "C" fn() -> i32,
|
||||
/// The process's wait queue. See [`ProcTable::queue`].
|
||||
pub queue: extern "C" fn(pid: i32) -> u32,
|
||||
}
|
||||
|
||||
/// One process, as the table sees it.
|
||||
@@ -87,6 +89,10 @@ struct Proc {
|
||||
/// raised), and the pid that sent each. Under the table's lock.
|
||||
killed: u64,
|
||||
senders: [i32; 64],
|
||||
/// The process's own wait queue (`crate::wait`), on which each of its
|
||||
/// threads keeps its wait slot registered for as long as it runs: a
|
||||
/// signal for the process, a child's exit or a halt notifies it.
|
||||
queue: crate::wait::Queue,
|
||||
}
|
||||
|
||||
struct Table {
|
||||
@@ -143,6 +149,7 @@ impl ProcTable {
|
||||
pending: Arc::new(AtomicU64::new(0)),
|
||||
killed: 0,
|
||||
senders: [0; 64],
|
||||
queue: crate::wait::Queue::new(),
|
||||
}],
|
||||
}),
|
||||
}
|
||||
@@ -175,6 +182,7 @@ impl ProcTable {
|
||||
pending: Arc::new(AtomicU64::new(0)),
|
||||
killed: 0,
|
||||
senders: [0; 64],
|
||||
queue: crate::wait::Queue::new(),
|
||||
});
|
||||
pid
|
||||
}
|
||||
@@ -216,7 +224,7 @@ impl ProcTable {
|
||||
if let Some(r) = self.remote {
|
||||
return (r.exit)(pid, status);
|
||||
}
|
||||
let (cell, done) = {
|
||||
let (cell, done, parent_q) = {
|
||||
let mut t = self.lock();
|
||||
// The parent is owed a SIGCHLD. A `wait4` parked on the condvar
|
||||
// doesn't need one, but a parent that polls `WNOHANG` and sleeps in
|
||||
@@ -235,8 +243,11 @@ impl ProcTable {
|
||||
p.ppid = INIT_PID;
|
||||
}
|
||||
}
|
||||
let parent_q = ppid
|
||||
.and_then(|ppid| t.procs.iter().find(|p| p.pid == ppid))
|
||||
.map_or(0, |p| p.queue.id());
|
||||
match t.procs.iter().find(|p| p.pid == pid) {
|
||||
Some(p) => (Arc::clone(&p.child.status), Arc::clone(&p.child.done)),
|
||||
Some(p) => (Arc::clone(&p.child.status), Arc::clone(&p.child.done), parent_q),
|
||||
None => return,
|
||||
}
|
||||
};
|
||||
@@ -244,6 +255,8 @@ impl ProcTable {
|
||||
// the table, so holding both here in the other order deadlocks.
|
||||
*cell.lock().unwrap_or_else(|e| e.into_inner()) = Some(status);
|
||||
done.notify_all();
|
||||
// The parent's `wait4`, and its SIGCHLD.
|
||||
crate::wait::notify(parent_q);
|
||||
}
|
||||
|
||||
/// Forget a collected process, so a long-lived init doesn't accumulate a
|
||||
@@ -405,6 +418,10 @@ impl ProcTable {
|
||||
p.senders[signo as usize - 1] = caller;
|
||||
p.pending.fetch_or(bit, Ordering::AcqRel);
|
||||
}
|
||||
// A signal ends a wait: wake each target's waiting threads.
|
||||
for &i in &hits {
|
||||
t.procs[i].queue.notify();
|
||||
}
|
||||
}
|
||||
Ok(hits.len())
|
||||
}
|
||||
@@ -420,15 +437,15 @@ impl ProcTable {
|
||||
return Err(Errno::EINVAL);
|
||||
}
|
||||
let t = self.lock();
|
||||
let hits: Vec<&Arc<AtomicU64>> =
|
||||
t.procs.iter().filter(|p| p.pgid == pgid).map(|p| &p.pending).collect();
|
||||
let hits: Vec<&Proc> = t.procs.iter().filter(|p| p.pgid == pgid).collect();
|
||||
if hits.is_empty() {
|
||||
return Err(Errno::ESRCH);
|
||||
}
|
||||
if signo > 0 {
|
||||
let bit = 1u64 << (signo - 1);
|
||||
for p in &hits {
|
||||
p.fetch_or(bit, Ordering::AcqRel);
|
||||
p.pending.fetch_or(bit, Ordering::AcqRel);
|
||||
p.queue.notify();
|
||||
}
|
||||
}
|
||||
Ok(hits.len())
|
||||
@@ -514,6 +531,19 @@ impl ProcTable {
|
||||
}
|
||||
self.halt_code.store(code, Ordering::Release);
|
||||
self.halting.store(true, Ordering::Release);
|
||||
// Every waiter notices the halt on its way out of its wait.
|
||||
for p in self.lock().procs.iter() {
|
||||
p.queue.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// `pid`'s wait queue, 0 for none: a thread of `pid` keeps its wait slot
|
||||
/// on it while it runs (see [`Proc::queue`]).
|
||||
pub fn queue(&self, pid: i32) -> u32 {
|
||||
if let Some(r) = self.remote {
|
||||
return (r.queue)(pid);
|
||||
}
|
||||
self.lock().procs.iter().find(|p| p.pid == pid).map_or(0, |p| p.queue.id())
|
||||
}
|
||||
|
||||
/// The exit code the machine is halting with, if it is.
|
||||
|
||||
@@ -54,6 +54,8 @@ pub struct Pty {
|
||||
/// the line discipline itself.
|
||||
pub tty: Tty,
|
||||
st: Mutex<State>,
|
||||
/// Who waits on either end (`crate::wait`).
|
||||
queue: crate::wait::Queue,
|
||||
}
|
||||
|
||||
impl Pty {
|
||||
@@ -71,9 +73,21 @@ impl Pty {
|
||||
slave_ever: false,
|
||||
slave_eof: false,
|
||||
}),
|
||||
queue: crate::wait::Queue::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The pair's wait queue.
|
||||
pub fn queue_id(&self) -> u32 {
|
||||
self.queue.id()
|
||||
}
|
||||
|
||||
/// Something changed that a reader on either end may be waiting for: the
|
||||
/// terminal's foreground group, say, which decides whose read may go on.
|
||||
pub fn notify(&self) {
|
||||
self.queue.notify();
|
||||
}
|
||||
|
||||
fn lock(&self) -> MutexGuard<'_, State> {
|
||||
self.st.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
@@ -104,6 +118,8 @@ impl Pty {
|
||||
if r.eof {
|
||||
s.slave_eof = true;
|
||||
}
|
||||
drop(s);
|
||||
self.queue.notify();
|
||||
r.signals
|
||||
}
|
||||
|
||||
@@ -116,6 +132,8 @@ impl Pty {
|
||||
Some(v) => s.to_master.extend(v),
|
||||
None => s.to_master.extend(bytes.iter().copied()),
|
||||
}
|
||||
drop(s);
|
||||
self.queue.notify();
|
||||
}
|
||||
|
||||
/// The master reads echo and program output. Empty and every slave gone is
|
||||
@@ -174,6 +192,7 @@ impl Pty {
|
||||
}
|
||||
drop(s);
|
||||
self.tty.flush_line();
|
||||
self.queue.notify();
|
||||
}
|
||||
|
||||
pub fn open_master(&self) {
|
||||
@@ -189,11 +208,16 @@ impl Pty {
|
||||
pub fn close_master(&self) -> bool {
|
||||
let mut s = self.lock();
|
||||
s.master_open = s.master_open.saturating_sub(1);
|
||||
s.master_open == 0
|
||||
let last = s.master_open == 0;
|
||||
drop(s);
|
||||
self.queue.notify();
|
||||
last
|
||||
}
|
||||
pub fn close_slave(&self) {
|
||||
let mut s = self.lock();
|
||||
s.slave_open = s.slave_open.saturating_sub(1);
|
||||
drop(s);
|
||||
self.queue.notify();
|
||||
}
|
||||
pub fn master_count(&self) -> u32 {
|
||||
self.lock().master_open
|
||||
@@ -250,6 +274,8 @@ pub struct Remote {
|
||||
pub sid: extern "C" fn(id: i32) -> i32,
|
||||
pub set_sid: extern "C" fn(id: i32, sid: i32),
|
||||
pub flush: extern "C" fn(id: i32, which: i32),
|
||||
/// The pair's wait queue (`crate::wait`).
|
||||
pub queue: extern "C" fn(id: i32) -> u32,
|
||||
}
|
||||
|
||||
/// One end of a pty in the global kernel. Dropping closes that end there, so a
|
||||
@@ -330,6 +356,10 @@ impl crate::vfs::FileHandle for RemotePty {
|
||||
(self.remote.readable)(self.id, self.m()) != 0
|
||||
}
|
||||
|
||||
fn wait_queue(&self) -> u32 {
|
||||
(self.remote.queue)(self.id)
|
||||
}
|
||||
|
||||
fn available(&self) -> Option<u64> {
|
||||
Some((self.remote.available)(self.id, self.m()).max(0) as u64)
|
||||
}
|
||||
|
||||
@@ -1114,6 +1114,12 @@ pub enum PollReport {
|
||||
/// `struct epoll_event` packed in 12 bytes (x86-64's, and the i386
|
||||
/// ABI's in any build), rather than 16.
|
||||
packed: bool,
|
||||
/// The set, whose wait queue the waiter registers on (it is chained
|
||||
/// onto every watched descriptor's), and its interest list's
|
||||
/// generation when the descriptors were collected: a change means
|
||||
/// the list in hand is out of date and the call must be made again.
|
||||
set: Arc<unix::EpollSet>,
|
||||
generation: u32,
|
||||
},
|
||||
/// `select`'s three `fd_set`s. A zero address means the caller did not
|
||||
/// pass that set.
|
||||
@@ -1192,7 +1198,7 @@ pub fn poll_finish(fds: &[PollFd], report: &PollReport, m: &mut GuestMem<'_>) ->
|
||||
}
|
||||
Ok(ready)
|
||||
}
|
||||
PollReport::Epoll { out, max, data, packed } => {
|
||||
PollReport::Epoll { out, max, data, packed, .. } => {
|
||||
// Only the ready ones, each with the caller's own token, which is
|
||||
// the whole difference between this and `poll`.
|
||||
let mut n = 0u64;
|
||||
@@ -2432,6 +2438,7 @@ impl Kernel {
|
||||
// happened to suit it.
|
||||
nr::EPOLL_PWAIT | nr::EPOLL_PWAIT2 => {
|
||||
let set = self.epoll_of(a[0] as i32).ok()?;
|
||||
let generation = set.generation();
|
||||
let fds = self.epoll_pollfds(&set);
|
||||
if poll_ready(&fds) {
|
||||
return None;
|
||||
@@ -2470,6 +2477,8 @@ impl Kernel {
|
||||
max: a[2] as u32,
|
||||
data: set.watches().iter().map(|w| (w.fd, w.data)).collect(),
|
||||
packed: gex_abi::X86 || self.compat,
|
||||
set: Arc::clone(&set),
|
||||
generation,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -5505,6 +5514,17 @@ impl Kernel {
|
||||
self.itimers.iter().any(|t| t.is_some())
|
||||
}
|
||||
|
||||
/// How long until the next interval timer fires, if one is armed: a
|
||||
/// parked thread's host sleeps no longer than that, since the timer's
|
||||
/// signal may be what ends its wait.
|
||||
pub fn timer_in_ns(&self) -> Option<u64> {
|
||||
if self.itimers.iter().all(|t| t.is_none()) {
|
||||
return None;
|
||||
}
|
||||
let now = self.now_nanos();
|
||||
self.itimers.iter().flatten().map(|t| t.next.saturating_sub(now).min(u64::MAX as u128) as u64).min()
|
||||
}
|
||||
|
||||
/// Whether an interval timer has expired and has yet to raise its signal.
|
||||
/// The host asks this on its way past: a process that never traps into
|
||||
/// the kernel (a shell's `timeout` spinning on `wait4`/`sigsuspend`) would
|
||||
@@ -6333,10 +6353,12 @@ impl Kernel {
|
||||
};
|
||||
// Watching something that is not open is an error, and a program that
|
||||
// gets silence instead waits for a readiness that cannot arrive.
|
||||
if op != unix::epoll_op::DEL {
|
||||
self.fds.get(fd)?;
|
||||
}
|
||||
set.ctl(op, fd, events, data)
|
||||
let source = if op != unix::epoll_op::DEL {
|
||||
self.fds.get(fd)?.handle.wait_queue()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
set.ctl_queue(op, fd, events, data, source)
|
||||
}
|
||||
|
||||
/// Note that `[addr, addr + len)` no longer holds the code it did.
|
||||
@@ -6383,6 +6405,7 @@ impl Kernel {
|
||||
.iter()
|
||||
.filter_map(|w| {
|
||||
let file = self.fds.get(w.fd).ok()?;
|
||||
set.rechain(w.fd, file.handle.wait_queue());
|
||||
Some(PollFd {
|
||||
addr: 0,
|
||||
fd: w.fd,
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
//! time-based -- [`poll_readable`] asks the clock -- so the handle carries its
|
||||
//! own [`ClockFn`].
|
||||
//!
|
||||
//! Not wired: a *blocking* read does not wake precisely at the deadline; the
|
||||
//! supported path is `TFD_NONBLOCK` plus `epoll`, where the loop's own
|
||||
//! `epoll_wait` timeout carries the timing.
|
||||
//! A waiter parks until the next expiry ([`ready_in_ns`]); arming the timer
|
||||
//! notifies its wait queue, so a waiter parked to an old expiry takes the new.
|
||||
//!
|
||||
//! [`poll_readable`]: crate::vfs::FileHandle::poll_readable
|
||||
//! [`ready_in_ns`]: crate::vfs::FileHandle::ready_in_ns
|
||||
//! [`ClockFn`]: crate::syscalls::ClockFn
|
||||
|
||||
use gex_abi::GuestEndian;
|
||||
@@ -40,6 +40,9 @@ pub struct TimerFd {
|
||||
/// read from `clock_gettime` on the same clock.
|
||||
map: Option<fn((u64, u32)) -> (u64, u32)>,
|
||||
state: Mutex<State>,
|
||||
/// Who waits on it (`crate::wait`): arming or disarming it. Its expiry
|
||||
/// is a deadline the waiter parks to ([`FileHandle::ready_in_ns`]).
|
||||
queue: crate::wait::Queue,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TimerFd {
|
||||
@@ -54,6 +57,7 @@ impl TimerFd {
|
||||
clock,
|
||||
map: None,
|
||||
state: Mutex::new(State::default()),
|
||||
queue: crate::wait::Queue::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +102,9 @@ impl TimerFd {
|
||||
s.next = if abstime { value } else { now.saturating_add(value) };
|
||||
s.interval = interval;
|
||||
}
|
||||
drop(s);
|
||||
// A waiter parked to the old expiry must take the new one.
|
||||
self.queue.notify();
|
||||
prev
|
||||
}
|
||||
|
||||
@@ -170,6 +177,16 @@ impl FileHandle for TimerFd {
|
||||
Self::expirations_at(&self.state.lock().unwrap(), now) > 0
|
||||
}
|
||||
|
||||
fn wait_queue(&self) -> u32 {
|
||||
self.queue.id()
|
||||
}
|
||||
|
||||
fn ready_in_ns(&self) -> Option<u64> {
|
||||
let now = self.now_ns();
|
||||
let s = self.state.lock().unwrap();
|
||||
(s.next != 0).then(|| s.next.saturating_sub(now))
|
||||
}
|
||||
|
||||
fn available(&self) -> Option<u64> {
|
||||
Some(if self.poll_readable() { 8 } else { 0 })
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::errno::{Errno, SysResult};
|
||||
use crate::file::OpenFile;
|
||||
use crate::vfs::{FileHandle, FileType, Metadata};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// How much a stream socket buffers before a write blocks. Sized so a slow
|
||||
@@ -40,9 +40,22 @@ struct Stream {
|
||||
#[derive(Debug, Default)]
|
||||
struct Pipe {
|
||||
state: Mutex<Stream>,
|
||||
/// The wait queues (`crate::wait`) of the end that reads this direction
|
||||
/// and of the end that writes it: bytes arriving or the writer going
|
||||
/// wakes the one, room freed or the reader going wakes the other.
|
||||
reader_q: AtomicU32,
|
||||
writer_q: AtomicU32,
|
||||
}
|
||||
|
||||
impl Pipe {
|
||||
fn notify_reader(&self) {
|
||||
crate::wait::notify(self.reader_q.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
fn notify_writer(&self) {
|
||||
crate::wait::notify(self.writer_q.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
fn send(&self, buf: &[u8], fds: Vec<Arc<OpenFile>>) -> SysResult {
|
||||
let mut s = self.state.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if s.closed {
|
||||
@@ -62,6 +75,10 @@ impl Pipe {
|
||||
}
|
||||
s.bytes.extend(&buf[..n]);
|
||||
s.written += n as u64;
|
||||
drop(s);
|
||||
if n > 0 {
|
||||
self.notify_reader();
|
||||
}
|
||||
Ok(n as u64)
|
||||
}
|
||||
|
||||
@@ -97,6 +114,9 @@ impl Pipe {
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
drop(s);
|
||||
// Room, for a writer parked on a full buffer.
|
||||
self.notify_writer();
|
||||
Ok((n as u64, out))
|
||||
}
|
||||
|
||||
@@ -116,6 +136,8 @@ impl Pipe {
|
||||
|
||||
fn close(&self) {
|
||||
self.state.lock().unwrap_or_else(|e| e.into_inner()).closed = true;
|
||||
self.notify_reader();
|
||||
self.notify_writer();
|
||||
}
|
||||
|
||||
fn is_closed(&self) -> bool {
|
||||
@@ -157,6 +179,9 @@ pub struct UnixSocket {
|
||||
pending_send: Mutex<Vec<Arc<OpenFile>>>,
|
||||
/// Descriptors the last `recvmsg` took off the stream.
|
||||
pending_recv: Mutex<Vec<Arc<OpenFile>>>,
|
||||
/// Who waits on this end (`crate::wait`): readers, writers, and for a
|
||||
/// listener `accept`.
|
||||
queue: crate::wait::Queue,
|
||||
}
|
||||
|
||||
impl UnixSocket {
|
||||
@@ -167,6 +192,42 @@ impl UnixSocket {
|
||||
stream,
|
||||
pending_send: Mutex::new(Vec::new()),
|
||||
pending_recv: Mutex::new(Vec::new()),
|
||||
queue: crate::wait::Queue::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// This end's wait queue.
|
||||
pub fn queue_id(&self) -> u32 {
|
||||
self.queue.id()
|
||||
}
|
||||
|
||||
/// Point a connection's two directions at this end's queue: it reads
|
||||
/// `rx` and writes `tx`.
|
||||
fn adopt(&self, rx: &Pipe, tx: &Pipe) {
|
||||
rx.reader_q.store(self.queue.id(), Ordering::SeqCst);
|
||||
tx.writer_q.store(self.queue.id(), Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// The queue of the end connected to this one (which reads what this one
|
||||
/// writes), or 0.
|
||||
pub fn peer_queue(&self) -> u32 {
|
||||
match &*self.role.lock().unwrap() {
|
||||
Role::Connected { tx, .. } => tx.reader_q.load(Ordering::SeqCst),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A host-serviced end (`HostListener`): what its peer writes is moved by
|
||||
/// the host on a turn of one of the peer's own waiters (the browser's
|
||||
/// compositor pump), so the peer's writes wake the peer's waiters rather
|
||||
/// than this end's, which nobody waits on.
|
||||
pub fn serviced_by_peer(&self) {
|
||||
if let Role::Connected { rx, tx, .. } = &*self.role.lock().unwrap() {
|
||||
let peer = tx.reader_q.load(Ordering::SeqCst);
|
||||
rx.reader_q.store(peer, Ordering::SeqCst);
|
||||
// And room the peer makes by reading is for the host to fill on
|
||||
// the peer's next turn: the peer's waiters, again.
|
||||
tx.writer_q.store(peer, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,12 +237,14 @@ impl UnixSocket {
|
||||
let a2b = Arc::new(Pipe::default());
|
||||
let b2a = Arc::new(Pipe::default());
|
||||
let a = UnixSocket::new(true);
|
||||
a.adopt(&b2a, &a2b);
|
||||
*a.role.lock().unwrap() = Role::Connected {
|
||||
rx: Arc::clone(&b2a),
|
||||
tx: Arc::clone(&a2b),
|
||||
peer: None,
|
||||
};
|
||||
let b = UnixSocket::new(true);
|
||||
b.adopt(&a2b, &b2a);
|
||||
*b.role.lock().unwrap() = Role::Connected {
|
||||
rx: a2b,
|
||||
tx: b2a,
|
||||
@@ -247,6 +310,8 @@ impl UnixSocket {
|
||||
};
|
||||
backlog.lock().unwrap().push_back(theirs);
|
||||
}
|
||||
// A connection is waiting for `accept`.
|
||||
listener.queue.notify();
|
||||
// Take the pair's plumbing rather than the socket itself, because the
|
||||
// descriptor the caller holds is this one.
|
||||
let mut role = self.role.lock().unwrap();
|
||||
@@ -254,6 +319,7 @@ impl UnixSocket {
|
||||
let Role::Connected { rx, tx, .. } = taken else {
|
||||
unreachable!("pair() makes connected sockets")
|
||||
};
|
||||
self.adopt(&rx, &tx);
|
||||
*role = Role::Connected {
|
||||
rx,
|
||||
tx,
|
||||
@@ -425,6 +491,10 @@ impl FileHandle for UnixSocket {
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_queue(&self) -> u32 {
|
||||
self.queue.id()
|
||||
}
|
||||
|
||||
fn poll_writable(&self) -> bool {
|
||||
match &*self.role.lock().unwrap() {
|
||||
Role::Connected { tx, .. } => tx.writable(),
|
||||
@@ -510,13 +580,13 @@ impl SocketNames {
|
||||
let (mine, theirs) = UnixSocket::pair();
|
||||
let taken = std::mem::replace(&mut *mine.role.lock().unwrap(), Role::Fresh);
|
||||
*client.role.lock().unwrap() = match taken {
|
||||
Role::Connected { rx, tx, .. } => Role::Connected {
|
||||
rx,
|
||||
tx,
|
||||
peer: Some(path.to_string()),
|
||||
},
|
||||
Role::Connected { rx, tx, .. } => {
|
||||
client.adopt(&rx, &tx);
|
||||
Role::Connected { rx, tx, peer: Some(path.to_string()) }
|
||||
}
|
||||
_ => unreachable!("pair() makes connected sockets"),
|
||||
};
|
||||
theirs.serviced_by_peer();
|
||||
host.accept(theirs);
|
||||
return Ok(0);
|
||||
}
|
||||
@@ -979,9 +1049,18 @@ mod lock_tests {
|
||||
/// An `epoll` set: the descriptors a program watches and what it wants told
|
||||
/// about each. `epoll_wait` turns the registrations back into the list `poll`
|
||||
/// already knows how to answer.
|
||||
///
|
||||
/// The set keeps its own wait queue, chained onto the queue of every
|
||||
/// descriptor it watches (`crate::wait::chain`): a change on a watched
|
||||
/// descriptor notifies the set, which notifies the threads in `epoll_wait` on
|
||||
/// it -- and only them. A change to the interest list itself notifies the set
|
||||
/// too and moves [`EpollSet::generation`], so a waiter knows its list of
|
||||
/// descriptors is out of date.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EpollSet {
|
||||
watched: Mutex<Vec<Watch>>,
|
||||
queue: crate::wait::Queue,
|
||||
generation: AtomicU32,
|
||||
}
|
||||
|
||||
/// One registration.
|
||||
@@ -991,6 +1070,8 @@ pub struct Watch {
|
||||
pub events: u32,
|
||||
/// Opaque word handed back to the caller (usually a pointer to its state).
|
||||
pub data: u64,
|
||||
/// The watched descriptor's wait queue this set is chained onto, 0 none.
|
||||
pub source: u32,
|
||||
}
|
||||
|
||||
/// `epoll_ctl`'s operations.
|
||||
@@ -1006,25 +1087,41 @@ impl EpollSet {
|
||||
}
|
||||
|
||||
pub fn ctl(&self, op: u64, fd: i32, events: u32, data: u64) -> SysResult {
|
||||
self.ctl_queue(op, fd, events, data, 0)
|
||||
}
|
||||
|
||||
/// `epoll_ctl`, with the watched descriptor's wait queue (`source`) to
|
||||
/// chain this set onto.
|
||||
pub fn ctl_queue(&self, op: u64, fd: i32, events: u32, data: u64, source: u32) -> SysResult {
|
||||
let mut w = self.watched.lock().unwrap_or_else(|e| e.into_inner());
|
||||
match op {
|
||||
let me = self.queue.id();
|
||||
let r = match op {
|
||||
epoll_op::ADD => {
|
||||
if w.iter().any(|x| x.fd == fd) {
|
||||
return Err(Errno::EEXIST);
|
||||
}
|
||||
w.push(Watch { fd, events, data });
|
||||
crate::wait::chain(source, me);
|
||||
w.push(Watch { fd, events, data, source });
|
||||
Ok(0)
|
||||
}
|
||||
epoll_op::MOD => match w.iter_mut().find(|x| x.fd == fd) {
|
||||
Some(x) => {
|
||||
x.events = events;
|
||||
x.data = data;
|
||||
if source != 0 && x.source != source {
|
||||
crate::wait::unchain(x.source, me);
|
||||
crate::wait::chain(source, me);
|
||||
x.source = source;
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
None => Err(Errno::ENOENT),
|
||||
},
|
||||
epoll_op::DEL => {
|
||||
let before = w.len();
|
||||
for x in w.iter().filter(|x| x.fd == fd) {
|
||||
crate::wait::unchain(x.source, me);
|
||||
}
|
||||
w.retain(|x| x.fd != fd);
|
||||
if w.len() == before {
|
||||
return Err(Errno::ENOENT);
|
||||
@@ -1032,6 +1129,24 @@ impl EpollSet {
|
||||
Ok(0)
|
||||
}
|
||||
_ => Err(Errno::EINVAL),
|
||||
};
|
||||
drop(w);
|
||||
self.generation.fetch_add(1, Ordering::SeqCst);
|
||||
self.queue.notify();
|
||||
r
|
||||
}
|
||||
|
||||
/// Make sure this set is chained onto `source`, the queue descriptor
|
||||
/// `fd` has now (a descriptor closed and its number reused names a new
|
||||
/// object). Called as a waiter collects the set's descriptors.
|
||||
pub fn rechain(&self, fd: i32, source: u32) {
|
||||
let mut w = self.watched.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(x) = w.iter_mut().find(|x| x.fd == fd) {
|
||||
if x.source != source {
|
||||
crate::wait::unchain(x.source, self.queue.id());
|
||||
crate::wait::chain(source, self.queue.id());
|
||||
x.source = source;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1041,6 +1156,25 @@ impl EpollSet {
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The set's own wait queue.
|
||||
pub fn queue_id(&self) -> u32 {
|
||||
self.queue.id()
|
||||
}
|
||||
|
||||
/// Moves on every change to the interest list.
|
||||
pub fn generation(&self) -> u32 {
|
||||
self.generation.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EpollSet {
|
||||
fn drop(&mut self) {
|
||||
let me = self.queue.id();
|
||||
for x in self.watched.get_mut().unwrap_or_else(|e| e.into_inner()).iter() {
|
||||
crate::wait::unchain(x.source, me);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileHandle for EpollSet {
|
||||
@@ -1048,6 +1182,10 @@ impl FileHandle for EpollSet {
|
||||
Err(Errno::EINVAL)
|
||||
}
|
||||
|
||||
fn wait_queue(&self) -> u32 {
|
||||
self.queue.id()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Result<Metadata, Errno> {
|
||||
Ok(Metadata {
|
||||
file_type: FileType::Regular,
|
||||
@@ -1138,6 +1276,8 @@ pub struct Remote {
|
||||
pub describe: fn(&crate::syscalls::Kernel, &Arc<OpenFile>) -> Option<String>,
|
||||
/// The other half of [`Remote::describe`], in the receiving process.
|
||||
pub rebuild: fn(&mut crate::syscalls::Kernel, &str) -> Option<Arc<OpenFile>>,
|
||||
/// The end's wait queue (`crate::wait`).
|
||||
pub queue: extern "C" fn(id: i32) -> u32,
|
||||
}
|
||||
|
||||
/// One end of a socket in the global kernel.
|
||||
@@ -1325,6 +1465,10 @@ impl crate::vfs::FileHandle for RemoteUnix {
|
||||
self.listening || (self.remote.writable)(self.id) != 0
|
||||
}
|
||||
|
||||
fn wait_queue(&self) -> u32 {
|
||||
(self.remote.queue)(self.id)
|
||||
}
|
||||
|
||||
// Without these a peer's close showed in `poll` and `epoll` only as
|
||||
// readable: no `POLLHUP`, no `EPOLLRDHUP`.
|
||||
fn poll_hup(&self) -> bool {
|
||||
|
||||
@@ -227,6 +227,21 @@ pub trait FileHandle: Send + Sync + std::fmt::Debug {
|
||||
false
|
||||
}
|
||||
|
||||
/// The wait queue a change in this handle's readiness is announced on
|
||||
/// ([`crate::wait`]): a thread waiting for it registers its wait slot
|
||||
/// there. 0 for a handle whose readiness no event changes (a file is
|
||||
/// always ready; a timer becomes ready by the clock, [`Self::ready_in_ns`]).
|
||||
fn wait_queue(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
/// How long until this handle becomes ready by the clock alone (a
|
||||
/// timerfd's next expiry, a sound device draining its buffer), for a
|
||||
/// waiter to bound its park by. `None` when only an event makes it ready.
|
||||
fn ready_in_ns(&self) -> Option<u64> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether the peer has shut its sending side, so reading ends at EOF
|
||||
/// once the buffer is empty: `POLLRDHUP` / `EPOLLRDHUP`, reported only
|
||||
/// when asked for.
|
||||
|
||||
@@ -202,6 +202,22 @@ impl FileHandle for OssHandle {
|
||||
false
|
||||
}
|
||||
|
||||
/// A full buffer drains by the clock, which no notify reports: a writer
|
||||
/// waiting for room is ready when one byte has played.
|
||||
fn ready_in_ns(&self) -> Option<u64> {
|
||||
if self.node != OssNode::Dsp {
|
||||
return None;
|
||||
}
|
||||
let mut s = self.stream.lock().unwrap_or_else(|e| e.into_inner());
|
||||
s.drain();
|
||||
if s.queued < BUFSIZE {
|
||||
// Room now: a writer is ready, and a reader never is.
|
||||
return None;
|
||||
}
|
||||
let bps = s.bytes_per_second().max(1);
|
||||
Some((1_000_000_000 / bps).max(1))
|
||||
}
|
||||
|
||||
fn ioctl(&self, m: &mut crate::GuestMem<'_>, request: u64, arg: u64) -> Option<SysResult> {
|
||||
let request = request as u32;
|
||||
// An int argument read and written back.
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//! Wait queues (docs/kernel-locking.md §5.2 step 2): Linux's wait-queue model.
|
||||
//!
|
||||
//! Every waitable object -- a pipe, a socket, an eventfd, an epoll set, the
|
||||
//! terminal, a process's children -- owns a [`Queue`]. A thread that waits
|
||||
//! registers its own wait slot on the queue of every object it waits for,
|
||||
//! checks readiness, and parks on its slot; whoever changes the object's
|
||||
//! readiness calls [`Queue::notify`], which wakes the slots registered on
|
||||
//! that queue and nothing else. A thread parked on a pipe is not woken by a
|
||||
//! syscall in some unrelated process, as it was by the one global wake word.
|
||||
//!
|
||||
//! The queues themselves live with the host, because a waiter and a notifier
|
||||
//! can be in different processes (different memories): in the page they are
|
||||
//! in the global kernel module's shared memory (`shims/kernel`, `k_wq_*`),
|
||||
//! which every Worker reaches. This crate only names them, by a number, and
|
||||
//! asks the host through [`Hooks`]. A host with no hooks (native, the unit
|
||||
//! tests) gets queue 0 for everything and every call is a no-op: it wakes
|
||||
//! its waiters its own way (`shims/native/src/ready.rs`).
|
||||
//!
|
||||
//! A queue can also be *chained* onto another ([`chain`]): an epoll set's
|
||||
//! queue is chained onto the queue of every descriptor it watches, so a
|
||||
//! change on a watched descriptor notifies the set, which notifies the
|
||||
//! threads waiting in `epoll_wait` on it.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// What a host supplies.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Hooks {
|
||||
/// A new, empty queue; never 0.
|
||||
pub new: fn() -> u32,
|
||||
/// The object is gone. Its waiters are notified first; the number may be
|
||||
/// reused, and a stale notify to it only wakes someone who re-checks.
|
||||
pub free: fn(u32),
|
||||
/// Wake every slot registered on the queue, and every queue chained to it.
|
||||
pub notify: fn(u32),
|
||||
/// Notify `to` whenever `from` is notified.
|
||||
pub chain: fn(from: u32, to: u32),
|
||||
/// Undo one [`Hooks::chain`].
|
||||
pub unchain: fn(from: u32, to: u32),
|
||||
}
|
||||
|
||||
static HOOKS: OnceLock<Hooks> = OnceLock::new();
|
||||
|
||||
/// Install the host's queues. Once per module instance; later calls are
|
||||
/// ignored (every Worker of a process runs the same setup).
|
||||
pub fn set_hooks(h: Hooks) {
|
||||
let _ = HOOKS.set(h);
|
||||
}
|
||||
|
||||
/// Whether a host has installed queues.
|
||||
pub fn present() -> bool {
|
||||
HOOKS.get().is_some()
|
||||
}
|
||||
|
||||
/// Notify a queue by number. 0 is no queue.
|
||||
pub fn notify(id: u32) {
|
||||
if id != 0 {
|
||||
if let Some(h) = HOOKS.get() {
|
||||
(h.notify)(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Chain `from` onto `to`: notifying `from` notifies `to`.
|
||||
pub fn chain(from: u32, to: u32) {
|
||||
if from != 0 && to != 0 {
|
||||
if let Some(h) = HOOKS.get() {
|
||||
(h.chain)(from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Undo a [`chain`].
|
||||
pub fn unchain(from: u32, to: u32) {
|
||||
if from != 0 && to != 0 {
|
||||
if let Some(h) = HOOKS.get() {
|
||||
(h.unchain)(from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One object's wait queue, freed with the object.
|
||||
#[derive(Debug)]
|
||||
pub struct Queue(u32);
|
||||
|
||||
impl Queue {
|
||||
pub fn new() -> Queue {
|
||||
Queue(HOOKS.get().map_or(0, |h| (h.new)()))
|
||||
}
|
||||
|
||||
/// The queue's number, for a waiter to register on; 0 when the host has
|
||||
/// no queues.
|
||||
pub fn id(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// The object's readiness may have changed: wake whoever waits on it.
|
||||
pub fn notify(&self) {
|
||||
notify(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Queue {
|
||||
fn default() -> Queue {
|
||||
Queue::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Queue {
|
||||
fn drop(&mut self) {
|
||||
if self.0 != 0 {
|
||||
if let Some(h) = HOOKS.get() {
|
||||
(h.free)(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn without_hooks_everything_is_queue_zero() {
|
||||
// The unit tests install no hooks, as natively.
|
||||
let q = Queue::new();
|
||||
assert_eq!(q.id(), 0);
|
||||
q.notify();
|
||||
notify(0);
|
||||
chain(0, 1);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Does a futex wake reach a Worker parked on that futex, well before the
|
||||
* park's bound?
|
||||
*
|
||||
* The page's Workers park on a futex with the kernel module's
|
||||
* `k_park_futex(addr, gen, ns)`: a wait on that futex's generation slot,
|
||||
* bounded by `ns`. `k_futex_wake(addr)` bumps the slot and must notify it.
|
||||
* When it notified only the global wake word, every futex park slept out its
|
||||
* whole bound (20 ms in W3's `gex_park`), and Undertale in the page lost a
|
||||
* third of its frame rate once nothing else cut those parks short.
|
||||
*
|
||||
* This instantiates the kernel module in two threads on shared memory. The
|
||||
* second thread parks on a futex for 5 s; the first wakes it after 100 ms.
|
||||
* The park must end, woken (0), within 1 s. A parked waiter on another
|
||||
* address must not be woken by it unless the two share a slot.
|
||||
*
|
||||
* Usage: node check-futex-wake.mjs [gex_kernel.wasm]
|
||||
*/
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Worker, isMainThread, workerData, parentPort } from 'node:worker_threads';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..', '..');
|
||||
|
||||
async function instantiate(module, memory, process_) {
|
||||
const inst = await WebAssembly.instantiate(module, {
|
||||
env: { memory, process: process_, lazy_fetch: () => 0 },
|
||||
});
|
||||
return inst.exports;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
const path = process.argv[2] || join(ROOT, 'web', 'public', 'gex', 'gex_kernel.wasm');
|
||||
const module = await WebAssembly.compile(await readFile(path));
|
||||
const memory = new WebAssembly.Memory({ initial: 2048, maximum: 65536, shared: true });
|
||||
const process_ = new WebAssembly.Memory({ initial: 1, maximum: 65535, shared: true });
|
||||
const k = await instantiate(module, memory, process_);
|
||||
const ADDR = 0x1234_5670n;
|
||||
const OTHER = 0x0765_4320n;
|
||||
const park = (addr, ms) => new Promise((resolve, reject) => {
|
||||
const w = new Worker(fileURLToPath(import.meta.url), {
|
||||
workerData: { module, memory, process_, addr, gen: k.k_futex_gen(addr), ns: ms * 1e6 },
|
||||
});
|
||||
w.once('message', resolve);
|
||||
w.once('error', reject);
|
||||
});
|
||||
// The kernel's `futex_slot` (16,384 slots): two addresses may share one.
|
||||
const slot = (addr) => {
|
||||
const a = addr >> 2n;
|
||||
return Number((BigInt.asUintN(64, (a ^ (a >> 15n)) * 0x9E3779B97F4A7C15n) >> 40n) & 16383n);
|
||||
};
|
||||
const sameSlot = slot(ADDR) === slot(OTHER);
|
||||
const woken = park(ADDR, 5000);
|
||||
const other = park(OTHER, 600);
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
k.k_futex_wake(ADDR);
|
||||
const [a, b] = await Promise.all([woken, other]);
|
||||
const fail = [];
|
||||
if (a.r !== 0 || a.ms > 1000) fail.push(`the woken park returned ${a.r} after ${a.ms.toFixed(0)} ms (want 0, well under its 5000 ms bound)`);
|
||||
if (!sameSlot && (b.r !== 2 || b.ms < 500)) fail.push(`a park on another futex returned ${b.r} after ${b.ms.toFixed(0)} ms (want its 600 ms timeout, 2)`);
|
||||
if (fail.length) {
|
||||
for (const f of fail) console.error(` ${f}`);
|
||||
console.error(`futex wake check FAILED: ${path}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(` a futex wake ends a parked Worker's wait (after ${a.ms.toFixed(0)} ms; the wake came 100 ms after the Worker started); another futex's waiter sleeps on`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
const { module, memory, process_, addr, gen, ns } = workerData;
|
||||
const k = await instantiate(module, memory, process_);
|
||||
const t0 = performance.now();
|
||||
const r = k.k_park_futex(addr, gen, BigInt(ns));
|
||||
parentPort.postMessage({ r, ms: performance.now() - t0 });
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Do the wait queues wake exactly the waiters registered on them, at once?
|
||||
* (docs/kernel-locking.md §5.2 step 2.)
|
||||
*
|
||||
* Each thread owns one wait slot (`k_ws_*`); a wait registers the slot on the
|
||||
* queue of every object it waits for (`k_wq_add`) and parks on it
|
||||
* (`k_ws_park`); whoever changes an object notifies its queue. This
|
||||
* instantiates the kernel module in several threads on shared memory and
|
||||
* parks them the way a guest's Worker does:
|
||||
*
|
||||
* - a poll over two pipes is woken by a write to the second, within
|
||||
* `WITHIN_MS` (1 ms on a quiet box, which is how it is timed: edger or
|
||||
* worldmachine, never a build box; 20 ms by default, which no bounded
|
||||
* re-check could meet by luck against the 5 s park);
|
||||
* - a waiter on a third pipe sleeps on through both writes (its own 600 ms
|
||||
* timeout, 2): one object's change wakes nobody else;
|
||||
* - a futex wake (`k_futex_wake`) ends a park on that futex's bucket queue
|
||||
* (`k_futex_queue`);
|
||||
* - a queue chained onto another (an epoll set onto a descriptor) is
|
||||
* notified with it.
|
||||
*
|
||||
* Usage: node check-wait-queues.mjs [gex_kernel.wasm]
|
||||
*/
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Worker, isMainThread, workerData, parentPort } from 'node:worker_threads';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..', '..');
|
||||
|
||||
async function instantiate(module, memory, process_) {
|
||||
const inst = await WebAssembly.instantiate(module, {
|
||||
env: { memory, process: process_, lazy_fetch: () => 0 },
|
||||
});
|
||||
return inst.exports;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
const path = process.argv[2] || join(ROOT, 'web', 'public', 'gex', 'gex_kernel.wasm');
|
||||
const module = await WebAssembly.compile(await readFile(path));
|
||||
const memory = new WebAssembly.Memory({ initial: 2048, maximum: 65536, shared: true });
|
||||
const process_ = new WebAssembly.Memory({ initial: 1, maximum: 65535, shared: true });
|
||||
const k = await instantiate(module, memory, process_);
|
||||
const PID = 1;
|
||||
const pipe = () => {
|
||||
const id = k.k_pipe_new(PID);
|
||||
k.k_pipe_open_end(id, 0, PID);
|
||||
k.k_pipe_open_end(id, 1, PID);
|
||||
return id;
|
||||
};
|
||||
const [p1, p2, p3] = [pipe(), pipe(), pipe()];
|
||||
const ADDR = 0x1234_5670n;
|
||||
const chained = k.k_wq_new();
|
||||
const set = k.k_wq_new();
|
||||
k.k_wq_chain(chained, set);
|
||||
// Each waiter: its own slot, registered on its queues, parked for `ms`.
|
||||
// `ready` resolves once its thread is about to park.
|
||||
const ready = [];
|
||||
const park = (queues, ms) => {
|
||||
const slot = k.k_ws_alloc();
|
||||
for (const q of queues) k.k_wq_add(q, slot);
|
||||
const seq = k.k_ws_seq(slot);
|
||||
let up;
|
||||
ready.push(new Promise((r) => { up = r; }));
|
||||
return new Promise((resolve, reject) => {
|
||||
const w = new Worker(fileURLToPath(import.meta.url), {
|
||||
workerData: { module, memory, process_, slot, seq, ns: ms * 1e6 },
|
||||
});
|
||||
w.on('message', (m) => {
|
||||
if (m === 'ready') return up();
|
||||
for (const q of queues) k.k_wq_del(q, slot);
|
||||
k.k_ws_free(slot);
|
||||
resolve(m);
|
||||
});
|
||||
w.once('error', reject);
|
||||
});
|
||||
};
|
||||
const notifies0 = k.k_ws_notifies();
|
||||
const poll = park([k.k_pipe_queue(p1), k.k_pipe_queue(p2)], 5000);
|
||||
const other = park([k.k_pipe_queue(p3)], 600);
|
||||
const futex = park([k.k_futex_queue(ADDR)], 5000);
|
||||
const epoll = park([set], 5000);
|
||||
await Promise.all(ready);
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
// A byte into the second pipe (from the process memory's first word).
|
||||
const t = performance.timeOrigin + performance.now();
|
||||
try { k.k_pipe_write(p2, 0, 1); } catch { k.k_pipe_write(p2, 0n, 1); } // PAddr: u32 or u64
|
||||
k.k_futex_wake(ADDR);
|
||||
k.k_wq_notify(chained);
|
||||
const [a, b, c, d] = await Promise.all([poll, other, futex, epoll]);
|
||||
const late = a.end - t;
|
||||
const WITHIN_MS = Number(process.env.WITHIN_MS ?? 20);
|
||||
const fail = [];
|
||||
if (a.r !== 0 || late > WITHIN_MS) fail.push(`the poll over two pipes returned ${a.r}, ${late.toFixed(2)} ms after the write to the second (want 0, within ${WITHIN_MS} ms)`);
|
||||
if (b.r !== 2 || b.ms < 500) fail.push(`a waiter on another pipe returned ${b.r} after ${b.ms.toFixed(0)} ms (want its 600 ms timeout, 2)`);
|
||||
if (c.r !== 0 || c.ms > 1000) fail.push(`a futex waiter returned ${c.r} after ${c.ms.toFixed(0)} ms (want 0, well under its 5000 ms bound)`);
|
||||
if (d.r !== 0 || d.ms > 1000) fail.push(`a waiter on a chained queue returned ${d.r} after ${d.ms.toFixed(0)} ms (want 0)`);
|
||||
const live = k.k_wq_live();
|
||||
if (fail.length) {
|
||||
for (const f of fail) console.error(` ${f}`);
|
||||
console.error(`wait queue check FAILED: ${path}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(` a write wakes the poll over two pipes ${late.toFixed(2)} ms later; a waiter on a third pipe slept its 600 ms out; a futex wake and a chained queue wake theirs (${k.k_ws_notifies() - notifies0} slot wakes, ${live} queues live)`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
const { module, memory, process_, slot, seq, ns } = workerData;
|
||||
const k = await instantiate(module, memory, process_);
|
||||
parentPort.postMessage('ready');
|
||||
const t0 = performance.now();
|
||||
const r = k.k_ws_park(slot, seq, BigInt(ns));
|
||||
// `performance.now()` counts from each thread's own origin.
|
||||
parentPort.postMessage({ r, ms: performance.now() - t0, end: performance.timeOrigin + performance.now() });
|
||||
}
|
||||
@@ -5,13 +5,13 @@
|
||||
* A Wayland client's input goes page -> compositor -> the process's ring in
|
||||
* the wl arena (`sendEvent`) -> the shim's compositor pump, which runs on a
|
||||
* scheduler turn of one of the process's Workers. With every Worker parked on
|
||||
* the global kernel's wake word (`gex_park`, 20 ms bound), nothing drained the
|
||||
* ring until a bound ran out: up to 20 ms on every click and key, and the
|
||||
* park-late diag found Wine's Wayland reader thread waiting out its bound.
|
||||
* `sendEvent` now bumps the wake word the process's `WlProxy` names.
|
||||
* its wait slot (`gex_park`), nothing drains the ring unless `sendEvent`
|
||||
* notifies the process's wait queue, which the process's `WlProxy` names and
|
||||
* every waiting thread of the process is registered on.
|
||||
*
|
||||
* This instantiates the kernel module in a second thread on shared memory and
|
||||
* parks it on the wake word for 5 s (`k_park`, as `gex_park` does); the first
|
||||
* parks it on a slot registered on a queue for 5 s (`k_ws_park`, as
|
||||
* `gex_park` does); the first
|
||||
* thread claims a block as a guest would, then sends a pointer event 100 ms
|
||||
* later. The park must end, woken (0), within a few milliseconds of the event.
|
||||
*
|
||||
@@ -43,7 +43,10 @@ if (isMainThread) {
|
||||
// The guest's half: a process claims its block (on its first publish).
|
||||
const channel = makeWlChannel();
|
||||
const proxy = new WlProxy(channel, () => {}, () => 7);
|
||||
proxy.wakeAddr = () => k.k_wake_addr();
|
||||
const queue = k.k_wq_new();
|
||||
const slot = k.k_ws_alloc();
|
||||
k.k_wq_add(queue, slot);
|
||||
proxy.wakeAddr = () => ({ addr: k.k_wq_addr(queue), slots: k.k_ws_addr(0) });
|
||||
const guestMem = new WebAssembly.Memory({ initial: 1, maximum: 16, shared: true });
|
||||
proxy.publish(guestMem, 0, 0);
|
||||
if (!proxy.ok) {
|
||||
@@ -55,10 +58,10 @@ if (isMainThread) {
|
||||
const compositorChannel = { arena: channel.arena };
|
||||
setKernelMemory(compositorChannel, memory);
|
||||
|
||||
const seq = k.k_wake_seq();
|
||||
const seq = k.k_ws_seq(slot);
|
||||
const parked = new Promise((resolve, reject) => {
|
||||
const w = new Worker(fileURLToPath(import.meta.url), {
|
||||
workerData: { module, memory, process_, seq, ns: 5000 * 1e6 },
|
||||
workerData: { module, memory, process_, slot, seq, ns: 5000 * 1e6 },
|
||||
});
|
||||
w.once('message', resolve);
|
||||
w.once('error', reject);
|
||||
@@ -77,9 +80,9 @@ if (isMainThread) {
|
||||
console.log(` page input wakes a process whose threads are all parked (${late.toFixed(1)} ms after the event; the park's bound was 5 s)`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
const { module, memory, process_, seq, ns } = workerData;
|
||||
const { module, memory, process_, slot, seq, ns } = workerData;
|
||||
const k = await instantiate(module, memory, process_);
|
||||
const r = k.k_park(seq, BigInt(ns));
|
||||
const r = k.k_ws_park(slot, seq, BigInt(ns));
|
||||
// `performance.now()` counts from each thread's own origin.
|
||||
parentPort.postMessage({ r, end: performance.timeOrigin + performance.now() });
|
||||
}
|
||||
+36
-31
@@ -852,18 +852,18 @@ export class Gex {
|
||||
#kernelWords = null;
|
||||
|
||||
/** What ends a network wait, sampled before the guest is given another
|
||||
* chance (`gex_wake`): the global kernel's wake counter and the page
|
||||
* network's event count. */
|
||||
* chance (`gex_wake`): the page network's event count. (This Worker's wait
|
||||
* slot is sampled by the shim itself, at the start of each scheduler turn:
|
||||
* `gex_wait_seq`.) */
|
||||
#netSnap() {
|
||||
const k = this.#kernel;
|
||||
return { k: k?.k_wake_addr ? k.k_wake_seq() : null, n: this.#net?.activitySeq?.() ?? null };
|
||||
return { n: this.#net?.activitySeq?.() ?? null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep, without blocking this Worker's event loop, until the network has
|
||||
* something (this Worker's own network's `onActivity`, or the page's
|
||||
* through the bridge's activity word), another Worker bumps the global
|
||||
* kernel's wake counter (`Atomics.waitAsync` on it), or the waiting
|
||||
* through the bridge's activity word), something the thread waits for is
|
||||
* notified (`Atomics.waitAsync` on this Worker's wait slot), or the waiting
|
||||
* thread's own deadline is due (`gex_wait_bound_ms`, at most 50 ms). It
|
||||
* replaces a MessageChannel turn loop that spun every network-waiting
|
||||
* Worker at full speed, each turn taking the host lock four times (node's
|
||||
@@ -871,11 +871,11 @@ export class Gex {
|
||||
*/
|
||||
#netWait(e, snap) {
|
||||
const ms = e.gex_wait_bound_ms ? e.gex_wait_bound_ms(50) : 1;
|
||||
const k = this.#kernel;
|
||||
const waits = [new Promise((r) => { this.#netWake = r; })];
|
||||
if (k?.k_wake_addr && snap.k !== null) {
|
||||
const word = e.gex_wait_word?.() >>> 0;
|
||||
if (word) {
|
||||
this.#kernelWords ??= new Int32Array(this.#kernelSpec.memory.buffer);
|
||||
const w = Atomics.waitAsync(this.#kernelWords, k.k_wake_addr() >>> 2, snap.k, ms);
|
||||
const w = Atomics.waitAsync(this.#kernelWords, word >>> 2, e.gex_wait_seq(), ms);
|
||||
if (!w.async) return Promise.resolve();
|
||||
waits.push(w.value);
|
||||
} else {
|
||||
@@ -1016,12 +1016,14 @@ export class Gex {
|
||||
|
||||
get memory() { return this.#memory; }
|
||||
|
||||
/** The global kernel's memory and its wake word's address, or null: what
|
||||
* a compositor bumps to wake this process's parked Workers. */
|
||||
/** The global kernel's memory, this process's wait queue's address in it
|
||||
* and where the wait slots' words start, or null: what a compositor walks
|
||||
* to wake this process's parked threads after an input event
|
||||
* (`wl-bridge.mjs` `sendEvent`). */
|
||||
kernelWake() {
|
||||
const k = this.#kernel;
|
||||
if (!k?.k_wake_addr || !this.#kernelSpec?.memory) return null;
|
||||
return { memory: this.#kernelSpec.memory, addr: k.k_wake_addr() };
|
||||
const e = this.#shim?.exports;
|
||||
if (!this.#kernel || !e?.gex_proc_queue_addr || !this.#kernelSpec?.memory) return null;
|
||||
return { memory: this.#kernelSpec.memory, addr: e.gex_proc_queue_addr() >>> 0, slots: e.gex_slots_addr() >>> 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2170,7 +2172,10 @@ export class Gex {
|
||||
'k_sock_bind', 'k_sock_connect', 'k_sock_accept', 'k_sock_pending',
|
||||
'k_sock_read', 'k_sock_write', 'k_sock_readable', 'k_sock_writable', 'k_sock_hup',
|
||||
'k_sock_available', 'k_sock_close', 'k_sock_shutdown', 'k_sock_exists',
|
||||
'k_wake_seq', 'k_wake', 'k_park',
|
||||
'k_ws_self', 'k_ws_seq', 'k_ws_park', 'k_ws_addr',
|
||||
'k_wq_new', 'k_wq_free', 'k_wq_notify', 'k_wq_chain', 'k_wq_unchain',
|
||||
'k_wq_add', 'k_wq_del', 'k_wq_addr',
|
||||
'k_pipe_queue', 'k_sock_queue', 'k_pty_queue', 'k_proc_queue', 'k_stdin_queue', 'k_futex_queue',
|
||||
// Which dispatch slots an identity's code is in, system-wide rather than
|
||||
// per Worker. Without a kernel these answer zero: one process has no
|
||||
// sibling to share with, and the shim keeps its own map.
|
||||
@@ -2214,25 +2219,25 @@ export class Gex {
|
||||
throw new Error('gex (single-thread build): a blocking call inside a nested guest call '
|
||||
+ `(${this.#nest.join(' > ')}) cannot suspend`);
|
||||
};
|
||||
out.k_park = new WebAssembly.Suspending((seq, ns) => {
|
||||
if (k.k_wake_seq() !== seq) return 1;
|
||||
// A wait slot's word is in the kernel's (plain) memory, which can
|
||||
// grow: the view is asked for each time.
|
||||
const words = () => new Int32Array(this.#kernelSpec.memory.buffer);
|
||||
out.k_ws_park = new WebAssembly.Suspending((slot, seq, ns) => {
|
||||
if (k.k_ws_seq(slot) !== seq) return 1;
|
||||
if (this.#hardNested()) nested();
|
||||
this.#where = `park(${ns})`;
|
||||
return sched.park(ns);
|
||||
return sched.parkSlot(words, (k.k_ws_addr(slot) >>> 0) >>> 2, seq, ns);
|
||||
});
|
||||
out.k_park_futex = new WebAssembly.Suspending((addr, gen, ns) => {
|
||||
if ((k.k_futex_gen(addr) >>> 0) !== (gen >>> 0)) return 1;
|
||||
if (this.#hardNested()) nested();
|
||||
this.#where = `futex(${addr},${ns})`;
|
||||
return sched.parkFutex(addr, ns);
|
||||
});
|
||||
out.k_wake = () => {
|
||||
k.k_wake();
|
||||
sched.wake();
|
||||
// A notify moved slot words nobody `Atomics.wait`s on here; the fibers
|
||||
// parked on them look (and every park and yield looks too, for the
|
||||
// notifies inside the kernel's own calls).
|
||||
out.k_wq_notify = (id) => {
|
||||
k.k_wq_notify(id);
|
||||
sched.checkSlots();
|
||||
};
|
||||
out.k_futex_wake = (addr) => {
|
||||
k.k_futex_wake(addr);
|
||||
sched.wakeFutex(addr);
|
||||
sched.checkSlots();
|
||||
};
|
||||
}
|
||||
return out;
|
||||
@@ -4492,7 +4497,7 @@ export class Gex {
|
||||
// case, so it is the condition for turning.
|
||||
const onLoop = !!e.gex_paused_for_host();
|
||||
this.#netTurns = onLoop ? this.#netTurns + 1 : 0;
|
||||
if (onLoop && e.gex_wait_bound_ms && this.#kernel?.k_wake_addr) {
|
||||
if (onLoop && e.gex_wait_bound_ms && e.gex_wait_word?.()) {
|
||||
// Asleep until the network, another Worker or the clock.
|
||||
this.waits.net++;
|
||||
await this.#netWait(e, snap);
|
||||
@@ -4920,7 +4925,7 @@ export class Gex {
|
||||
// --- The single-thread build (single.mjs, docs/single-thread.md) ---------
|
||||
//
|
||||
// Every guest thread is a fiber of one realm, its guest on a JSPI stack. A
|
||||
// blocking call suspends that stack (`k_park`, below) where the default
|
||||
// blocking call suspends that stack (`k_ws_park`, below) where the default
|
||||
// build waits in `Atomics.wait`, so wherever JS calls into wasm that may
|
||||
// block it calls through `WebAssembly.promising`, and wherever wasm calls
|
||||
// JS that calls back into wasm that may block, that JS is a
|
||||
@@ -4936,7 +4941,7 @@ export class Gex {
|
||||
return this.#enterPf(ctx, pc);
|
||||
}
|
||||
#parkPf = null;
|
||||
/** `gex_park`, on a stack of its own (it suspends in `k_park`). */
|
||||
/** `gex_park`, on a stack of its own (it suspends in `k_ws_park`). */
|
||||
#parkP(ms) {
|
||||
this.#parkPf ??= WebAssembly.promising(this.#shim.exports.gex_park);
|
||||
return this.#parkPf(ms);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// It holds a process memory of its own only because the kernel imports one and
|
||||
// `k_pipe_write` reads bytes from there; it is a staging buffer, nothing more.
|
||||
import { lazyFetch } from './lazy-fetch.mjs';
|
||||
import { single, sched } from './single.mjs';
|
||||
let k = null;
|
||||
let stage = null; // Uint8Array over this Worker's dummy process memory
|
||||
const STAGE_AT = 0x1000;
|
||||
@@ -40,8 +41,7 @@ self.onmessage = async (e) => {
|
||||
}
|
||||
if (!k) return;
|
||||
if (msg?.kind === 'write') {
|
||||
// Into the kernel, then a wake: whoever is parked on the other end is woken
|
||||
// by the same counter another guest's write would bump.
|
||||
// Into the kernel, whose pipe notifies whoever waits on its other end.
|
||||
const bytes = msg.bytes;
|
||||
let off = 0;
|
||||
while (off < bytes.length) {
|
||||
@@ -51,13 +51,13 @@ self.onmessage = async (e) => {
|
||||
if (wrote <= 0) break;
|
||||
off += wrote;
|
||||
}
|
||||
k.k_wake();
|
||||
if (single.on) sched.checkSlots();
|
||||
post({ kind: 'wrote', id: msg.id, n: off });
|
||||
return;
|
||||
}
|
||||
if (msg?.kind === 'close') {
|
||||
k.k_pipe_close_end(msg.pipe, 1);
|
||||
k.k_wake();
|
||||
if (single.on) sched.checkSlots();
|
||||
return;
|
||||
}
|
||||
};
|
||||
+54
-22
@@ -179,10 +179,15 @@ const SLICE_MS = 8;
|
||||
class Waiter {
|
||||
constructor(resolve, futex, fiber) {
|
||||
this.resolve = resolve;
|
||||
this.futex = futex; // a BigInt address, or null for the global word
|
||||
this.futex = futex; // a BigInt address, null for the global word, or a slot (below)
|
||||
this.fiber = fiber;
|
||||
this.timer = 0;
|
||||
this.done = false;
|
||||
/** A wait slot's word (`k_ws_park`): a function giving the kernel
|
||||
* memory's current words (it can grow), the index, the value parked at. */
|
||||
this.words = null;
|
||||
this.at = 0;
|
||||
this.seq = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +196,11 @@ export class Sched {
|
||||
#global = new Set();
|
||||
/** Fibers parked on a futex, by address. */
|
||||
#futex = new Map();
|
||||
/** Fibers parked on their wait slot (`k_ws_park`, docs/kernel-locking.md
|
||||
* §5.2 step 2). A notify bumps the slot's word in the kernel's (plain)
|
||||
* memory and its `memory.atomic.notify` wakes nobody, so whoever may have
|
||||
* notified asks `checkSlots` -- and every park and yield does. */
|
||||
#slots = new Set();
|
||||
/** Resolutions held back for a macrotask (see `#resume`). */
|
||||
#held = [];
|
||||
/** Woken waiters not yet resumed: they run when the running fiber parks,
|
||||
@@ -260,7 +270,8 @@ export class Sched {
|
||||
if (w.done) return;
|
||||
w.done = true;
|
||||
if (w.timer) clearTimeout(w.timer);
|
||||
if (w.futex === null) this.#global.delete(w);
|
||||
if (w.words) this.#slots.delete(w);
|
||||
else if (w.futex === null) this.#global.delete(w);
|
||||
else {
|
||||
const s = this.#futex.get(w.futex);
|
||||
s?.delete(w);
|
||||
@@ -287,52 +298,71 @@ export class Sched {
|
||||
for (const f of ready) this.#resume(f);
|
||||
}
|
||||
|
||||
#park(futex, ns, fiber) {
|
||||
#park(futex, ns, fiber, slot = null) {
|
||||
// Whatever the running fiber notified before it parks.
|
||||
this.checkSlots();
|
||||
return new Promise((resolve) => {
|
||||
const w = new Waiter(resolve, futex, fiber);
|
||||
if (futex === null) this.#global.add(w);
|
||||
if (slot) {
|
||||
w.words = slot.words;
|
||||
w.at = slot.at;
|
||||
w.seq = slot.seq;
|
||||
this.#slots.add(w);
|
||||
} else if (futex === null) this.#global.add(w);
|
||||
else {
|
||||
let s = this.#futex.get(futex);
|
||||
if (!s) this.#futex.set(futex, (s = new Set()));
|
||||
s.add(w);
|
||||
}
|
||||
// `ns` bounds every wait, as it bounds `memory.atomic.wait32`.
|
||||
const ms = Math.max(0, Number(ns) / 1e6);
|
||||
if (Number.isFinite(ms) && ms < 2 ** 31) {
|
||||
// `ns` bounds a wait, as it bounds `memory.atomic.wait32`; a negative
|
||||
// one waits for ever.
|
||||
const n = Number(ns);
|
||||
const ms = Math.max(0, n / 1e6);
|
||||
if (n >= 0 && Number.isFinite(ms) && ms < 2 ** 31) {
|
||||
w.timer = setTimeout(() => { this.stats.timeouts++; this.#settle(w, 2); }, ms);
|
||||
}
|
||||
this.#flush();
|
||||
});
|
||||
}
|
||||
|
||||
/** Sleep until the wait slot word `words()[at]` leaves `seq`, or `ns` pass
|
||||
* (negative: for ever): 0 woken, 1 it had already moved, 2 timed out. */
|
||||
parkSlot(words, at, seq, ns, fiber = null) {
|
||||
if (Atomics.load(words(), at) !== seq) return 1;
|
||||
this.stats.slotParks = (this.stats.slotParks ?? 0) + 1;
|
||||
return this.#park(null, ns, fiber, { words, at, seq });
|
||||
}
|
||||
|
||||
/** A wait slot may have been notified (a kernel call, the compositor's
|
||||
* input): every fiber whose slot moved re-checks. */
|
||||
checkSlots() {
|
||||
if (!this.#slots.size) return;
|
||||
for (const w of [...this.#slots]) {
|
||||
if (Atomics.load(w.words(), w.at) !== w.seq) this.#settle(w, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/** Sleep until the global counter is bumped, or `ns` pass: 0 woken, 2 timed out. */
|
||||
park(ns, fiber = null) {
|
||||
this.stats.parks++;
|
||||
return this.#park(null, ns, fiber);
|
||||
}
|
||||
|
||||
/** Sleep until futex `addr` is woken, or `ns` pass. */
|
||||
parkFutex(addr, ns, fiber = null) {
|
||||
this.stats.futexParks++;
|
||||
return this.#park(BigInt.asUintN(64, BigInt(addr)), ns, fiber);
|
||||
}
|
||||
|
||||
/** The global counter moved: everyone parked on it re-checks. */
|
||||
/** Something of the page's own happened that this realm has no queue for
|
||||
* -- the network's bytes, which the one Wisp connection has no way to say
|
||||
* are whose: every parked fiber re-checks. The single-thread build only;
|
||||
* the default build's network waits turn their own Worker's loop. */
|
||||
wake() {
|
||||
if (!this.#global.size) return;
|
||||
if (!this.#global.size && !this.#slots.size) return;
|
||||
this.stats.wakes++;
|
||||
for (const w of [...this.#global]) this.#settle(w, 0);
|
||||
}
|
||||
|
||||
/** A futex was woken (and with it the global counter). */
|
||||
wakeFutex(addr) {
|
||||
const s = this.#futex.get(BigInt.asUintN(64, BigInt(addr)));
|
||||
if (s) for (const w of [...s]) this.#settle(w, 0);
|
||||
this.wake();
|
||||
for (const w of [...this.#slots]) this.#settle(w, 0);
|
||||
}
|
||||
|
||||
/** Forget a fiber's waits (its Worker was terminated). */
|
||||
drop(fiber) {
|
||||
for (const w of [...this.#slots]) if (w.fiber === fiber) { w.done = true; clearTimeout(w.timer); this.#slots.delete(w); }
|
||||
for (const w of [...this.#global]) if (w.fiber === fiber) { w.done = true; clearTimeout(w.timer); this.#global.delete(w); }
|
||||
for (const [k, s] of this.#futex) {
|
||||
for (const w of [...s]) if (w.fiber === fiber) { w.done = true; clearTimeout(w.timer); s.delete(w); }
|
||||
@@ -346,6 +376,7 @@ export class Sched {
|
||||
* loop when it has run out.
|
||||
*/
|
||||
maybeYield() {
|
||||
this.checkSlots();
|
||||
if (!this.sliceOver()) return 0;
|
||||
this.stats.yields++;
|
||||
return new Promise((resolve) => {
|
||||
@@ -360,6 +391,7 @@ export class Sched {
|
||||
* just woke would wait out the slice.
|
||||
*/
|
||||
yieldNow() {
|
||||
this.checkSlots();
|
||||
if (!this.#ready.length) return this.maybeYield();
|
||||
this.stats.yields++;
|
||||
this.#flush();
|
||||
@@ -368,7 +400,7 @@ export class Sched {
|
||||
|
||||
/** How many fibers are parked, for a census. */
|
||||
get parked() {
|
||||
let n = this.#global.size;
|
||||
let n = this.#global.size + this.#slots.size;
|
||||
for (const s of this.#futex.values()) n += s.size;
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
import { lazyFetch } from './lazy-fetch.mjs';
|
||||
import { single, sched } from './single.mjs';
|
||||
|
||||
/** The single-thread build's fibers park on the scheduler, not on the
|
||||
* kernel's wake word: whatever this file changes in the kernel wakes them. */
|
||||
const poke = () => { if (single.on) sched.wake(); };
|
||||
/** The single-thread build's fibers park on the scheduler, not on their
|
||||
* wait slots' words: whatever this file changes in the kernel, they look. */
|
||||
const poke = () => { if (single.on) sched.checkSlots(); };
|
||||
|
||||
/// Where the page writes bytes for the kernel to copy out of. The kernel
|
||||
/// imports the calling process's memory and addresses it with `memory.copy`;
|
||||
@@ -166,7 +166,6 @@ export class PageTty {
|
||||
/** There will be no more input -- the page has gone, or `^D` at the end. */
|
||||
eof() {
|
||||
this.#k.k_stdin_eof();
|
||||
this.#k.k_wake?.();
|
||||
poke();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
// and is the only writer of it, plus a ring the compositor writes input into.
|
||||
// No locks -- one writer per word, and the compositor polls at frame rate.
|
||||
|
||||
import { single, sched } from './single.mjs';
|
||||
|
||||
/** Processes that can publish surfaces at once. */
|
||||
const BLOCKS = 8;
|
||||
/** Surfaces one process can have on screen. */
|
||||
@@ -99,9 +101,12 @@ const B_HEARTBEAT = 1;
|
||||
const B_COUNT = 2;
|
||||
const B_GEN = 3;
|
||||
const B_ACK = 4;
|
||||
/** Where the global kernel's wake word is (`k_wake_addr`), or zero: what the
|
||||
* compositor bumps after putting an event in this block's ring. */
|
||||
/** Where the process's wait queue is in the global kernel's memory
|
||||
* (`gex_proc_queue_addr`), or zero, and where the wait slots' words start
|
||||
* (`gex_slots_addr`): what the compositor notifies after putting an event in
|
||||
* this block's ring (docs/kernel-locking.md §5.2 step 2). */
|
||||
const B_WAKE = 5;
|
||||
const B_SLOTS = 6;
|
||||
const B_WORDS = 8;
|
||||
const BLOCK_WORDS = B_WORDS + SLOTS * REC;
|
||||
|
||||
@@ -241,16 +246,40 @@ export function sendEvent(channel, block, kind, a1 = 0, b1 = 0, c1 = 0, d1 = 0,
|
||||
a[slot + 6] = surface | 0;
|
||||
a[slot + 7] = 0;
|
||||
Atomics.store(a, base + K_HEAD, head + 1); // publish last
|
||||
// And a wake, as another guest's syscall would give: with every thread of
|
||||
// the process parked, nothing drains the ring until a park's bound (20 ms)
|
||||
// runs out. The Worker that wakes takes a scheduler turn, whose compositor
|
||||
// pump delivers the event and wakes the client's reader in turn.
|
||||
const wake = Atomics.load(a, blockBase(block) + B_WAKE);
|
||||
// And a notify on the process's wait queue: with every thread of the
|
||||
// process parked, nothing drains the ring otherwise. The thread that wakes
|
||||
// takes a scheduler turn, whose compositor pump delivers the event and
|
||||
// notifies the client's reader in turn.
|
||||
const q = Atomics.load(a, blockBase(block) + B_WAKE);
|
||||
const slots = Atomics.load(a, blockBase(block) + B_SLOTS);
|
||||
const k = channel.kernelMemory;
|
||||
if (wake && k && typeof SharedArrayBuffer === 'function' && k.buffer instanceof SharedArrayBuffer) {
|
||||
const w = new Int32Array(k.buffer);
|
||||
Atomics.add(w, wake >>> 2, 1);
|
||||
Atomics.notify(w, wake >>> 2);
|
||||
if (q && slots && k) {
|
||||
notifyQueue(new Int32Array(k.buffer), q >>> 0, slots >>> 0);
|
||||
// The single-thread build: the fibers parked on those words look.
|
||||
if (single.on) sched.checkSlots();
|
||||
}
|
||||
}
|
||||
|
||||
/** Queue words: a bitmap of the slots registered on it, 8 words
|
||||
* (shims/kernel/src/wait.rs `Queue`). */
|
||||
const Q_BITS = 8;
|
||||
|
||||
/**
|
||||
* Notify the wait queue at byte address `q` in the kernel's memory `w`, as
|
||||
* `k_wq_notify` does: bump and wake the word of every slot registered on it,
|
||||
* the slots' words starting at `slots`. Not the queues chained to it -- a
|
||||
* process queue has none.
|
||||
*/
|
||||
export function notifyQueue(w, q, slots) {
|
||||
for (let i = 0; i < Q_BITS; i++) {
|
||||
let bits = Atomics.load(w, (q >>> 2) + i) >>> 0;
|
||||
while (bits) {
|
||||
const b = 31 - Math.clz32(bits & -bits);
|
||||
bits &= bits - 1;
|
||||
const at = (slots >>> 2) + i * 32 + b;
|
||||
Atomics.add(w, at, 1);
|
||||
if (w.buffer instanceof SharedArrayBuffer) Atomics.notify(w, at);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +313,8 @@ export class WlProxy {
|
||||
#tokenFn;
|
||||
/** Called with the token once it is known, so the host can send the memory. */
|
||||
onToken = null;
|
||||
/** The global kernel's wake word address (`k_wake_addr`), or 0; asked when
|
||||
/** This process's wait queue's address in the global kernel's memory and
|
||||
* where the slot words start, as `{ addr, slots }`, or null; asked when
|
||||
* the block is claimed. See `B_WAKE`. */
|
||||
wakeAddr = null;
|
||||
|
||||
@@ -340,8 +370,11 @@ export class WlProxy {
|
||||
}
|
||||
|
||||
#noteWake() {
|
||||
const at = this.wakeAddr?.() | 0;
|
||||
if (at) Atomics.store(this.#a, this.#base + B_WAKE, at);
|
||||
const w = this.wakeAddr?.();
|
||||
if (w?.addr && w?.slots) {
|
||||
Atomics.store(this.#a, this.#base + B_SLOTS, w.slots | 0);
|
||||
Atomics.store(this.#a, this.#base + B_WAKE, w.addr | 0);
|
||||
}
|
||||
}
|
||||
|
||||
get token() {
|
||||
|
||||
@@ -491,7 +491,7 @@ async function handle(msg) {
|
||||
port.post({ kind: 'wl-memory', token, memory: gex.memory, kernel: gex.kernelWake()?.memory });
|
||||
};
|
||||
proxy.onToken = gex.onWlToken;
|
||||
proxy.wakeAddr = () => gex.kernelWake()?.addr ?? 0;
|
||||
proxy.wakeAddr = () => gex.kernelWake();
|
||||
} else if (msg.wlStub) {
|
||||
// A headless compositor for node: the shim's gex-wayland runs and binds
|
||||
// `wayland-0`, so a guest connects and inits exactly as in the browser, but
|
||||
|
||||
+260
-413
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,380 @@
|
||||
//! Waiting, the Linux way (docs/kernel-locking.md §5.2 step 2).
|
||||
//!
|
||||
//! A Worker runs one guest thread, and the thread owns one *wait slot*: a
|
||||
//! word in the global kernel's memory (`k_ws_*`), found from the Worker's
|
||||
//! kernel stack. When the thread blocks, this registers the slot on the wait
|
||||
//! queue of every object the thread waits for -- the pipe, the socket, each
|
||||
//! descriptor of a `poll`, the epoll set (whose queue is chained onto its
|
||||
//! descriptors'), the futex's bucket -- and on its process's queue, which a
|
||||
//! signal, a child's exit or the process ending notifies. Then it checks
|
||||
//! readiness, and parks on the slot until a notify or its own deadline. A
|
||||
//! write to a pipe wakes the threads registered on that pipe and nobody
|
||||
//! else: there is no global wake word any more, and no syscall wakes every
|
||||
//! parked Worker of every process.
|
||||
//!
|
||||
//! The scheduler's idle turn asks only its own thread, from the snapshot
|
||||
//! taken when the wait began ([`MyWait`]), with no kernel lock: a turn takes
|
||||
//! KLock only to finish the syscall once the wait is over, to pump the
|
||||
//! compositor when it has work, or when a signal may have arrived (the
|
||||
//! signal state is still under KLock, §5.2 step 7).
|
||||
|
||||
use super::*;
|
||||
|
||||
thread_local! {
|
||||
/// This Worker's wait slot, -1 until first asked.
|
||||
static SLOT: Cell<i32> = const { Cell::new(-1) };
|
||||
/// What the slot read at the start of this scheduler turn: the park that
|
||||
/// ends the turn sleeps only while it still reads this, so a notify that
|
||||
/// lands between the readiness check and the park is not slept through.
|
||||
static SEQ: Cell<i32> = const { Cell::new(0) };
|
||||
/// The wait this Worker's thread is in, as it was when it began.
|
||||
static WAIT: RefCell<Option<MyWait>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// This process's wait queue (`k_proc_queue`), with the pid it is for.
|
||||
static PROC_Q: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
/// When this process's next interval timer fires, on `now_ns`'s clock;
|
||||
/// `u64::MAX` for none. Published after every syscall and every locked idle
|
||||
/// turn, for a parked thread's deadline (a timer's signal is what ends a
|
||||
/// `sigsuspend` or interrupts a wait).
|
||||
pub(crate) static TIMER_AT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(u64::MAX);
|
||||
|
||||
/// A wait, as the scheduler turn that found it saw it under the lock.
|
||||
pub(crate) struct MyWait {
|
||||
/// The thread (`cur()`), and what it waits for.
|
||||
thread: usize,
|
||||
waiting: Waiting,
|
||||
blocked_on: Option<gex_kernel::BlockOp>,
|
||||
/// The queues its slot is on.
|
||||
queues: Vec<u32>,
|
||||
/// The idle turn's verdict, which a lock-free turn repeats.
|
||||
pub why: u8,
|
||||
pub all_blocked: bool,
|
||||
pub ends_by_itself: bool,
|
||||
/// The thread's own deadline, and what its descriptors become ready at
|
||||
/// by the clock alone (a timerfd).
|
||||
soonest: Option<u64>,
|
||||
/// `LOCAL_SIGNALS` as the last locked turn saw it.
|
||||
local_sig: u32,
|
||||
}
|
||||
|
||||
/// This Worker's slot. The slot is the Worker's kernel stack's number, so
|
||||
/// it lives exactly as long as the Worker and needs no freeing.
|
||||
pub(crate) fn slot() -> i32 {
|
||||
SLOT.with(|s| {
|
||||
if s.get() < 0 {
|
||||
s.set(unsafe { k_ws_self() });
|
||||
}
|
||||
s.get()
|
||||
})
|
||||
}
|
||||
|
||||
/// This process's wait queue.
|
||||
pub(crate) fn proc_queue() -> u32 {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
let pid = my_pid();
|
||||
let v = PROC_Q.load(Relaxed);
|
||||
if pid != 0 && (v >> 32) as i32 == pid {
|
||||
return v as u32;
|
||||
}
|
||||
let q = unsafe { k_proc_queue(pid) };
|
||||
PROC_Q.store(((pid as u32 as u64) << 32) | q as u64, Relaxed);
|
||||
q
|
||||
}
|
||||
|
||||
/// Wake every waiting thread of this process: it is ending, or one of its
|
||||
/// own threads raised a signal for another (`tgkill`), which only this
|
||||
/// process's kernel knows about.
|
||||
pub(crate) fn wake_process() {
|
||||
if unsafe { k_present() } != 0 {
|
||||
unsafe { k_wq_notify(proc_queue()) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample the slot at the start of a scheduler turn.
|
||||
pub(crate) fn begin_turn() {
|
||||
if unsafe { k_present() } != 0 {
|
||||
SEQ.with(|q| q.set(unsafe { k_ws_seq(slot()) }));
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish when this process's next timer fires.
|
||||
pub(crate) fn publish_timer(h: &Host) {
|
||||
let at = h.kernel.timer_in_ns().map_or(u64::MAX, |d| now_ns().saturating_add(d));
|
||||
let was = TIMER_AT.swap(at, std::sync::atomic::Ordering::Relaxed);
|
||||
// Sooner than the threads parked in other Workers sleep to: they look
|
||||
// again (`park_ns` reads it when they park).
|
||||
if at < was {
|
||||
wake_process();
|
||||
}
|
||||
}
|
||||
|
||||
/// The queues a wait registers on, and when it becomes ready by the clock.
|
||||
fn queues_of(waiting: &Waiting, blocked_on: &Option<gex_kernel::BlockOp>, now: u64, page: bool) -> (Vec<u32>, Option<u64>) {
|
||||
let mut qs = vec![proc_queue()];
|
||||
let mut soonest = match waiting {
|
||||
Waiting::Sleep(at) => Some(*at),
|
||||
Waiting::Futex { deadline, .. } | Waiting::Net(deadline) | Waiting::Pi { deadline, .. } => *deadline,
|
||||
_ => None,
|
||||
};
|
||||
let mut handle = |h: &Arc<dyn gex_kernel::vfs::FileHandle>, qs: &mut Vec<u32>| {
|
||||
let q = h.wait_queue();
|
||||
if q == 0 && !page && h.ready_in_ns().is_none() {
|
||||
unqueued(h);
|
||||
}
|
||||
qs.push(q);
|
||||
if let Some(d) = h.ready_in_ns() {
|
||||
let at = now.saturating_add(d);
|
||||
soonest = Some(soonest.map_or(at, |s| s.min(at)));
|
||||
}
|
||||
};
|
||||
match waiting {
|
||||
Waiting::Futex { parked, .. } => qs.push(unsafe { k_futex_queue(parked.addr()) }),
|
||||
Waiting::Pi { addr, .. } => qs.push(unsafe { k_futex_queue(*addr) }),
|
||||
Waiting::Read(h) | Waiting::Write(h) | Waiting::Accept(h) => handle(h, &mut qs),
|
||||
Waiting::Net(_) => {
|
||||
if let Some(gex_kernel::BlockOp::Poll { fds, report, .. }) = blocked_on {
|
||||
match report {
|
||||
// The set's own queue, chained onto every descriptor it
|
||||
// watches; a timerfd in the set still bounds the park.
|
||||
gex_kernel::syscalls::PollReport::Epoll { set, .. } => {
|
||||
qs.push(set.queue_id());
|
||||
for p in fds {
|
||||
if let Some(d) = p.handle.as_ref().and_then(|h| h.ready_in_ns()) {
|
||||
let at = now.saturating_add(d);
|
||||
soonest = Some(soonest.map_or(at, |s| s.min(at)));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for p in fds {
|
||||
if let Some(h) = &p.handle {
|
||||
handle(h, &mut qs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
qs.retain(|&q| q != 0);
|
||||
qs.sort_unstable();
|
||||
qs.dedup();
|
||||
(qs, soonest)
|
||||
}
|
||||
|
||||
/// A wait on a descriptor with no queue and no clock: nothing will notify
|
||||
/// it, so a thread that parks on it sleeps until its deadline. Said once per
|
||||
/// process, by type, so a kind of descriptor that should have a queue is
|
||||
/// found rather than hung on. (A wait the page ends -- a Wisp socket, a
|
||||
/// socket the page serves -- never parks, and says nothing.)
|
||||
fn unqueued(h: &Arc<dyn gex_kernel::vfs::FileHandle>) {
|
||||
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 {}: a wait on a descriptor with no wait queue: {h:?}\n", my_pid()));
|
||||
}
|
||||
}
|
||||
|
||||
/// The locked half of an idle turn: this Worker's thread `i` is waiting and
|
||||
/// not ready. Register its slot for the wait if this is a new one (and say
|
||||
/// so: the caller checks readiness again, now that a change will notify).
|
||||
pub(crate) fn enter(h: &mut Host, i: usize, why: u8, all_blocked: bool, ends_by_itself: bool) -> bool {
|
||||
if unsafe { k_present() } == 0 {
|
||||
return false;
|
||||
}
|
||||
let now = now_ns();
|
||||
publish_timer(h);
|
||||
let local_sig = LOCAL_SIGNALS.load(std::sync::atomic::Ordering::Acquire);
|
||||
let t = &h.threads[i];
|
||||
let fresh = WAIT.with(|w| {
|
||||
let mut w = w.borrow_mut();
|
||||
if let Some(m) = w.as_mut().filter(|m| m.thread == i && same_wait(&m.waiting, &t.waiting)) {
|
||||
m.why = why;
|
||||
m.all_blocked = all_blocked;
|
||||
m.ends_by_itself = ends_by_itself;
|
||||
m.local_sig = local_sig;
|
||||
return false;
|
||||
}
|
||||
if let Some(old) = w.take() {
|
||||
unregister(&old);
|
||||
}
|
||||
let (queues, soonest) = queues_of(&t.waiting, &t.blocked_on, now, why & 4 != 0);
|
||||
let slot = slot() as u32;
|
||||
for &q in &queues {
|
||||
unsafe { k_wq_add(q, slot) };
|
||||
}
|
||||
*w = Some(MyWait {
|
||||
thread: i,
|
||||
waiting: t.waiting.clone(),
|
||||
blocked_on: t.blocked_on.clone(),
|
||||
queues,
|
||||
why,
|
||||
all_blocked,
|
||||
ends_by_itself,
|
||||
soonest,
|
||||
local_sig,
|
||||
});
|
||||
true
|
||||
});
|
||||
fresh
|
||||
}
|
||||
|
||||
/// Whether two waits are the same wait (a thread that blocked again on
|
||||
/// something else must register afresh).
|
||||
fn same_wait(a: &Waiting, b: &Waiting) -> bool {
|
||||
match (a, b) {
|
||||
(Waiting::Futex { parked: p, wake_gen: g, .. }, Waiting::Futex { parked: q, wake_gen: k, .. }) => {
|
||||
p.addr() == q.addr() && g == k
|
||||
}
|
||||
(Waiting::Sleep(x), Waiting::Sleep(y)) => x == y,
|
||||
(Waiting::Read(x), Waiting::Read(y))
|
||||
| (Waiting::Write(x), Waiting::Write(y))
|
||||
| (Waiting::Accept(x), Waiting::Accept(y)) => Arc::ptr_eq(x, y),
|
||||
(Waiting::Net(x), Waiting::Net(y)) => x == y,
|
||||
(Waiting::Pi { addr: a, gen: g, .. }, Waiting::Pi { addr: b, gen: k, .. }) => a == b && g == k,
|
||||
(Waiting::Child, Waiting::Child) | (Waiting::Signal, Waiting::Signal) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn unregister(m: &MyWait) {
|
||||
let slot = slot() as u32;
|
||||
for &q in &m.queues {
|
||||
unsafe { k_wq_del(q, slot) };
|
||||
}
|
||||
}
|
||||
|
||||
/// The wait is over (the thread was picked, or the process is ending).
|
||||
pub(crate) fn leave() {
|
||||
WAIT.with(|w| {
|
||||
if let Some(old) = w.borrow_mut().take() {
|
||||
unregister(&old);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// An idle turn without the kernel lock: `Some((why, ends_by_itself))` when this Worker's
|
||||
/// thread is still in the wait its last locked turn registered and is not
|
||||
/// ready -- `why` the `paused()` bits to set. `None` when the turn must take
|
||||
/// the lock: the wait may be over, a signal may have arrived, the
|
||||
/// compositor has work, or there is no wait in hand.
|
||||
pub(crate) fn idle_turn() -> Option<(u8, bool)> {
|
||||
if unsafe { k_present() } == 0 || EXITED.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return None;
|
||||
}
|
||||
WAIT.with(|w| {
|
||||
let w = w.borrow();
|
||||
let m = w.as_ref()?;
|
||||
if m.thread != cur() || m.all_blocked {
|
||||
return None;
|
||||
}
|
||||
if crate::wl::pump_wanted() {
|
||||
return None;
|
||||
}
|
||||
// A signal for this process, from another (`kill`), from one of its
|
||||
// own threads (`tgkill`), or from its timer: asked under the lock,
|
||||
// where the signal state is.
|
||||
if LOCAL_SIGNALS.load(std::sync::atomic::Ordering::Acquire) != m.local_sig
|
||||
|| unsafe { k_has_signals(my_pid()) } != 0
|
||||
|| TIMER_AT.load(std::sync::atomic::Ordering::Relaxed) <= now_ns()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if wait_ready(&m.waiting, &m.blocked_on, now_ns(), false) {
|
||||
return None;
|
||||
}
|
||||
Some((m.why, m.ends_by_itself))
|
||||
})
|
||||
}
|
||||
|
||||
/// How long this Worker's thread may sleep: until its own deadline, a
|
||||
/// descriptor's clock, or its process's timer; `None` for as long as it
|
||||
/// takes. A deadlock candidate is looked at again (`DEADLOCK_CONFIRM`).
|
||||
pub(crate) fn park_ns(now: u64) -> Option<u64> {
|
||||
let (soonest, all_blocked) = WAIT.with(|w| {
|
||||
w.borrow()
|
||||
.as_ref()
|
||||
.filter(|m| m.thread == cur())
|
||||
.map_or((None, false), |m| (m.soonest, m.all_blocked))
|
||||
});
|
||||
let timer = TIMER_AT.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let timer = (timer != u64::MAX).then_some(timer);
|
||||
let at = match (soonest, timer) {
|
||||
(Some(s), Some(t)) => Some(s.min(t)),
|
||||
(s, t) => s.or(t),
|
||||
};
|
||||
let mut ns = at.map(|at| at.saturating_sub(now).max(50_000));
|
||||
if all_blocked {
|
||||
ns = Some(ns.map_or(DEADLOCK_LOOK_NS, |n| n.min(DEADLOCK_LOOK_NS)));
|
||||
}
|
||||
ns
|
||||
}
|
||||
|
||||
/// How often a process whose every thread is parked on a futex nothing in
|
||||
/// it can wake is looked at again, to confirm the deadlock it reports.
|
||||
const DEADLOCK_LOOK_NS: u64 = 20_000_000;
|
||||
|
||||
/// Park this Worker on its slot: until a notify, or `park_ns`. Returns the
|
||||
/// wait instruction's answer (0 woken, 1 moved already, 2 timed out).
|
||||
pub(crate) fn park() -> i32 {
|
||||
let now = now_ns();
|
||||
let ns = park_ns(now).map_or(-1, |n| n as i64);
|
||||
let seq = SEQ.with(|q| q.get());
|
||||
unsafe { k_ws_park(slot(), seq, ns) }
|
||||
}
|
||||
|
||||
/// Park on the slot for at most `ns`: a wait on the page, from a Worker
|
||||
/// whose frames keep it from turning its event loop (`wasm32_wait`).
|
||||
pub(crate) fn park_bounded(ns: u64) -> i32 {
|
||||
let ns = park_ns(now_ns()).map_or(ns, |n| n.min(ns));
|
||||
let seq = SEQ.with(|q| q.get());
|
||||
unsafe { k_ws_park(slot(), seq, ns as i64) }
|
||||
}
|
||||
|
||||
/// The slot word's address and the value this turn sampled, for a wait the
|
||||
/// page's event loop must keep turning through (`gex.mjs` `#netWait`).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_wait_word() -> u32 {
|
||||
if unsafe { k_present() } == 0 {
|
||||
return 0;
|
||||
}
|
||||
unsafe { k_ws_addr(slot()) }
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_wait_seq() -> i32 {
|
||||
SEQ.with(|q| q.get())
|
||||
}
|
||||
|
||||
/// Where this process's wait queue is in the global kernel's memory, and
|
||||
/// where the slot words start: the page's compositor, which has no kernel
|
||||
/// instance of its own, wakes the process's parked threads after putting an
|
||||
/// input event in its ring by walking the queue's bitmap (`wl-bridge.mjs`
|
||||
/// `sendEvent`). 0 with no global kernel.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_proc_queue_addr() -> u32 {
|
||||
if unsafe { k_present() } == 0 {
|
||||
return 0;
|
||||
}
|
||||
unsafe { k_wq_addr(proc_queue()) }
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_slots_addr() -> u32 {
|
||||
if unsafe { k_present() } == 0 {
|
||||
return 0;
|
||||
}
|
||||
unsafe { k_ws_addr(0) }
|
||||
}
|
||||
|
||||
/// Whether this Worker's thread is in a registered wait, for `wasm32_wait`.
|
||||
pub(crate) fn waiting_here() -> bool {
|
||||
WAIT.with(|w| w.borrow().as_ref().is_some_and(|m| m.thread == cur()))
|
||||
}
|
||||
|
||||
/// Whether the registered wait wants the page (`paused()` bit 4).
|
||||
pub(crate) fn wants_page() -> Option<bool> {
|
||||
WAIT.with(|w| w.borrow().as_ref().filter(|m| m.thread == cur()).map(|m| m.why & 4 != 0))
|
||||
}
|
||||
@@ -218,6 +218,17 @@ fn output_size() -> (i32, i32) {
|
||||
/// because the ioctl arrives on whichever thread the client called it from.
|
||||
static COMPOSITOR: std::sync::Mutex<Option<WaylandDevice>> = std::sync::Mutex::new(None);
|
||||
|
||||
/// Whether this process's compositor has anything to move (`needs_pump`),
|
||||
/// for a scheduler turn that takes no lock (`waitq::idle_turn`): if so, the
|
||||
/// turn takes the lock and pumps. A compositor busy elsewhere is a yes.
|
||||
pub fn pump_wanted() -> bool {
|
||||
let device = match COMPOSITOR.try_lock() {
|
||||
Ok(d) => d.clone(),
|
||||
Err(_) => return true,
|
||||
};
|
||||
device.is_some_and(|d| d.needs_pump())
|
||||
}
|
||||
|
||||
/// Remember it at mount, so the GL device can find it later.
|
||||
pub fn set_compositor(device: &WaylandDevice) {
|
||||
*COMPOSITOR.lock().unwrap_or_else(|e| e.into_inner()) = Some(device.clone());
|
||||
|
||||
+98
-138
@@ -19,6 +19,8 @@ use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use gex_kernel::proc::ProcTable;
|
||||
|
||||
mod wait;
|
||||
|
||||
/// Stacks, one per Worker. Every Worker instantiates this module and shares its
|
||||
/// memory but has its own `__stack_pointer`, all initialised to the same
|
||||
/// address by wasm-ld -- so without a distinct slice each, two Workers here at
|
||||
@@ -51,63 +53,10 @@ pub extern "C" fn k_stack_count() -> u32 {
|
||||
STACK_COUNT
|
||||
}
|
||||
|
||||
/// The word every Worker waits on and bumps, so a blocked guest is woken by a
|
||||
/// wake rather than by JS polling once a millisecond. It lives here because
|
||||
/// `memory.atomic.wait32`/`notify` act on the addressed memory, and only this
|
||||
/// module's is shared across every process; a word in a process's memory is
|
||||
/// visible only to that process's own threads.
|
||||
///
|
||||
/// A counter, not a flag, so a wake between deciding to park and parking is not
|
||||
/// lost: the parker samples the value and sleeps only while it is unchanged.
|
||||
static WAKE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
|
||||
|
||||
fn wake_addr() -> *mut i32 {
|
||||
&WAKE as *const std::sync::atomic::AtomicI32 as *mut i32
|
||||
}
|
||||
|
||||
/// Where the counter is in this module's memory: a Worker that must keep
|
||||
/// its event loop running (a network wait) waits on it with
|
||||
/// `Atomics.waitAsync` instead of polling (gex.mjs `#netWait`).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wake_addr() -> u32 {
|
||||
wake_addr() as u32
|
||||
}
|
||||
|
||||
/// What the counter reads now. Sampled before a Worker decides to park.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wake_seq() -> i32 {
|
||||
WAKE.load(std::sync::atomic::Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// Something happened that another Worker may have been waiting for. Called
|
||||
/// after every syscall, not at hand-picked spots: missing one wake-up strands a
|
||||
/// process, and a notify with nobody waiting costs a few nanoseconds.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wake() {
|
||||
WAKE.fetch_add(1, std::sync::atomic::Ordering::Release);
|
||||
// SAFETY: the address of a static in this module's own shared memory,
|
||||
// which is what this instruction requires.
|
||||
unsafe {
|
||||
core::arch::wasm32::memory_atomic_notify(wake_addr(), u32::MAX);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sleep until the counter leaves `seq`, or `ns` nanoseconds pass. Returns 0
|
||||
/// woken, 1 value already moved, 2 timed out. A bounded wait keeps a Worker
|
||||
/// answerable to the page.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_park(seq: i32, ns: i64) -> i32 {
|
||||
// SAFETY: as above. Blocking is allowed because every guest runs in a
|
||||
// Worker; the page's own thread never enters this module.
|
||||
unsafe { core::arch::wasm32::memory_atomic_wait32(wake_addr(), seq, ns) }
|
||||
}
|
||||
|
||||
/// Per-address futex wake generations, shared across every Worker.
|
||||
///
|
||||
/// The global [`WAKE`] counter says "somebody did something" and makes every
|
||||
/// parked Worker re-check; this says *which futex* was woken, precisely. A
|
||||
/// Worker parking on a futex records the address's generation here, and wakes
|
||||
/// only when it changes -- a real wake ALWAYS bumps it, whatever the futex word
|
||||
/// A Worker parking on a futex records the address's generation here, and is
|
||||
/// ready only when it changes -- a real wake ALWAYS bumps it, whatever the futex word
|
||||
/// does afterward, so this has none of the ambiguity of guessing from the word
|
||||
/// (which a heavily-contended condvar counter can leave back at the waited-for
|
||||
/// value before the sleeper is scheduled, stranding a whole thread pool). It is
|
||||
@@ -128,26 +77,44 @@ fn futex_slot(addr: u64) -> usize {
|
||||
(((a ^ (a >> 15)).wrapping_mul(0x9E37_79B9_7F4A_7C15)) >> 40) as usize & (FUTEX_SLOTS - 1)
|
||||
}
|
||||
|
||||
/// A futex at `addr` was woken: bump its generation and wake every parked
|
||||
/// Worker to re-check. Called by the browser host for every wake its per-Worker
|
||||
/// `FutexTable` recorded -- a `FUTEX_WAKE`, a thread exit clearing its tid, any
|
||||
/// of them.
|
||||
/// The wait queue of the futex bucket `addr` hashes to, made on first use: a
|
||||
/// thread parked on a futex registers its wait slot here
|
||||
/// (docs/kernel-locking.md §5.2 step 2), and a wake notifies it. Fewer
|
||||
/// buckets than generation slots; a shared bucket only wakes a waiter that
|
||||
/// then finds its own generation unmoved and parks again.
|
||||
const FUTEX_QUEUES: usize = 4096;
|
||||
static FUTEX_QS: [std::sync::atomic::AtomicU32; FUTEX_QUEUES] =
|
||||
[const { std::sync::atomic::AtomicU32::new(0) }; FUTEX_QUEUES];
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_futex_queue(addr: u64) -> u32 {
|
||||
use std::sync::atomic::Ordering::SeqCst;
|
||||
note_panics();
|
||||
let at = &FUTEX_QS[futex_slot(addr) & (FUTEX_QUEUES - 1)];
|
||||
let q = at.load(SeqCst);
|
||||
if q != 0 {
|
||||
return q;
|
||||
}
|
||||
let fresh = wait::new_queue();
|
||||
match at.compare_exchange(0, fresh, SeqCst, SeqCst) {
|
||||
Ok(_) => fresh,
|
||||
Err(theirs) => {
|
||||
wait::free_queue(fresh);
|
||||
theirs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A futex at `addr` was woken: bump its generation and notify the threads
|
||||
/// waiting on its bucket's queue. Called by the browser host for every wake
|
||||
/// its per-Worker `FutexTable` recorded -- a `FUTEX_WAKE`, a thread exit
|
||||
/// clearing its tid, any of them.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_futex_wake(addr: u64) {
|
||||
let slot = &FUTEX_GENS[futex_slot(addr)];
|
||||
slot.fetch_add(1, std::sync::atomic::Ordering::Release);
|
||||
// The Workers parked on this futex wait on its slot (`k_park_futex`), not
|
||||
// on `WAKE`: without this notify they slept out their whole bound (20 ms
|
||||
// in W3's `gex_park`) on every wake. That was Undertale's page regression
|
||||
// once d761d4d4 stopped other threads' deadlines from cutting it short.
|
||||
// SAFETY: the address of a static in this module's own shared memory.
|
||||
unsafe {
|
||||
core::arch::wasm32::memory_atomic_notify(
|
||||
slot as *const std::sync::atomic::AtomicU32 as *mut i32,
|
||||
u32::MAX,
|
||||
);
|
||||
}
|
||||
k_wake();
|
||||
FUTEX_GENS[futex_slot(addr)].fetch_add(1, std::sync::atomic::Ordering::Release);
|
||||
// The threads parked on this futex's bucket, and nobody else.
|
||||
let q = FUTEX_QS[futex_slot(addr) & (FUTEX_QUEUES - 1)].load(std::sync::atomic::Ordering::SeqCst);
|
||||
wait::notify(q);
|
||||
}
|
||||
|
||||
/// The generation of the futex at `addr`, sampled when a Worker parks on it and
|
||||
@@ -157,28 +124,6 @@ pub extern "C" fn k_futex_gen(addr: u64) -> u32 {
|
||||
FUTEX_GENS[futex_slot(addr)].load(std::sync::atomic::Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// Sleep until the futex at `addr` is woken (its generation leaves `gen`), or
|
||||
/// `ns` pass. Returns 0 woken, 1 the generation already moved, 2 timed out.
|
||||
///
|
||||
/// Unlike [`k_park`], which sleeps on the one global [`WAKE`] counter that every
|
||||
/// syscall in every process bumps, this sleeps on just this futex's generation
|
||||
/// slot -- so an idle condvar waiter (a GLib thread-pool worker, say) is not
|
||||
/// woken by the blanket notify a busy sibling fires on every syscall. That
|
||||
/// thundering herd made a multi-threaded GTK program crawl: N idle threads each
|
||||
/// took the host lock to re-check on every one of another thread's syscalls. A
|
||||
/// `FUTEX_WAKE` to this address (via `k_futex_wake`) still bumps the slot and
|
||||
/// wakes it precisely; the caller passes a bounded `ns` so signals, deadlines
|
||||
/// and page work -- which do not touch this slot -- are still answered in time.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_park_futex(addr: u64, gen: u32, ns: i64) -> i32 {
|
||||
let slot = futex_slot(addr);
|
||||
let at = &FUTEX_GENS[slot] as *const std::sync::atomic::AtomicU32 as *mut i32;
|
||||
// SAFETY: the address of a static in this module's own shared memory, which
|
||||
// is what the wait instruction requires. `gen` is compared as the same bit
|
||||
// pattern the generation was stored with (a wrapping u32 counter).
|
||||
unsafe { core::arch::wasm32::memory_atomic_wait32(at, gen as i32, ns) }
|
||||
}
|
||||
|
||||
/// What this module last panicked with. It has no console (it imports nothing
|
||||
/// but two memories), so without this a panic unwound to a bare `unreachable`
|
||||
/// with no message. The host reads it straight out of shared memory; see
|
||||
@@ -190,6 +135,7 @@ static PANIC: std::sync::Mutex<String> = std::sync::Mutex::new(String::new());
|
||||
fn note_panics() {
|
||||
static ONCE: std::sync::Once = std::sync::Once::new();
|
||||
ONCE.call_once(|| {
|
||||
wait::install();
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
let mut slot = PANIC.lock().unwrap_or_else(|e| e.into_inner());
|
||||
*slot = format!("{info}");
|
||||
@@ -269,9 +215,9 @@ pub extern "C" fn k_slot_len() -> u32 {
|
||||
slots().lock().unwrap_or_else(|e| e.into_inner()).len() as u32
|
||||
}
|
||||
|
||||
/// Standard input, shared. Whoever has the keystroke writes it and calls
|
||||
/// [`k_wake`], so a parked reader is woken by the same counter a pipe write
|
||||
/// bumps -- rather than by a `postMessage` a Worker parked inside wasm cannot
|
||||
/// Standard input, shared. Whoever has the keystroke writes it, which
|
||||
/// notifies its queue ([`stdin_queue`]), so a parked reader is woken --
|
||||
/// rather than by a `postMessage` a Worker parked inside wasm cannot
|
||||
/// receive.
|
||||
fn stdin() -> &'static Mutex<(std::collections::VecDeque<u8>, bool)> {
|
||||
note_panics();
|
||||
@@ -279,6 +225,20 @@ fn stdin() -> &'static Mutex<(std::collections::VecDeque<u8>, bool)> {
|
||||
Q.get_or_init(|| Mutex::new((std::collections::VecDeque::new(), false)))
|
||||
}
|
||||
|
||||
/// The wait queue of standard input and the shared terminal: a reader of
|
||||
/// either registers here, and typing, end of input or a change of the
|
||||
/// foreground group notifies it.
|
||||
fn stdin_queue() -> &'static gex_kernel::wait::Queue {
|
||||
note_panics();
|
||||
static Q: OnceLock<gex_kernel::wait::Queue> = OnceLock::new();
|
||||
Q.get_or_init(gex_kernel::wait::Queue::new)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_stdin_queue() -> u32 {
|
||||
stdin_queue().id()
|
||||
}
|
||||
|
||||
/// Append what was typed, from the caller's process memory.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_stdin_write(at: gex_kmem::PAddr, len: u32) -> i32 {
|
||||
@@ -286,8 +246,8 @@ pub extern "C" fn k_stdin_write(at: gex_kmem::PAddr, len: u32) -> i32 {
|
||||
// SAFETY: the same copy every pipe write makes, and `pull_checked`
|
||||
// refuses a range outside the process memory rather than reading it.
|
||||
unsafe { gex_kmem::pull_checked(buf.as_mut_ptr(), at, len) };
|
||||
let mut s = stdin().lock().unwrap_or_else(|e| e.into_inner());
|
||||
s.0.extend(buf);
|
||||
stdin().lock().unwrap_or_else(|e| e.into_inner()).0.extend(buf);
|
||||
stdin_queue().notify();
|
||||
len as i32
|
||||
}
|
||||
|
||||
@@ -295,6 +255,7 @@ pub extern "C" fn k_stdin_write(at: gex_kmem::PAddr, len: u32) -> i32 {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_stdin_eof() {
|
||||
stdin().lock().unwrap_or_else(|e| e.into_inner()).1 = true;
|
||||
stdin_queue().notify();
|
||||
}
|
||||
|
||||
/// Bytes waiting, or -1 once there will be no more and none are left.
|
||||
@@ -383,7 +344,7 @@ pub extern "C" fn k_tty_input(at: gex_kmem::PAddr, len: u32) -> i32 {
|
||||
let _ = procs().kill_group(gex_kernel::INIT_PID, fg_group(), sig);
|
||||
}
|
||||
// Wake a reader parked inside wasm, which runs no event loop.
|
||||
k_wake();
|
||||
stdin_queue().notify();
|
||||
echoed().lock().unwrap_or_else(|e| e.into_inner()).len() as i32
|
||||
}
|
||||
|
||||
@@ -468,12 +429,11 @@ pub extern "C" fn k_tty_signal(signo: u32) -> i32 {
|
||||
return -22;
|
||||
}
|
||||
note_kill(0, -fg_group(), signo);
|
||||
let n = match procs().kill_group(gex_kernel::INIT_PID, fg_group(), signo) {
|
||||
// `kill_group` wakes the group's waiting threads.
|
||||
match procs().kill_group(gex_kernel::INIT_PID, fg_group(), signo) {
|
||||
Ok(n) => n as i32,
|
||||
Err(e) => -(e.0 as i32),
|
||||
};
|
||||
k_wake();
|
||||
n
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -484,6 +444,8 @@ pub extern "C" fn k_tty_fg() -> i32 {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_tty_set_fg(pgrp: i32) {
|
||||
tty().set_fg(pgrp);
|
||||
// Whose read may go on has changed.
|
||||
stdin_queue().notify();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1752,6 +1714,12 @@ pub extern "C" fn k_has_signals(pid: i32) -> i32 {
|
||||
procs().has_signals(pid) as i32
|
||||
}
|
||||
|
||||
/// `pid`'s wait queue (`ProcTable::queue`).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_proc_queue(pid: i32) -> u32 {
|
||||
procs().queue(pid)
|
||||
}
|
||||
|
||||
/// Whether `pid` is a live process.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_exists(pid: i32) -> i32 {
|
||||
@@ -1975,6 +1943,12 @@ pub extern "C" fn k_pipe_read(id: i32, at: gex_kmem::PAddr, len: u32) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// The pipe's wait queue.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_pipe_queue(id: i32) -> u32 {
|
||||
pipe_at(id).map_or(0, |p| p.wait_queue())
|
||||
}
|
||||
|
||||
/// Would a read block? False also when every writer has gone, which is EOF.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_pipe_readable(id: i32) -> i32 {
|
||||
@@ -2143,7 +2117,6 @@ pub extern "C" fn k_pty_close_end(id: i32, master: i32, pid: i32) {
|
||||
.insert(id, pid);
|
||||
}
|
||||
}
|
||||
k_wake();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2168,9 +2141,6 @@ pub extern "C" fn k_pty_forget(pid: i32) -> u32 {
|
||||
}
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
k_wake();
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
@@ -2231,8 +2201,6 @@ pub extern "C" fn k_pty_write(id: i32, master: i32, at: gex_kmem::PAddr, len: u3
|
||||
} else {
|
||||
p.slave_write(&buf);
|
||||
}
|
||||
// Wake a reader parked in another Worker, which runs no event loop.
|
||||
k_wake();
|
||||
len as i32
|
||||
}
|
||||
|
||||
@@ -2375,9 +2343,16 @@ pub extern "C" fn k_pty_fg(id: i32) -> i32 {
|
||||
pub extern "C" fn k_pty_set_fg(id: i32, pgrp: i32) {
|
||||
if let Some(p) = pty_at(id) {
|
||||
p.tty.set_fg(pgrp);
|
||||
p.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// The pair's wait queue.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_pty_queue(id: i32) -> u32 {
|
||||
pty_at(id).map_or(0, |p| p.queue_id())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_pty_sid(id: i32) -> i32 {
|
||||
pty_at(id).map_or(0, |p| p.tty.sid())
|
||||
@@ -2840,17 +2815,8 @@ pub extern "C" fn k_sock_connect(path: gex_kmem::PAddr, path_len: u32) -> i32 {
|
||||
let p = path_of_raw(path, path_len);
|
||||
let s = std::sync::Arc::new(UnixSocket::new(true));
|
||||
match names().connect(&s, &p) {
|
||||
Ok(_) => {
|
||||
// A connection is waiting on someone's listener, and that someone
|
||||
// may be parked. The guest syscall path wakes after every call;
|
||||
// these entry points are reached from the *host* -- the
|
||||
// compositor's X window manager, the page's own sockets -- and woke
|
||||
// nothing at all. A parked Worker re-checks on its own every 20 ms
|
||||
// whatever happens, so this is latency rather than correctness:
|
||||
// without it a host-side connect waits for that timer.
|
||||
k_wake();
|
||||
keep(s)
|
||||
}
|
||||
// The listener's queue is notified by the connect itself.
|
||||
Ok(_) => keep(s),
|
||||
Err(e) => -e.0,
|
||||
}
|
||||
}
|
||||
@@ -2860,12 +2826,7 @@ pub extern "C" fn k_sock_connect(path: gex_kmem::PAddr, path_len: u32) -> i32 {
|
||||
pub extern "C" fn k_sock_accept(listener: i32) -> i32 {
|
||||
let Some(l) = sock_at(listener) else { return -9 };
|
||||
match l.accept() {
|
||||
Ok(s) => {
|
||||
// The other end is connected now, which a parked client may be
|
||||
// waiting to hear.
|
||||
k_wake();
|
||||
keep(s)
|
||||
}
|
||||
Ok(s) => keep(s),
|
||||
Err(e) => -e.0,
|
||||
}
|
||||
}
|
||||
@@ -2899,17 +2860,19 @@ pub extern "C" fn k_sock_write(id: i32, at: gex_kmem::PAddr, len: u32) -> i32 {
|
||||
if len > 0 {
|
||||
unsafe { gex_kmem::pull_checked(buf.as_mut_ptr(), at, len) };
|
||||
}
|
||||
// The send notifies the reading end's queue.
|
||||
match s.send(&buf) {
|
||||
Ok(n) => {
|
||||
// Bytes for whoever is reading, who may be parked. Same reason as
|
||||
// `k_sock_connect`: the host has no syscall path to wake for it.
|
||||
k_wake();
|
||||
n as i32
|
||||
}
|
||||
Ok(n) => n as i32,
|
||||
Err(e) => -e.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The end's wait queue.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_sock_queue(id: i32) -> u32 {
|
||||
sock_at(id).map_or(0, |s| s.queue_id())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_sock_readable(id: i32) -> i32 {
|
||||
use gex_kernel::vfs::FileHandle;
|
||||
@@ -3104,10 +3067,7 @@ pub extern "C" fn k_sock_write_fds(
|
||||
// back (and its references with it) rather than riding a later send.
|
||||
s.stage_send(Vec::new());
|
||||
match r {
|
||||
Ok(n) => {
|
||||
k_wake();
|
||||
n as i32
|
||||
}
|
||||
Ok(n) => n as i32,
|
||||
Err(e) => -e.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
//! Wait slots and wait queues (docs/kernel-locking.md §5.2 step 2).
|
||||
//!
|
||||
//! A *slot* is one waiting thread: a word here, in the memory every Worker
|
||||
//! shares, that its Worker parks on (`k_ws_park`) and a waker bumps and
|
||||
//! notifies. Each Worker runs one guest thread, so a Worker owns one slot for
|
||||
//! as long as it lives.
|
||||
//!
|
||||
//! A *queue* is one waitable object's list of waiters: a bitmap of slots,
|
||||
//! plus the queues chained to it (an epoll set watching the object). A waiter
|
||||
//! sets its bit on every object it waits for, checks readiness, parks on its
|
||||
//! slot, and clears the bits when the wait is over; whoever changes the
|
||||
//! object's readiness notifies its queue, which wakes the slots whose bits
|
||||
//! are set and nothing else.
|
||||
//!
|
||||
//! Lock-free on both sides: registration is a `fetch_or`, a notify reads the
|
||||
//! bitmap and bumps the slots in it. A waiter samples its slot's value
|
||||
//! *before* it registers and checks, and parks only while the slot still
|
||||
//! reads that value -- so a notify between its check and its park is not
|
||||
//! lost. (wasm's atomics are sequentially consistent, which is what makes the
|
||||
//! register-then-check against change-then-notify order sound.)
|
||||
//!
|
||||
//! The page reaches the same structures from JavaScript (`wl-bridge.mjs`
|
||||
//! wakes a process's compositor connection this way), so the layout is
|
||||
//! fixed and simple: see `Queue` and `k_wq_dir`.
|
||||
|
||||
use std::sync::atomic::{AtomicI32, AtomicPtr, AtomicU32, Ordering::SeqCst};
|
||||
|
||||
/// How many slots: more than the Workers that can exist at once
|
||||
/// (`STACK_COUNT`), each of which owns at most one.
|
||||
pub const SLOTS: usize = 256;
|
||||
const WORDS: usize = SLOTS / 32;
|
||||
/// Queues chained to one queue, kept inline (JavaScript walks them too).
|
||||
/// More spill into a map only Rust reads.
|
||||
const CHAINS: usize = 4;
|
||||
|
||||
/// One queue: 64 bytes. Fixed layout, read by JavaScript: the slot bitmap
|
||||
/// (8 words), then the inline chains (4), then the spill count.
|
||||
#[repr(C, align(64))]
|
||||
pub struct Queue {
|
||||
bits: [AtomicU32; WORDS],
|
||||
chains: [AtomicU32; CHAINS],
|
||||
spilled: AtomicU32,
|
||||
/// 1 while the queue is allocated, for a diagnostic.
|
||||
live: AtomicU32,
|
||||
_pad: [u32; 2],
|
||||
}
|
||||
|
||||
const QUEUE_BYTES: usize = 64;
|
||||
const _: () = assert!(std::mem::size_of::<Queue>() == QUEUE_BYTES);
|
||||
|
||||
/// Queues are allocated in chunks, never freed (a freed queue's number is
|
||||
/// reused), so a queue's address is stable and JavaScript can cache it.
|
||||
const CHUNK: usize = 1024;
|
||||
const CHUNKS: usize = 256;
|
||||
|
||||
static DIR: [AtomicPtr<Queue>; CHUNKS] = [const { AtomicPtr::new(std::ptr::null_mut()) }; CHUNKS];
|
||||
|
||||
/// The slot words.
|
||||
#[repr(C, align(64))]
|
||||
struct Slots([AtomicI32; SLOTS]);
|
||||
static SLOT_WORDS: Slots = Slots([const { AtomicI32::new(0) }; SLOTS]);
|
||||
/// Which slots are in use.
|
||||
static SLOT_USED: [AtomicU32; WORDS] = [const { AtomicU32::new(0) }; WORDS];
|
||||
/// How many slot notifies there have been, ever: the single-thread build
|
||||
/// (where nothing can block in `memory.atomic.wait32`) compares it after a
|
||||
/// call to learn whether a parked fiber may have been woken.
|
||||
static NOTIFIES: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// A tiny spin lock, for the queue allocator and the spill map: the page's
|
||||
/// own thread notifies (a keystroke) and must never block in a wait.
|
||||
struct Spin(AtomicU32);
|
||||
impl Spin {
|
||||
fn lock(&self) -> SpinGuard<'_> {
|
||||
while self.0.compare_exchange_weak(0, 1, SeqCst, SeqCst).is_err() {
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
SpinGuard(self)
|
||||
}
|
||||
}
|
||||
struct SpinGuard<'a>(&'a Spin);
|
||||
impl Drop for SpinGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.0 .0.store(0, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
static ALLOC: Spin = Spin(AtomicU32::new(0));
|
||||
/// Under `ALLOC`: the next never-used queue number, and the freed ones.
|
||||
static mut NEXT: u32 = 1;
|
||||
static mut FREE: Vec<u32> = Vec::new();
|
||||
static SPILL_LOCK: Spin = Spin(AtomicU32::new(0));
|
||||
/// Under `SPILL_LOCK`: chains past the inline ones, by queue.
|
||||
static mut SPILL: Option<std::collections::BTreeMap<u32, Vec<u32>>> = None;
|
||||
|
||||
fn queue(id: u32) -> Option<&'static Queue> {
|
||||
let (c, i) = (id as usize / CHUNK, id as usize % CHUNK);
|
||||
if id == 0 || c >= CHUNKS {
|
||||
return None;
|
||||
}
|
||||
let p = DIR[c].load(SeqCst);
|
||||
if p.is_null() {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: a published chunk is CHUNK queues, never freed.
|
||||
Some(unsafe { &*p.add(i) })
|
||||
}
|
||||
|
||||
pub fn new_queue() -> u32 {
|
||||
let _g = ALLOC.lock();
|
||||
// SAFETY: `NEXT` and `FREE` are only touched under `ALLOC`.
|
||||
let id = unsafe {
|
||||
let free = &mut *std::ptr::addr_of_mut!(FREE);
|
||||
match free.pop() {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let id = NEXT;
|
||||
let c = id as usize / CHUNK;
|
||||
if c >= CHUNKS {
|
||||
return 0;
|
||||
}
|
||||
if DIR[c].load(SeqCst).is_null() {
|
||||
let chunk: Box<[Queue]> = (0..CHUNK)
|
||||
.map(|_| Queue {
|
||||
bits: [const { AtomicU32::new(0) }; WORDS],
|
||||
chains: [const { AtomicU32::new(0) }; CHAINS],
|
||||
spilled: AtomicU32::new(0),
|
||||
live: AtomicU32::new(0),
|
||||
_pad: [0; 2],
|
||||
})
|
||||
.collect();
|
||||
DIR[c].store(Box::leak(chunk).as_mut_ptr(), SeqCst);
|
||||
}
|
||||
NEXT += 1;
|
||||
id
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(q) = queue(id) {
|
||||
q.live.store(1, SeqCst);
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
pub fn free_queue(id: u32) {
|
||||
let Some(q) = queue(id) else { return };
|
||||
// Whoever waits on a vanishing object must look again: its last close
|
||||
// is a hang-up or an end of file.
|
||||
notify(id);
|
||||
for w in &q.bits {
|
||||
w.store(0, SeqCst);
|
||||
}
|
||||
for c in &q.chains {
|
||||
c.store(0, SeqCst);
|
||||
}
|
||||
if q.spilled.swap(0, SeqCst) != 0 {
|
||||
let _g = SPILL_LOCK.lock();
|
||||
// SAFETY: under `SPILL_LOCK`.
|
||||
if let Some(m) = unsafe { &mut *std::ptr::addr_of_mut!(SPILL) } {
|
||||
m.remove(&id);
|
||||
}
|
||||
}
|
||||
q.live.store(0, SeqCst);
|
||||
let _g = ALLOC.lock();
|
||||
// SAFETY: under `ALLOC`.
|
||||
unsafe { (*std::ptr::addr_of_mut!(FREE)).push(id) };
|
||||
}
|
||||
|
||||
fn wake_slot(s: usize) {
|
||||
let w = &SLOT_WORDS.0[s];
|
||||
w.fetch_add(1, SeqCst);
|
||||
NOTIFIES.fetch_add(1, SeqCst);
|
||||
// SAFETY: the address of a static in this module's shared memory.
|
||||
unsafe {
|
||||
core::arch::wasm32::memory_atomic_notify(w as *const AtomicI32 as *mut i32, u32::MAX);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_depth(id: u32, depth: u32) {
|
||||
let Some(q) = queue(id) else { return };
|
||||
for (wi, w) in q.bits.iter().enumerate() {
|
||||
let mut bits = w.load(SeqCst);
|
||||
while bits != 0 {
|
||||
let b = bits.trailing_zeros() as usize;
|
||||
bits &= bits - 1;
|
||||
wake_slot(wi * 32 + b);
|
||||
}
|
||||
}
|
||||
// An epoll set inside an epoll set inside...: Linux bounds the nesting
|
||||
// too (EPOLL_MAX_NESTS, 4).
|
||||
if depth >= 4 {
|
||||
return;
|
||||
}
|
||||
for c in &q.chains {
|
||||
let to = c.load(SeqCst);
|
||||
if to != 0 {
|
||||
notify_depth(to, depth + 1);
|
||||
}
|
||||
}
|
||||
if q.spilled.load(SeqCst) != 0 {
|
||||
let more = {
|
||||
let _g = SPILL_LOCK.lock();
|
||||
// SAFETY: under `SPILL_LOCK`.
|
||||
unsafe { &*std::ptr::addr_of!(SPILL) }
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(&id).cloned())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
for to in more {
|
||||
notify_depth(to, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify(id: u32) {
|
||||
notify_depth(id, 0);
|
||||
}
|
||||
|
||||
fn chain(from: u32, to: u32) {
|
||||
let Some(q) = queue(from) else { return };
|
||||
for c in &q.chains {
|
||||
if c.compare_exchange(0, to, SeqCst, SeqCst).is_ok() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let _g = SPILL_LOCK.lock();
|
||||
// SAFETY: under `SPILL_LOCK`.
|
||||
let m = unsafe { &mut *std::ptr::addr_of_mut!(SPILL) }.get_or_insert_with(Default::default);
|
||||
m.entry(from).or_default().push(to);
|
||||
q.spilled.store(1, SeqCst);
|
||||
}
|
||||
|
||||
fn unchain(from: u32, to: u32) {
|
||||
let Some(q) = queue(from) else { return };
|
||||
for c in &q.chains {
|
||||
if c.compare_exchange(to, 0, SeqCst, SeqCst).is_ok() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if q.spilled.load(SeqCst) != 0 {
|
||||
let _g = SPILL_LOCK.lock();
|
||||
// SAFETY: under `SPILL_LOCK`.
|
||||
if let Some(m) = unsafe { &mut *std::ptr::addr_of_mut!(SPILL) } {
|
||||
if let Some(v) = m.get_mut(&from) {
|
||||
if let Some(i) = v.iter().position(|&x| x == to) {
|
||||
v.swap_remove(i);
|
||||
}
|
||||
if v.is_empty() {
|
||||
m.remove(&from);
|
||||
q.spilled.store(0, SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Install the queues as `gex_kernel::wait`'s, for the objects this module
|
||||
/// keeps (pipes, sockets, ptys, the process table).
|
||||
pub fn install() {
|
||||
gex_kernel::wait::set_hooks(gex_kernel::wait::Hooks {
|
||||
new: new_queue,
|
||||
free: free_queue,
|
||||
notify,
|
||||
chain,
|
||||
unchain,
|
||||
});
|
||||
}
|
||||
|
||||
// -- exports -----------------------------------------------------------------
|
||||
|
||||
/// A new queue, for an object a process keeps (an eventfd, an epoll set).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wq_new() -> u32 {
|
||||
new_queue()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wq_free(id: u32) {
|
||||
free_queue(id);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wq_notify(id: u32) {
|
||||
notify(id);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wq_chain(from: u32, to: u32) {
|
||||
chain(from, to);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wq_unchain(from: u32, to: u32) {
|
||||
unchain(from, to);
|
||||
}
|
||||
|
||||
/// Register slot `slot` on queue `id`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wq_add(id: u32, slot: u32) {
|
||||
if let Some(q) = queue(id) {
|
||||
let s = slot as usize % SLOTS;
|
||||
q.bits[s / 32].fetch_or(1 << (s % 32), SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// Undo [`k_wq_add`].
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wq_del(id: u32, slot: u32) {
|
||||
if let Some(q) = queue(id) {
|
||||
let s = slot as usize % SLOTS;
|
||||
q.bits[s / 32].fetch_and(!(1 << (s % 32)), SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// Where queue `id` is in this module's memory, or 0: JavaScript notifies a
|
||||
/// queue by walking it (`wl-bridge.mjs`, `notifyQueue`).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wq_addr(id: u32) -> u32 {
|
||||
queue(id).map_or(0, |q| q as *const Queue as u32)
|
||||
}
|
||||
|
||||
/// Where the chunk directory is: `CHUNKS` addresses, each of `CHUNK`
|
||||
/// queues of `QUEUE_BYTES`, for JavaScript to find a chained queue by number.
|
||||
/// (`wl-bridge.mjs` `notifyQueue` mirrors these constants.)
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wq_dir() -> u32 {
|
||||
DIR.as_ptr() as u32
|
||||
}
|
||||
|
||||
/// The calling Worker's own slot: its kernel stack's number, plus one, or
|
||||
/// 0 on the stack wasm-ld linked in (the first Worker's). A Worker's stack
|
||||
/// slice is its for as long as it lives, so the slot is too, and needs no
|
||||
/// freeing; a slice handed to a new Worker hands it the slot, and anything
|
||||
/// still registered there is at worst a spurious wake, which re-checks.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_ws_self() -> i32 {
|
||||
let local = 0u8;
|
||||
let at = core::hint::black_box(&local) as *const u8 as u32;
|
||||
let base = crate::k_stack_area();
|
||||
let end = base + crate::STACK_BYTES * crate::STACK_COUNT;
|
||||
if at >= base && at < end {
|
||||
((at - base) / crate::STACK_BYTES + 1) as i32
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// The first slot `k_ws_alloc` hands out: those below are the Workers'
|
||||
/// (`k_ws_self`).
|
||||
const FIRST_ALLOC: usize = crate::STACK_COUNT as usize + 1;
|
||||
const _: () = assert!(FIRST_ALLOC < SLOTS);
|
||||
|
||||
/// Take a free slot for a waiter that is not a Worker's guest thread (the
|
||||
/// page's X server, `hostx.js`), or -1.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_ws_alloc() -> i32 {
|
||||
for (wi, w) in SLOT_USED.iter().enumerate() {
|
||||
let mut cur = w.load(SeqCst);
|
||||
// The Workers' own slots are never handed out.
|
||||
let mut reserved = 0u32;
|
||||
for b in 0..32 {
|
||||
if wi * 32 + b < FIRST_ALLOC {
|
||||
reserved |= 1 << b;
|
||||
}
|
||||
}
|
||||
while cur | reserved != u32::MAX {
|
||||
let b = (!(cur | reserved)).trailing_zeros();
|
||||
match w.compare_exchange(cur, cur | (1 << b), SeqCst, SeqCst) {
|
||||
Ok(_) => return (wi * 32 + b as usize) as i32,
|
||||
Err(now) => cur = now,
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
|
||||
}
|
||||
|
||||
/// Give a slot back (its Worker is ending).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_ws_free(slot: i32) {
|
||||
if slot >= 0 && (slot as usize) < SLOTS {
|
||||
let s = slot as usize;
|
||||
SLOT_USED[s / 32].fetch_and(!(1 << (s % 32)), SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// What slot `slot` reads now: sampled before a waiter registers and checks.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_ws_seq(slot: i32) -> i32 {
|
||||
SLOT_WORDS.0[slot as usize % SLOTS].load(SeqCst)
|
||||
}
|
||||
|
||||
/// Where slot `slot`'s word is: a Worker that must keep its event loop
|
||||
/// turning (`gex.mjs` `#netWait`) waits on it with `Atomics.waitAsync`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_ws_addr(slot: i32) -> u32 {
|
||||
&SLOT_WORDS.0[slot as usize % SLOTS] as *const AtomicI32 as u32
|
||||
}
|
||||
|
||||
/// Sleep until slot `slot` leaves `seq`, or `ns` pass: 0 woken, 1 it had
|
||||
/// already moved, 2 timed out. A negative `ns` waits for ever.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_ws_park(slot: i32, seq: i32, ns: i64) -> i32 {
|
||||
let w = &SLOT_WORDS.0[slot as usize % SLOTS];
|
||||
// SAFETY: the address of a static in this module's shared memory, which
|
||||
// is what the instruction requires; only a Worker calls this.
|
||||
unsafe { core::arch::wasm32::memory_atomic_wait32(w as *const AtomicI32 as *mut i32, seq, ns) }
|
||||
}
|
||||
|
||||
/// Wake slot `slot` directly: a signal to the thread that owns it.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_ws_wake(slot: i32) {
|
||||
if slot >= 0 {
|
||||
wake_slot(slot as usize % SLOTS);
|
||||
}
|
||||
}
|
||||
|
||||
/// How many slot wakes there have been (see `NOTIFIES`).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_ws_notifies() -> u32 {
|
||||
NOTIFIES.load(SeqCst)
|
||||
}
|
||||
|
||||
/// How many queues are allocated now, for a census.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn k_wq_live() -> u32 {
|
||||
let _g = ALLOC.lock();
|
||||
// SAFETY: under `ALLOC`.
|
||||
unsafe { NEXT - 1 - (*std::ptr::addr_of!(FREE)).len() as u32 }
|
||||
}
|
||||
+72
-17
@@ -20,7 +20,7 @@
|
||||
// screen, exactly like a guest's own Wayland surface.
|
||||
//
|
||||
// The server blocks. `main()` never returns, and every wait goes through
|
||||
// `gex_poll` -> `wait()` here -> `k_park`, the same bounded park a guest
|
||||
// `gex_poll` -> `wait()` here -> `k_ws_park`, the same park a guest
|
||||
// process uses. That is why the housekeeping -- pumping the window manager,
|
||||
// publishing the scene, taking input -- happens inside `tick()`, which the
|
||||
// server calls every turn of its loop: after `callMain` there is no JavaScript
|
||||
@@ -91,6 +91,8 @@ export class HostX {
|
||||
#hwWords = null;
|
||||
/** The global kernel, instantiated against the X server's memory. */
|
||||
#k = null;
|
||||
/** Its memory. */
|
||||
#kmem = null;
|
||||
/** What each descriptor means. */
|
||||
#fds = new Map();
|
||||
/** Pools of pixels, by descriptor: where they are in the server's memory. */
|
||||
@@ -194,14 +196,17 @@ export class HostX {
|
||||
: 0);
|
||||
if (top) inst.exports.__stack_pointer.value = top | 0;
|
||||
this.#k = inst.exports;
|
||||
this.#kmem = msg.kernel.memory;
|
||||
}
|
||||
if (!this.#k) throw new Error('the host X server needs the global kernel');
|
||||
this.#waitInit();
|
||||
|
||||
// -- the arena ---------------------------------------------------------
|
||||
this.#proxy = new WlProxy(msg.wlChannel, () => this.emit({ kind: 'wl' }), () => this.#token);
|
||||
// The server waits on the global kernel's wake word (`wait` below), which
|
||||
// the compositor bumps when it puts input in this block's ring.
|
||||
this.#proxy.wakeAddr = () => this.#k?.k_wake_addr?.() ?? 0;
|
||||
// The server waits on its wait slot (`wait` below), which the compositor
|
||||
// notifies through the server's own queue when it puts input in this
|
||||
// block's ring.
|
||||
this.#proxy.wakeAddr = () => ({ addr: this.#k.k_wq_addr(this.#queue) >>> 0, slots: this.#k.k_ws_addr(0) >>> 0 });
|
||||
// The compositor Worker reads an X window's pixels out of this memory, so
|
||||
// it needs the memory itself -- the object, not a buffer, since a shared
|
||||
// memory that grows hands out a new one.
|
||||
@@ -307,9 +312,41 @@ export class HostX {
|
||||
this.log(`[hostx] cannot bind ${JSON.stringify(name)}: errno ${-id}`);
|
||||
return -1;
|
||||
}
|
||||
this.#watch(id);
|
||||
return this.#reserve({ kind: 'listen', id });
|
||||
}
|
||||
|
||||
// -- waiting (docs/kernel-locking.md §5.2 step 2) -------------------------
|
||||
//
|
||||
// The server waits as a guest thread does: on a wait slot of its own in the
|
||||
// global kernel's memory, registered on the wait queue of every socket it
|
||||
// holds and on a queue of its own the compositor notifies for input. A
|
||||
// guest's write to one of its sockets notifies it, and nothing else does.
|
||||
|
||||
/** The server's wait slot, its own queue, and the slot's value when the
|
||||
* last wait returned (a notify since then ends the next wait at once). */
|
||||
#slot = -1;
|
||||
#queue = 0;
|
||||
#seq = 0;
|
||||
|
||||
#waitInit() {
|
||||
const k = this.#k;
|
||||
this.#slot = k.k_ws_alloc();
|
||||
if (this.#slot < 0) throw new Error('the host X server found no free wait slot');
|
||||
this.#queue = k.k_wq_new();
|
||||
k.k_wq_add(this.#queue, this.#slot);
|
||||
this.#seq = k.k_ws_seq(this.#slot);
|
||||
}
|
||||
|
||||
/** Register on kernel socket `id`'s queue, for as long as it is held. */
|
||||
#watch(id) {
|
||||
this.#k.k_wq_add(this.#k.k_sock_queue(id), this.#slot);
|
||||
}
|
||||
|
||||
#unwatch(id) {
|
||||
this.#k.k_wq_del(this.#k.k_sock_queue(id), this.#slot);
|
||||
}
|
||||
|
||||
/** `k_sock_bind` wants the name in the process's memory. */
|
||||
#kSockBind(name) {
|
||||
const bytes = new TextEncoder().encode(name);
|
||||
@@ -372,6 +409,7 @@ export class HostX {
|
||||
const name = new TextDecoder().decode(self.#x.HEAPU8.subarray(path, path + len));
|
||||
const id = self.#kSockBind(name);
|
||||
if (id < 0) return id;
|
||||
self.#watch(id);
|
||||
self.#fds.set(fd, { kind: 'listen', id });
|
||||
return 0;
|
||||
},
|
||||
@@ -379,6 +417,7 @@ export class HostX {
|
||||
connect(fd, path, len) {
|
||||
const id = self.#k.k_sock_connect(path, len);
|
||||
if (id < 0) return id;
|
||||
self.#watch(id);
|
||||
self.#fds.set(fd, { kind: 'kernel', id });
|
||||
return 0;
|
||||
},
|
||||
@@ -388,6 +427,7 @@ export class HostX {
|
||||
if (!ep || ep.kind !== 'listen') return -EBADF;
|
||||
const id = self.#k.k_sock_accept(ep.id);
|
||||
if (id < 0) return id;
|
||||
self.#watch(id);
|
||||
const client = self.#reserve({ kind: 'kernel', id });
|
||||
self.log(`[hostx] a guest connected to the X display (fd ${client})`);
|
||||
return client;
|
||||
@@ -396,7 +436,10 @@ export class HostX {
|
||||
close(fd) {
|
||||
const ep = self.#fds.get(fd);
|
||||
if (!ep) return 0;
|
||||
if (ep.kind === 'kernel' || ep.kind === 'listen') self.#k.k_sock_close(ep.id);
|
||||
if (ep.kind === 'kernel' || ep.kind === 'listen') {
|
||||
self.#unwatch(ep.id);
|
||||
self.#k.k_sock_close(ep.id);
|
||||
}
|
||||
// A pool's descriptor is closed as soon as the server has handed it to
|
||||
// wl_shm, and the pixels have to outlive that: the compositor is still
|
||||
// reading them. So the allocation stays -- freeing it here is the one
|
||||
@@ -521,30 +564,42 @@ export class HostX {
|
||||
},
|
||||
|
||||
/**
|
||||
* Wait, the way a guest process waits: on the global kernel's wake word.
|
||||
* A Worker may block, which is what makes this possible at all -- and
|
||||
* what makes the X server's loop look exactly like a native one.
|
||||
* Wait, the way a guest process waits: on its wait slot, until one of
|
||||
* its sockets (or its compositor input) is notified, or `ms` -- the
|
||||
* server's own slice (gexos.c `gex_poll`). A Worker may block, which is
|
||||
* what makes this possible at all -- and what makes the X server's loop
|
||||
* look exactly like a native one.
|
||||
*/
|
||||
wait(ms) {
|
||||
const k = self.#k;
|
||||
const slot = self.#slot;
|
||||
const seq = self.#seq;
|
||||
const ns = BigInt(Math.max(1, ms)) * 1000000n;
|
||||
const done = () => {
|
||||
self.#seq = k.k_ws_seq(slot);
|
||||
return 0;
|
||||
};
|
||||
// The single-thread build (docs/single-thread.md): the server is a
|
||||
// fiber of the system Worker, so it parks on the scheduler, and the
|
||||
// wait is a promise its stack suspends on (gexos.c GEX_XWL_JSPI).
|
||||
if (single.on) return sched.park(Math.max(1, ms) * 1e6).then(() => 0);
|
||||
const seq = self.#k.k_wake_seq();
|
||||
self.#k.k_park(seq, BigInt(Math.max(1, ms)) * 1000000n);
|
||||
return 0;
|
||||
if (single.on) {
|
||||
const words = () => new Int32Array(self.#kmem.buffer);
|
||||
const r = sched.parkSlot(words, (k.k_ws_addr(slot) >>> 0) >>> 2, seq, ns);
|
||||
return r === 1 ? done() : r.then(done);
|
||||
}
|
||||
k.k_ws_park(slot, seq, ns);
|
||||
return done();
|
||||
},
|
||||
};
|
||||
// And what it does to a guest's socket is a wake the guests' fibers must
|
||||
// see: a guest process bumps the scheduler after every system call
|
||||
// (gex.mjs `k_wake`), and this server makes its socket calls straight
|
||||
// into the kernel module, which has no way to say so.
|
||||
// And what it does to a guest's socket notifies that socket's waiters,
|
||||
// whose words the guests' fibers do not `Atomics.wait` on in the
|
||||
// single-thread build: they look.
|
||||
if (single.on) {
|
||||
for (const name of ['connect', 'accept', 'close', 'send', 'recv']) {
|
||||
const fn = raw[name];
|
||||
raw[name] = (...a) => {
|
||||
const r = fn(...a);
|
||||
sched.wake();
|
||||
sched.checkSlots();
|
||||
return r;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user