Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/src/architecture/files.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ prefix. An empty view names nothing.
directory moved elsewhere never reach outside it. An absolute link is
resolved again in the client's own view, so it reaches nothing the client
does not hold.
- **A process's threads share its connection.** Each directory capability is
one connection per process, on which its threads' requests are in flight at
once, each in its own share of the window the client lends; none waits for
another's round trip but one that needs the whole window, or one beyond the
sixteen a connection carries. The server answers connections in turn, so a
process's share of the server does not grow with its threads.
- **Sharing is granted, never reached.** A file outside a program's view
reaches it only through the file picker, which hands over that one file.
- **No special cases.** The boot partition is in the updater's view and nobody
Expand Down Expand Up @@ -79,6 +85,7 @@ directory, which the child resolves in its own view.
- Ordinary I/O failure returns an error. A file server's crash invalidates
that server's open files, which then answer that they are gone; a fresh open
reaches the restarted server. Unsaved changes may be lost; data already
confirmed durable stays durable.
confirmed durable stays durable. A server that stays silent past its bound
while it owes a reply is let go as if it had crashed, as a block server is.
- Per-session temporary storage is an ordinary file server holding its
contents in RAM.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
status: open
kind: tooling
opened: 2026-11-11
---

# A call beside another thread of its process is unmeasured on metal

A process's threads have their requests on one directory capability in flight at once: up to the wire's 16, each in
its own share of the window, every thread waiting for its own tag's reply (`sdk/toyos/src/fs.rs`, its in-flight core
`fs/flight.rs`, modelled under loom in `kernel/loom/tests/fs_flight.rs`). One connection per process and capability
stays, so the server's per-connection fairness is unchanged; a connection per thread was the alternative, rejected
because it multiplies windows and server slots by threads and gives a many-threaded process a larger share of the
server.

Before, the process took one turn on the capability for each whole round trip. On the T14 (osbench `fileops`,
scratch stage timers, 2026-11-11, evidence of PR 1225), `fstat` on /tmp beside a thread of the same process asking as
fast as it is answered took 21.4 to 21.6 µs p50 against 7.6 alone (2.8x), and beside another process 8.4 to 8.5 µs
(1.1x). What a call beside another thread costs now is unmeasured: the benches are down.

- **Owed on metal:** `osbench_disk` on the T14, main from before the tagged replies and after, two boots a side in
A, B, B, A order. On both volumes, main after must show `stat.beside_a_thread` and
`open_close.beside_a_thread` within 1.2x of `stat` and `open_close` in the same boot; main before is the
comparison.

**Exit**: that measurement, the two rows within 1.2x on both volumes, or each remaining difference measured to its
parts.

**Owner**: agents/fileops (the file server's client).
24 changes: 24 additions & 0 deletions issues/a-file-server-reply-has-no-derived-worst-case.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
status: open
kind: defect
opened: 2026-11-12
severity: [hang]
---

# A file server's reply has no derived worst case

A file server's client lets its server go once it has waited `REPLY_BOUND_NS` for a reply it is owed: the block
protocol's silence bound, 365 s, and 60 s of room (`sdk/toyos/src/fs.rs`). Every request in flight on that connection
then fails `Unanswered`, and every file it opened answers `Stale`.

Nothing derives what a reply can take. One request may wait on several block requests in turn, each allowed the
block protocol's silence: an fsync writes back every page its file owes inside the server's one loop
(`system/fileserver/src/pages.rs`), and a write that needs room writes back first. The server answers its other
connections one request each in between, and nothing bounds how many there are. So a server that keeps its block
contract can stay silent past the bound and be let go, and the bound itself rests on no figure of the server's.

**Exit**: a derived worst case for a file server's reply, from the block requests one request may wait on and the
turns it may wait behind, with the client's bound set from it; or a server that tells a client it owes a reply is
still being worked on, so that silence past a bound means a hang.

**Owner**: agents/fileops (the file server and its client).

This file was deleted.

66 changes: 66 additions & 0 deletions kernel/loom/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,69 @@ pub mod sched;

#[cfg(feature = "loom")]
pub mod sched_model;

/// What a file server connection's requests in flight are built on, as loom's:
/// its atomics and lock, and the futex. A wait sleeps only while its word
/// still says what the waiter saw, checked under the lock a wake takes after
/// its store, and returns only once that word is woken, as the kernel's futex
/// wakes only a waiter of the word posted to: a wake the protocol leaves out
/// is loom's deadlock.
#[cfg(feature = "loom")]
pub mod flight_prim {
use std::collections::HashMap;

pub use loom::sync::atomic::{AtomicU32, Ordering};
use loom::sync::{Condvar, Mutex};

pub struct Lock<T>(Mutex<T>);

impl<T> Lock<T> {
pub fn new(value: T) -> Self {
Self(Mutex::new(value))
}

pub fn lock(&self) -> loom::sync::MutexGuard<'_, T> {
self.0.lock().unwrap()
}
}

/// Wakes counted by the word's address.
struct Futex {
wakes: Mutex<HashMap<usize, u64>>,
woken: Condvar,
}

loom::lazy_static! {
static ref FUTEX: Futex = Futex { wakes: Mutex::new(HashMap::new()), woken: Condvar::new() };
}

fn address(word: &AtomicU32) -> usize {
word as *const AtomicU32 as usize
}

pub fn wait(word: &AtomicU32, expected: u32) {
let mut wakes = FUTEX.wakes.lock().unwrap();
if word.load(Ordering::SeqCst) != expected {
return;
}
let seen = wakes.get(&address(word)).copied().unwrap_or(0);
while wakes.get(&address(word)).copied().unwrap_or(0) == seen {
wakes = FUTEX.woken.wait(wakes).unwrap();
}
}

pub fn wake_one(word: &AtomicU32) {
wake_all(word);
}

pub fn wake_all(word: &AtomicU32) {
*FUTEX.wakes.lock().unwrap().entry(address(word)).or_insert(0) += 1;
FUTEX.woken.notify_all();
}
}

/// One file server connection's requests in flight, the SDK's, driven by
/// `tests/fs_flight.rs` over [`flight_prim`].
#[cfg(feature = "loom")]
#[path = "../../../sdk/toyos/src/fs/flight.rs"]
pub mod fs_flight;
Loading