From dd0ceb7c443270d0b0f92c0e84da64f7f0ff7b6c Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 14:36:47 +0200 Subject: [PATCH 1/2] The quit's walk stays in the kernel, and the manual says what a quit request does The record asked which of two readings of the design holds: a parent asks its children to quit over its own protocol and the kernel keeps only the kill, or the kernel's walk of the subtree is the mechanism the interrupt, hang-up and terminate events need. The orchestrator decided the second. Why the walk stays: whether a process listens and whether it is claimed for its end are checked and acted on as one act under one lock, which the process-lifecycle model checks, so no read comes between them; a hung or deaf process in the middle of a tree cannot shield what is below it, since the walk reaches each process itself; and the userland alternative needs a listener in every process and a relay in three runtimes to carry a request down (the orchestrator's analysis; not re-derived here). The authority page now states it: a holder of a child's handle may ask its subtree to quit with a reason, a listener receives it as an event, one that never listened or has not taken the last request is ended, and the kernel never asks on its own. "Quitting gracefully is the parent's job" is replaced by that last clause, which is the part of it that was true. The sessions page read as if a sign-out offered every app the chance to finish, and as if a second request ended a process that ignored the first. What happens: the first ask ends at once every app that never listened; a second request ends a process that has not yet taken the first and asks again one that has taken it; a process that took a request and carries on is ended only by a force. issues/the-kernel-decides-who-is-asked-to-quit-and-who-is-killed.md is deleted and its one citation, in the kernel inventory's list of unresolved design changes, with it. The durable rule already stands at the site that owns it, kernel/proclife/src/quit.rs's header. Co-Authored-By: Claude Fable 5.2 --- docs/src/architecture/authority.md | 10 ++++++- docs/src/architecture/sessions.md | 17 +++++++----- ...-who-is-asked-to-quit-and-who-is-killed.md | 26 ------------------- ...-small-interrupts-post-and-threads-wait.md | 2 -- 4 files changed, 20 insertions(+), 35 deletions(-) delete mode 100644 issues/the-kernel-decides-who-is-asked-to-quit-and-who-is-killed.md diff --git a/docs/src/architecture/authority.md b/docs/src/architecture/authority.md index 61ce40ef3de..12a9273e2ab 100644 --- a/docs/src/architecture/authority.md +++ b/docs/src/architecture/authority.md @@ -69,7 +69,15 @@ ## The process tree - Every process has one parent. A parent's end, whether exit, crash or kill, - ends its whole subtree at once; quitting gracefully is the parent's job. + ends its whole subtree at once. +- A holder of a child's process handle may ask the child and everything + below it to quit, with a reason: an interrupt, a hang-up or a terminate. The + kernel carries the request to every process below. A process that listens + for such requests receives it as an event. One that never listened, or has + not yet taken the last request, is ended instead, and its subtree with it; + no process in between can shield what is below it. +- The kernel never asks on its own: whether and when to ask is the holder's + decision. - A child's end is readiness on its process handle, and says how: an exit code, a kill, or a fault of a named class. - There are no process groups, POSIX sessions, controlling terminals, signals, diff --git a/docs/src/architecture/sessions.md b/docs/src/architecture/sessions.md index 3a122b2d0ff..26d58f37afb 100644 --- a/docs/src/architecture/sessions.md +++ b/docs/src/architecture/sessions.md @@ -44,14 +44,19 @@ ## Ending a session -- Sign-out asks the session's apps to finish, allows cancel or force, then - ends the session's subtree. +- Sign-out asks each of the session's apps to quit + ([Processes and authority](authority.md)). An app that never listened for + such a request ends at once. The others receive it, and the sign-out waits + for them, reporting what remains, until it is cancelled or forced. The + session's subtree ends once its last app has. - A dropped connection or closed terminal sends its tree a hang-up, reports what remains and for how long, and keeps waiting. -- **No timer ends a process.** Only an explicit force does: a sign-out's - force, scoped to the user's own sessions, or the system controls' stop. A - second quit request ends a process that ignored the first only when a person - or an explicit force sends it. +- **No timer ends a process.** A process that received a request and carries + on is ended only by an explicit force: a sign-out's force, scoped to the + user's own sessions, or the system controls' stop. No request is repeated on + a timer. A second one, which ends a process that has not yet taken the + first and asks again one that has, is sent only by a person's action or + the machine's stop. ## Permissions diff --git a/issues/the-kernel-decides-who-is-asked-to-quit-and-who-is-killed.md b/issues/the-kernel-decides-who-is-asked-to-quit-and-who-is-killed.md deleted file mode 100644 index 2a002a7e8ca..00000000000 --- a/issues/the-kernel-decides-who-is-asked-to-quit-and-who-is-killed.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -status: open -kind: finding -opened: 2026-11-11 ---- - -# The kernel decides who is asked to quit and who is killed - -An open design question, the orchestrator's to resolve, and an inference: -raised by the review of #1208, of the inventory in -`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`. - -`SYS_PROCESS_QUIT` walks the subtree in the kernel, puts the reason in the -notice of each process that listens and kills each that does not -(`kernel/proclife/src/quit.rs`, 216 lines; `quit_process` in -`kernel/src/process.rs`; `kernel/src/object/notice.rs`, 61). The design -reads both ways: quitting gracefully is the parent's job, and interrupt, -hang-up and terminate requests are native events a program receives. Under -the first a parent would ask its children over its own protocol and the -kernel would keep only the kill; under the second the kernel's walk is the -mechanism those events need. -`issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md` -built it as its stage 6. - -**Exit**: the design says which reading holds; this file is deleted, or the -walk's removal becomes a stage of the track. diff --git a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md index 28f64e413f7..5c6fe62371b 100644 --- a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md +++ b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md @@ -406,8 +406,6 @@ Changes of the design, each inferred and none of them work until resolved: `issues/a-namespace-is-a-kernel-object-and-service-lookup-is-kept-out-of-the-kernel.md`. - **The stop's sweep of userland threads**: `issues/the-kernel-stops-every-userland-thread-at-a-stop-the-supervisor-orders.md`. -- **The quit's walk**: - `issues/the-kernel-decides-who-is-asked-to-quit-and-who-is-killed.md`. ### The next removals, ranked (a proposal) From 954b57d85f0754177dfdc70158a7e1fa2dd8f695 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 11 Nov 2026 15:33:10 +0200 Subject: [PATCH 2/2] The machine's stop keeps its gate and waits for no thread The kernel's stop did two things: it closed Ring 3 to every thread but its caller, and it swept the process table, marked parked threads and waited inside a budget until every other thread had parked, banded or exited, then wrote a record of the count. The first is the kernel's alone: its last sequence takes ports back, settles what programs left in flight and cuts power, and no program can vouch that nothing runs in Ring 3 across it. The second kept a thread parked when the stop began from running the rest of its syscall, since a wake put a marked thread in the band unrun, and gave a thread running in a syscall a budget to reach its boundary that it could outlast. Neither is something the machine's end needs: what had to finish is flushed, synced and ended before the supervisor asks. A thread inside a syscall, or parked in one and woken later, now runs on to its boundary, and what it logs may follow the last word. The gate stays. `quiesce::stop` names its caller, then demands a pass of every other CPU and returns once each has begun one (`Doorbell:: demand_pass`, `driver::pass_on_every_other_cpu`, shared by both architectures): a pass runs in Ring 0, and whatever a CPU returns to Ring 3 after it reads the stop begun and is banded at that boundary. A CPU that begins no pass inside `DEAF_CPU` is wedged, a panic. This answer from Ring 0 replaces the user-half shootdown `pio::take_back` ran for the same reason, and the `Stop` shootdown origin goes with it. Removed: the sweep, its watch and budget, `note_progress` and the `SeqCst` fence it put on every park, band and exit; the watch `Gate` and its loom model and control; `TaskShared`'s `STOP` mark with `stop_if_blocked`, `stop_pending` and `mark_stop`, and `CpuSched::place`'s arm that banded a woken or adopted stopped task (a banded task's word reads `Ready` on its own CPU, so no wake claims it and a retire finds it at home and leaves it); the `stop:` record and its parser in `toyos-quiesce`, which keeps the supervisor's bounds; the `stop-budget-spent` actuator. The band itself stays: a task stopped at its boundary still needs a container. Scheduler functions changed: `CpuSched::place`, `keep_stopped`, `handle_retire` (a comment), `SchedPass::dispose_stop`, `invariants::check_cpu`, `Doorbell::begin_pass` (and new `demand_pass`, `pass_demanded`), `driver::pass`, `driver::pass_block`, `driver::ring_all_but_self` (now `pass_on_every_other_cpu`). Tests: `machine_shutdown_short_stop` judged the record's shortfall, which no longer exists; it becomes `machine_shutdown_with_a_thread_spinning`, two CPUs with a spinner in Ring 3 when the stop begins, judged by a clean power-off. `virt_smp` no longer reads a record's CPU count, and the USB ordering row orders usbd's end before the kernel's last word. The new loom model `a_pass_that_answers_a_demand_reads_what_its_demander_wrote` reds under the existing `doorbell-kick-relaxed` control, and `a_retire_for_a_stopped_task_leaves_it_in_the_band` replaces the unit tests of the removed mark. The question record asked whether the kernel's sweep held anything the supervisor's stop does not, on the premise that the design names only the power-off as the kernel's. The design never named the sweep, but the Ring 3 gate is not the supervisor's to provide; that is what the record's exit wanted written at the stop's header, and it is. The record is deleted, with six records about the removed wait, record, staging and stop routes; the services page says the kernel waits for no program once the stop is asked. An unrelated aarch64 defect met on the way, `virt_mask_windows` red on main itself, is recorded. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 - Cargo.toml | 3 - build/ci.rs | 11 +- docs/src/architecture/services.md | 5 +- ...ash-record-survives-a-reset-outside-ram.md | 4 +- ...-last-word-and-no-record-can-say-so-now.md | 43 --- ...r-off-after-the-kernels-own-acpi-enable.md | 2 +- ...ve-up-on-one-thread-beside-the-held-one.md | 78 ----- ...st-its-serial-ready-beside-other-guests.md | 26 -- ...-small-interrupts-post-and-threads-wait.md | 7 +- ...-thread-at-a-stop-the-supervisor-orders.md | 31 -- ...thout-saying-which-thread-took-the-time.md | 84 ------ ...-staging-dies-on-a-guest-clock-deadline.md | 17 -- ...heduler-explorer-never-generates-a-stop.md | 26 -- ...k-with-no-hook-in-a-lower-el-data-abort.md | 39 +++ kernel/Cargo.toml | 4 - kernel/loom/Cargo.toml | 7 - kernel/loom/tests/loom_sleep.rs | 31 ++ kernel/loom/tests/loom_watch.rs | 77 +---- kernel/pure/sched/cpu.rs | 121 ++------ kernel/pure/sched/invariants.rs | 10 - kernel/pure/sched/mailbox.rs | 42 ++- kernel/pure/sched/task.rs | 79 +---- kernel/pure/sched/watch.rs | 55 +--- kernel/src/actuator.rs | 3 - kernel/src/arch/x86_64/pio.rs | 9 +- kernel/src/invalidation.rs | 10 +- kernel/src/log/console.rs | 5 +- kernel/src/power.rs | 8 +- kernel/src/process.rs | 7 +- kernel/src/quiesce.rs | 230 +++------------ kernel/src/sched/driver.rs | 44 +-- kernel/src/scheduler.rs | 12 +- kernel/src/syscall/machine.rs | 17 +- kernel/src/watch.rs | 5 +- lib/quiesce/Cargo.toml | 2 +- lib/quiesce/src/lib.rs | 271 +----------------- tests/common/power.rs | 38 +-- tests/guest/rust-tests/src/bin/stop_short.rs | 14 - .../guest/rust-tests/src/bin/stop_spinning.rs | 13 + tests/toyos.rs | 37 ++- 41 files changed, 312 insertions(+), 1217 deletions(-) delete mode 100644 issues/a-thread-ended-after-the-boots-last-word-and-no-record-can-say-so-now.md delete mode 100644 issues/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md delete mode 100644 issues/quiesce-wakes-on-the-last-park-lost-its-serial-ready-beside-other-guests.md delete mode 100644 issues/the-kernel-stops-every-userland-thread-at-a-stop-the-supervisor-orders.md delete mode 100644 issues/the-machine-stops-without-saying-which-thread-took-the-time.md delete mode 100644 issues/the-quiesce-last-staging-dies-on-a-guest-clock-deadline.md delete mode 100644 issues/the-scheduler-explorer-never-generates-a-stop.md create mode 100644 issues/virt-mask-windows-finds-a-mask-with-no-hook-in-a-lower-el-data-abort.md delete mode 100644 tests/guest/rust-tests/src/bin/stop_short.rs create mode 100644 tests/guest/rust-tests/src/bin/stop_spinning.rs diff --git a/Cargo.lock b/Cargo.lock index 3fff926604a..005403001d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1564,7 +1564,6 @@ dependencies = [ "toyos-pci", "toyos-phys", "toyos-proclife", - "toyos-quiesce", "toyos-random", "toyos-register", "toyos-rootimage", @@ -3659,7 +3658,6 @@ dependencies = [ "toyos-microcode", "toyos-osrelease", "toyos-parity", - "toyos-quiesce", "toyos-radio", "toyos-session", "toyos-ssh", diff --git a/Cargo.toml b/Cargo.toml index 72da98b6910..ad7c6846858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -337,9 +337,6 @@ acpiserver-api = { path = "lib/acpiserver-api" } # The metal loop, the suite's command line, and what the metal profile # derives, times and reads off a boot. toyos-metal = { path = "tests/metal" } -# The shutdown's stop record: the kernel renders it and the harness reads it -# back off a log, so one declaration is what both sides spell it with. -toyos-quiesce = { path = "lib/quiesce" } # The parity table `crypto_parity` prints and the judge reads, under one rule. toyos-parity = { path = "lib/parity" } # The package energy counter's decode, so the `counters` judge's reading of diff --git a/build/ci.rs b/build/ci.rs index 32a8297bd99..610306b693e 100644 --- a/build/ci.rs +++ b/build/ci.rs @@ -620,6 +620,12 @@ pub(crate) const CONTROLS: &[Control] = &[ test: "a_halted_cpu_with_queued_work_was_kicked", message: "halted with 2 of 2 messages queued and no IPI in flight", }]), + // The same edge under a demanded pass, in a run of its own: the model + // above ends its process. + red(KERNEL_LOOM, "doorbell-kick-relaxed", Some("loom_sleep"), &[Says { + test: "a_pass_that_answers_a_demand_reads_what_its_demander_wrote", + message: "a pass answered the demand and read the gate closed after it", + }]), red(KERNEL_LOOM, "push-fence-relaxed", Some("loom_push"), &[Says { test: "a_cpu_that_halts_without_seeing_the_surplus_was_pushed", message: "published and no push behind it", @@ -646,11 +652,6 @@ pub(crate) const CONTROLS: &[Control] = &[ test: "a_second_post_is_not_lost_to_a_flag_the_waiter_consumed", message: "parked with both conditions true and no wake owed: a post answered off a load", }]), - // The stop's store-buffering pair with the gate's fences gone. - red(KERNEL_LOOM, "gate-fence-off", Some("loom_watch"), &[Says { - test: "a_transition_racing_an_opening_gate_is_never_missed", - message: "the stop parked over a thread that had parked, and nothing posted it", - }]), // `kernel-loom`'s control, over the kernel's `Once` as the watch models' // ring entry. red(KERNEL_LOOM, "poll-fire-load-store", Some("loom_watch"), &[ diff --git a/docs/src/architecture/services.md b/docs/src/architecture/services.md index 37747acaf96..f770dad2a11 100644 --- a/docs/src/architecture/services.md +++ b/docs/src/architecture/services.md @@ -10,7 +10,10 @@ - It never dies and stays small: its decisions are pure, host-tested logic, and the process only holds handles, spawns and runs its loop. - It owns the machine's orderly stop, derived from the manifest's - dependencies, and replaces a running service's binary when an authorised + dependencies. Once it asks the kernel to end the machine, no program but + the asking thread runs its own code again; the kernel waits for no + program to finish, so what must finish first is done before the ask. +- It replaces a running service's binary when an authorised caller in a login session asks. The other services are the components in the [Overview](overview.md). Each is diff --git a/issues/a-crash-record-survives-a-reset-outside-ram.md b/issues/a-crash-record-survives-a-reset-outside-ram.md index cd3024a3edb..64b0812efd8 100644 --- a/issues/a-crash-record-survives-a-reset-outside-ram.md +++ b/issues/a-crash-record-survives-a-reset-outside-ram.md @@ -17,8 +17,8 @@ What went with the black-box page and comes back with this track: the metal rows that judged a sealed record (`boot_deadline_ends_a_wedge`, `hard_lockup_ends_a_deaf_cpu`, `watchdog_resets` and the `wedge-before-reset` and `hard-lockup-probe` arms they staged), the -records a stop writes after `logkeeper`'s last flush (its stop record, the -machine's census and the panel's, which `irq_census_conservation` and the +records a stop writes after `logkeeper`'s last flush (the machine's census +and the panel's, which `irq_census_conservation` and the boot's panel timings read), and the window between `logkeeper`'s stop mark and the reset: a hang in the drivers' stop, the kernel's quiesce or the reset, which the deadline then ends, leaves the volume of a clean stop and diff --git a/issues/a-thread-ended-after-the-boots-last-word-and-no-record-can-say-so-now.md b/issues/a-thread-ended-after-the-boots-last-word-and-no-record-can-say-so-now.md deleted file mode 100644 index 3bd94bf798e..00000000000 --- a/issues/a-thread-ended-after-the-boots-last-word-and-no-record-can-say-so-now.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-25 ---- - -# A thread ended after the boot's last word, and no record can say so now - -`quiesce_wakes_on_the_last_park` (`tests/common/power.rs`'s `stopped_boot`) -was red twice in one fast-tier run on PR #492, once beside other guests and -once alone, with the dev host also running another worktree's suite: - -``` -1 line(s) reached the console after the boot's last word: - [kernel 2.546 cpu0 tid=2] exit: test_rs_quiesce_last tid=2 code=0 cpu=2014ms -``` - -(`2.468` on the rerun.) Three runs alone right after, on the same tree, were -green. The round under test changed no kernel, init, test-runner or -`tests/quiescelastcase` file, so the record was the kernel's own: the thread -the job parked last ended, and the record of its end reached the console -after `Rebooting.`. - -**The record that showed it is deleted, and what it showed is not answered.** -A thread's end writes nothing now (`kernel/src/process.rs`'s header), so that -line cannot follow the last word again and no boot can show this. The sighting -does not say which of two things happened: the thread ran its exit after the -stop had counted it stopped, or it ended before the last word and its record -was committed behind it. The first is a thread running kernel code on a -machine the stop has declared still; the stop's `stop:` record carries counts -taken at its last sweep and nothing later. - -Owner: the stop path, `kernel/src/quiesce.rs`. - -## Exit condition - -- `quiesce_wakes_on_the_last_park`, deleted with the commits - `issues/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md` - names, is back, and with it `stopped_boot`, which the tree no longer has. -- The stop says how many threads ended between its `stop:` record and the - reset, in a record the next loader pass prints; `stopped_boot` reds on one - that is not zero, and the test is green beside other guests on a loaded - host with that check in it. diff --git a/issues/no-t14-row-reads-the-power-off-after-the-kernels-own-acpi-enable.md b/issues/no-t14-row-reads-the-power-off-after-the-kernels-own-acpi-enable.md index 2d1a94ce7e5..5f02e29b4fc 100644 --- a/issues/no-t14-row-reads-the-power-off-after-the-kernels-own-acpi-enable.md +++ b/issues/no-t14-row-reads-the-power-off-after-the-kernels-own-acpi-enable.md @@ -31,7 +31,7 @@ ACPI_ENABLE 0xf0 written to SMI_CMD 0xb2, SCI_EN set 13371ns after` at 13.535 s, the supervisor's `(Shutdown)` line followed at 13.540 s, and the loader pass after it found no panic record. -**What QEMU reads of it.** `machine_shutdown`, `machine_shutdown_short_stop` +**What QEMU reads of it.** `machine_shutdown`, `machine_shutdown_with_a_thread_spinning` and `acpi_power_button` power q35 off in ACPI mode, and none of them after the kernel's enable: OVMF hands q35 over with `SCI_EN` set, so the mint writes nothing. A guest can be put in legacy mode from outside, with nothing diff --git a/issues/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md b/issues/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md deleted file mode 100644 index 2bf016e4533..00000000000 --- a/issues/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -status: assigned -kind: defect -opened: 2026-09-28 ---- - -# `quiesce_wakes_on_the_last_park`'s stop gave up on one thread beside the held one - -Three Fast tiers carry the identical failure: - -``` -FAIL quiesce_wakes_on_the_last_park: the stop gave up on 2 thread(s) that never reached a safe point: - stop: 3 of 5 userland thread(s) stopped across 2 cpu(s) in 2010 ms of a 2010 ms budget over 2 sweep(s), 0 of N userland block operation(s) still open; this reset lands wherever the other 2 are -``` - -- PR #539 at `2d6d228f`. -- PR #555 at `d2656765`. -- PR #559 at `ac948e6a`. - -`quiesce_wakes_on_the_last_teardown`'s boot shares the same shape, in the Fast -tier: PR #549 at `751e36d9`, `4 of 6 … 2010 ms of a 2010 ms budget over 2 -sweep(s)`. That boot normally counts 5 userland threads. - -The earliest is PR #510 at `98e803cb`, recorded in -`issues/quiesce-wakes-on-the-last-park-lost-its-serial-ready-beside-other-guests.md`: -`stop: 4 of 7 userland thread(s) stopped ... in 2010 ms of a 2010 ms budget`. -That boot then lost its READY, so the harness reported the READY and not the -stop. - -One of the two threads is the held thread by construction: `quiesce::last::hold` -yields until the latest sweep counts 1 running, so a sweep that counts 2 keeps -it spinning. The defect is the other thread. No sighting can name it, because -the `stop:` record carries only counts. - -**Hypothesis A, untested.** The hold's yield loop keeps its CPU busy. A Ready -thread queued on that CPU then runs only if `dispose_yield` -(`kernel/pure/sched/cpu.rs`) re-inserts the spinner behind it. - -**What no guest test checks while these are deleted.** - -- that a band, a park or an exit wakes the stop, rather than its deadline; -- `in_flight == 0` with `begun > 0`; -- the thread census; -- the `console-queue-at-the-stop` drain; -- that the stop waits for a teardown in flight — - `quiesce_wakes_on_the_last_teardown`'s only claim, and the only enabled - guest check of it, deleted on this same issue. - -`quiesce_refuses_a_second_shutdown` stays green over a lost post. - -**Exit**: - -- an instrument that names each thread still running when the stop gives up, - with its name, tid, cpu and scheduler state; -- the mechanism it names fixed; -- this test green beside other guests; -- `quiesce_wakes_on_the_last_teardown` — the stop waits for a teardown in - flight — green beside other guests; -- an enabled guest test checking each claim listed above. - -Owner: the stop path, `kernel/src/quiesce.rs`; held by the orchestrator. - -**Unrun since it was disabled**: PR #562 deleted the stop's completion from -every QEMU verdict: `stopped_boot`'s `stopped_the_machine` check, whose message -the sightings above quote, and `woken_by_its_threads`. The stop gives up at a -budget of the kernel's own clock, and the metal check that redded a stop -that gave up (`metal::Readback::stop_completed`) read the stop record off the -black-box page and went with it, so nothing reds one now. This test now -judges the held thread before the sync and more than one sweep, and the failure -quoted above no longer reds it. That change has never run: the test's first run -back is also that change's. - -**Its tests are deleted**: `6b3ce2374` took `quiesce_wakes_on_the_last_park` -and `quiesce_wakes_on_the_last_teardown` out, and `b82f45e6e` the -`quiesce-last-teardown` actuator only the second armed. `git revert b82f45e6e -6b3ce2374` brings both back as they stood before #536; `git show 84471bc58` -holds #536's adaptation of `tests/quiescelastcase/system.toml` and of -`tests/common/power.rs`'s `woken_by_the_held_thread`. diff --git a/issues/quiesce-wakes-on-the-last-park-lost-its-serial-ready-beside-other-guests.md b/issues/quiesce-wakes-on-the-last-park-lost-its-serial-ready-beside-other-guests.md deleted file mode 100644 index bbfc027a8f7..00000000000 --- a/issues/quiesce-wakes-on-the-last-park-lost-its-serial-ready-beside-other-guests.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -status: open -kind: finding -opened: 2026-09-26 ---- - -# `quiesce_wakes_on_the_last_park` lost its serial READY beside other guests - -Fast tier at `98e803cb` (PR #510's branch; another worktree's DNS work was -building at the same time): `QEMU died before ===READY=== (status: exit 0)`. -The console carried the boot through `quiesce-last-park: quiesce-last is held -until the stop waits on it alone`, `stop: 4 of 7 userland thread(s) stopped -... in 2010 ms of a 2010 ms budget`, `usb-quiesce: disk 0 SYNCHRONIZE CACHE -ok` and `Rebooting.`, then `shutdown: /log did not answer in 2000ms`; the uart -captured `nothing at all`. The harness's re-run alone was green in 2 s. - -**Exit**: a cause for the empty uart on a boot that rebooted as designed, or -the marker waited for where the boot's reboot cannot race it. - -Again in PR #532's fast tier at `a7f423b5`, 399 passed and this one of two reds: -`QEMU died before ===READY===` (exit status 0), the console through -`stop: 7 of 7 userland thread(s) stopped ... in 1 ms of a 2010 ms budget`, -`Rebooting.` and `shutdown: /log did not answer in 2000ms`, the uart -`nothing at all`; `ALONE ... GREEN`. That branch touches no quiesce path. - -`quiesce_wakes_on_the_last_park` is deleted; `issues/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md` records the commit that restores it. diff --git a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md index 5c6fe62371b..1abc8f01b42 100644 --- a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md +++ b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md @@ -358,7 +358,7 @@ state. Nothing here is ruled, and the ranking is a proposal. | Interrupt controllers, vectors, traps | 10 | 3,970 | yes | | Log ring, console UART, panic console, panic capture, own symbols | 17 | 3,517 | yes | | Scheduling: the core's driver, context switch, idle states, jobs, preemption | 20 | 3,160 | yes | -| Firmware tables, ACPI mode, reset and power-off, the machine's stop, what the loader named | 12 | 3,099 | yes for the tables, the `acpi` row and power; the rest below | +| Firmware tables, ACPI mode, reset and power-off, the machine's stop, what the loader named | 12 | 3,099 | yes for the tables, the `acpi` row, power and the stop's gate; what the loader named has left (#1210) | | Processes, threads, spawn | 8 | 2,736 | yes, less the userland text a spawn carries (below) | | Clocks: TSC and generic timer, the wall clock, typed durations | 7 | 1,847 | yes; the x86-64 CMOS driver and its IRQ8, 285 of these lines, went after this count | | Counters, the diary, IRQ census, mask windows, boot deadline, hard lockup, chipset watchdog | 14 | 1,844 | counters and diary yes; the three bounds are unnamed and inferred to be the kernel's, since they bound the kernel | @@ -404,8 +404,6 @@ Changes of the design, each inferred and none of them work until resolved: something in userland that maps a build-id to a file; neither is built. - **Namespaces**: `issues/a-namespace-is-a-kernel-object-and-service-lookup-is-kept-out-of-the-kernel.md`. -- **The stop's sweep of userland threads**: - `issues/the-kernel-stops-every-userland-thread-at-a-stop-the-supervisor-orders.md`. ### The next removals, ranked (a proposal) @@ -448,8 +446,7 @@ out, and a larger number needs one of the design changes above. (`issues/fstats-answer-is-declared-twice.md` owns its two declarations). - Stale text: `kernel/Cargo.toml`'s description says "filesystem"; `kernel/CLAUDE.md`'s first caveat is about a filesystem on the idle path; - `quiesce.rs`'s header speaks of the block layer and a USB stop, neither - of which the kernel has; `UserImage`'s doc comment + `UserImage`'s doc comment (`kernel/src/process.rs`) says "the file the kernel opened", and the kernel opens none. diff --git a/issues/the-kernel-stops-every-userland-thread-at-a-stop-the-supervisor-orders.md b/issues/the-kernel-stops-every-userland-thread-at-a-stop-the-supervisor-orders.md deleted file mode 100644 index efdd46ec1c6..00000000000 --- a/issues/the-kernel-stops-every-userland-thread-at-a-stop-the-supervisor-orders.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -status: open -kind: finding -opened: 2026-11-11 ---- - -# The kernel stops every userland thread at a stop the supervisor orders - -An open design question, the orchestrator's to resolve. Found by the -inventory in -`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`. - -`SYS_SHUTDOWN` and `SYS_REBOOT` first stop every userland thread at its -next return to Ring 3 and wait for them inside a budget -(`kernel/src/quiesce.rs`, 216 lines, deciding by `toyos-quiesce`, 294, and -about 110 of `kernel/src/syscall/machine.rs`), then log the census and end -the machine. The supervisor owns the orderly stop and has already ended or -flushed what it started before it asks -(`issues/the-supervisor-is-host-tested-and-owns-the-stop.md`). The design -names the power-off as the kernel's and does not name this stop of -threads. - -What is not established is what the kernel's stop holds that the -supervisor's does not: the census is of a machine that has stopped, and the -console wire is taken from its holder. Whether a supervisor that ends its -subtree before the call makes the kernel's sweep redundant is an inference -nobody has checked against the stop's tests. - -**Exit**: what the kernel's sweep establishes that the supervisor's stop -cannot is written at `kernel/src/quiesce.rs`'s header and this file -deleted, or the sweep is removed as a stage of the track. diff --git a/issues/the-machine-stops-without-saying-which-thread-took-the-time.md b/issues/the-machine-stops-without-saying-which-thread-took-the-time.md deleted file mode 100644 index b84a901ca40..00000000000 --- a/issues/the-machine-stops-without-saying-which-thread-took-the-time.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-14 ---- - -# The machine stop says how many threads it waited for and never which - -`kernel/src/quiesce.rs`'s `stop` sweeps the process table, parks until a thread -it names stops, parks or exits, and sweeps again until every one has. The record it writes -says how many threads there were and how long they took; it does not say -**which** one the time went on. - -## What the machine measured - -Run 41, `tests/images/metalcase.toml` on the T14, at `52c8a3f9` — before the sweep had a -cadence, so the sweep count is that kernel's and not this one's: - -``` -[20:38:12 7.955 cpu4] stop: 7 of 7 userland thread(s) stopped across 8 cpu(s) -in 129 ms over 67953 sweep(s), 0 block operation(s) still open -``` - -The same kernel on the same machine, `tests/images/metaldevicecase.toml`, one boot later: - -``` -[20:39:43 13.896 cpu6] stop: 5 of 5 userland thread(s) stopped across 8 cpu(s) -in 0 ms over 1 sweep(s), 0 block operation(s) still open -``` - -129 ms against 0 ms, and nothing in either file says what the difference was. -What can be established from the log is only this: - -- It was not a *parked* thread. `stop_if_blocked` is a CAS, not a wait, so a - parked thread is marked on the first sweep that sees it. -- So it was a thread that was running — either in Ring 3, where the kick's IPI - takes it to `kernel_exit_to_user_check` at its next return, or inside a - syscall, where its safe point is however far away that syscall's remaining - work puts it. -- 129 ms is about ten of cpu1's timer periods on that boot (275 timer - interrupts over 3.55 s), which is more than a kick-to-boundary costs. - -That points at a syscall rather than a spinning userland thread, and it is an -inference and not a reading. `tests/images/metalcase.toml` starts `sshd` and -`tests/images/metaldevicecase.toml` does not, which is the difference between the two boots -above; `sshd` wrote no `exit:` record on run 41 at all, so it was alive and -running when the stop found it. Which of the seven took the time is not in the -file. - -## What the dev host measures, and what the budget could be - -Twelve boots of `quiesce_stops_the_machine` at `feffffb6` — a kernel that swept -on a 10 ms cadence rather than on its threads' transitions — `tests/quiescecase`, -six writer threads on two vcpus: - -| host | park, per boot | -|---|---| -| quiet | 10, 10, 10, 20, 169 ms | -| loaded, 24 spinners, load 36-95 | 10, 10, 10, 12, 26, 156, 158 ms | - -All twelve read `9 of 9` stopped and `0` open, so none is a reading of a stop -that struggled. `quiesce::PARK` is 2,010 ms, and the worst of them spends 8% of -it — on the quiet host, which is what says the quantity is not a function of -host load alone. - -The budget cannot simply be widened to the bound that would cover a thread -inside a run of block work: `block::DEADMAN` is 120 s and the boot deadline is -armed straight through `quiesce` — `kernel/src/deadline.rs` stands down only for -a panic — so a park that long is ended as a wedge rather than expiring with the -record this issue is about. - -## Why it is not free to answer - -The record is a fixed line rendered by `toyos-quiesce` and read back by -`tests/metal/src/lib.rs` and the harness. -Naming a thread means the record carries a pid, a tid and the class of wait -that thread was in — which the sweep does not collect today, because it counts -rather than remembers. - -## What would show it - -The record naming the last thread to stop, and a boot of `tests/images/metalcase.toml` -whose stop time can then be attributed to it rather than inferred. Until the -record names one, its `in N ms` explains nothing about itself. diff --git a/issues/the-quiesce-last-staging-dies-on-a-guest-clock-deadline.md b/issues/the-quiesce-last-staging-dies-on-a-guest-clock-deadline.md deleted file mode 100644 index 42f4bd70799..00000000000 --- a/issues/the-quiesce-last-staging-dies-on-a-guest-clock-deadline.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-30 ---- - -# The `quiesce-last-*` staging dies on a guest-clock deadline, `kernel/src/quiesce.rs`'s `last::STAGED` - -`hold` panics the boot when the stop has not counted the held thread alone -within it, and `await_the_held_thread` panics when no thread named -`quiesce-last` has reached its syscall within it. A QEMU test's only clock is the harness's -ceiling: a guest slower than the deadline dies with a staging panic that names -no defect. - -Owner: orchestrator. Exit condition: both sides of the staging wait on the -other's event with no deadline, and a staging that never arrives is the -harness's ceiling. diff --git a/issues/the-scheduler-explorer-never-generates-a-stop.md b/issues/the-scheduler-explorer-never-generates-a-stop.md deleted file mode 100644 index 868732b893a..00000000000 --- a/issues/the-scheduler-explorer-never-generates-a-stop.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-25 ---- - -# The scheduler's interleaving explorer never generates a stop - -`kernel/sim` explores interleavings of blocks, wakes, migrations and -teardowns across CPUs, but no `Op` it generates marks a task `STOP`. The machine's -stop adds three routes into the per-CPU `stopped` band: - -- `TaskShared::stop_if_blocked`'s CAS against a parked task; -- `SchedPass::dispose_stop` at a running task's Ring 3 boundary; -- `CpuSched::place` banding a woken or adopted task that already carries the - mark. - -Each of them races a cross-CPU wake, an adopt in transit or a retire. The only -coverage is the single-CPU unit tests in `kernel/pure/sched/cpu.rs` and -`kernel/pure/sched/task.rs`, which drive each route once and in order. The sim's -invariants include I1, every task in exactly one container. None of them has -been checked with a stopped task in the machine. - -**Exit condition**: the explorer generates `stop_if_blocked` and a stop at a -running task's safe point, interleaved with its existing `Block`, `Wake` and -`Teardown` ops, and I1 holds across every schedule it runs. diff --git a/issues/virt-mask-windows-finds-a-mask-with-no-hook-in-a-lower-el-data-abort.md b/issues/virt-mask-windows-finds-a-mask-with-no-hook-in-a-lower-el-data-abort.md new file mode 100644 index 00000000000..b41b4f620ad --- /dev/null +++ b/issues/virt-mask-windows-finds-a-mask-with-no-hook-in-a-lower-el-data-abort.md @@ -0,0 +1,39 @@ +--- +status: open +kind: defect +opened: 2026-11-11 +--- + +# `virt_mask_windows` finds a mask with no hook in a lower-EL data abort + +The aarch64 mask-windows kernel panics during `unmap_touch`'s fault, before +any stop begins: + +``` +panic at kernel/src/windows.rs:79:5: data abort from a lower EL +DOUBLE PANIC: the cpu was already in PageFault +``` + +A diagnostic build, whose witness logs what it refused before its panic, +named the refusal `cpu0 found interrupts masked with no window open: a mask +reached no hook`, with the machine's stop not begun. So some path from a +lower-EL data abort masks interrupts without the witness's hook. + +On main without any branch, guest-rows was red at 422915e2c (run +43027396712) and green at 34cb4c918 (run 43026252506). It was red twice on +an experiment off 422915e2c that changed only the machine's stop; the +diagnostic line is from the second of those runs. It was green once at a +branch off 34cb4c918 (run 43030668596). Logs: +https://github.com/ToyOSOrg/evidence/tree/cf4270976c96690090ad2a3ac8fb4e406e5d49e4 +(`guest-main-*.log`, `guest-experiment-*.log`, `guest-rows-*.log`). + +Every red ran 422915e2c's code and every green later code: 34cb4c918 +descends from 422915e2c and adds kernel changes (#1204: the IPC syscalls, +handle objects, the loader's start of a process). So these runs cannot tell +an intermittent defect from one fixed in between. The first step is to repeat +the row at 422915e2c and at current main. + +**Exit**: the mask that reaches no hook is found and given one, and +`virt_mask_windows` is green on repeated runs; or repeats at 422915e2c red +and at current main stay green, a change in between is shown to have fixed +it, and this file is deleted. diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 0889eacac62..d7b8d158fed 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -104,9 +104,6 @@ notify-flag-load-only = [] # `pure/sched/cpu.rs`'s steal probe rings its victim before it sets the # thief's bit, and `kernel/loom/tests/loom_sleep.rs` must red. ask-rings-first = [] -# The two `SeqCst` fences of `pure/sched/watch.rs`'s `Gate` go, and -# `kernel/loom/tests/loom_watch.rs` must red — the stop's store-buffering pair. -gate-fence-off = [] # The staleness test's negative control: `pure/sched/cpu.rs`'s # `CpuHandle::answering` answers `true` for every CPU under it, which is what # every path that chooses a CPU believed before it existed. `kernel-sim`'s @@ -408,7 +405,6 @@ toyos-rootimage = { path = "../lib/rootimage" } # Its own image's ranges and symbols and the supervisor's program, as the # build laid them out: the kernel parses no ELF. toyos-kimage = { path = "../lib/kimage" } -toyos-quiesce = { path = "../lib/quiesce" } toyos-random = { path = "../lib/random" } toyos-symbols = { path = "../lib/symbols" } toyos-userbound = { path = "../lib/userbound" } diff --git a/kernel/loom/Cargo.toml b/kernel/loom/Cargo.toml index f9d8c0a3446..f62c170126f 100644 --- a/kernel/loom/Cargo.toml +++ b/kernel/loom/Cargo.toml @@ -269,13 +269,6 @@ commit-ignores-notify = [] # cargo test -p kernel-loom --features notify-flag-load-only --test loom_watch # notify-flag-load-only = [] -# The negative control for the stop's gate. It removes the two `SeqCst` fences -# of `watch.rs`'s `Gate`, so a poster's transition and the stop's sweep may -# each miss the other, and `loom_watch.rs` must red: -# -# cargo test -p kernel-loom --features gate-fence-off --test loom_watch -# -gate-fence-off = [] # The control for `poll_racing`'s lost-completion half: its producer posts, then # stores the readiness, the reverse of the order `pcidev::note_fault` keeps, so # both `*_racing_a_post*_completes_exactly_once` models must red with a poll diff --git a/kernel/loom/tests/loom_sleep.rs b/kernel/loom/tests/loom_sleep.rs index 7bddea8f018..49c5cd3c850 100644 --- a/kernel/loom/tests/loom_sleep.rs +++ b/kernel/loom/tests/loom_sleep.rs @@ -30,6 +30,9 @@ //! makes that read-modify-write relaxed and this file must red, at //! [`a_halted_cpu_with_queued_work_was_kicked`], whose assertion names the //! failure exactly: halted with messages still queued and no IPI in flight. +//! A demanded pass rests on the same edge, and +//! [`a_pass_that_answers_a_demand_reads_what_its_demander_wrote`] must red +//! with it. #![cfg(feature = "loom")] @@ -204,3 +207,31 @@ fn an_ask_is_taken_by_the_pass_its_ring_owes() { assert!(taken(victim.take_asks()), "the pass the ask's ring owed did not find it"); }); } + +/// The machine's stop against one CPU's pass. The stop writes its gate, then +/// demands a pass; once it reads the demand answered, what that CPU reads +/// after its pass is the gate open. The gate is relaxed on both sides, so the +/// doorbell's edge alone carries it. +#[test] +fn a_pass_that_answers_a_demand_reads_what_its_demander_wrote() { + model(|| { + let gate = Arc::new(AtomicBool::new(false)); + let doorbell = Arc::new(Doorbell::new()); + + let stop = { + let (gate, doorbell) = (gate.clone(), doorbell.clone()); + loom::thread::spawn(move || { + gate.store(true, Ordering::Relaxed); + let _ = doorbell.demand_pass(); + !doorbell.pass_demanded() + }) + }; + + doorbell.begin_pass(); + let open = gate.load(Ordering::Relaxed); + + if stop.join().unwrap() { + assert!(open, "a pass answered the demand and read the gate closed after it"); + } + }); +} diff --git a/kernel/loom/tests/loom_watch.rs b/kernel/loom/tests/loom_watch.rs index 3417dce4e22..3a327f0edb0 100644 --- a/kernel/loom/tests/loom_watch.rs +++ b/kernel/loom/tests/loom_watch.rs @@ -18,11 +18,9 @@ //! cargo test -p kernel-loom --features commit-ignores-notify --test loom_watch //! ``` //! -//! Four more controls: `notify-flag-load-only` lets a post that finds its bits +//! Three more controls: `notify-flag-load-only` lets a post that finds its bits //! already set answer off a load, and //! `a_second_post_is_not_lost_to_a_flag_the_waiter_consumed` must red; -//! `gate-fence-off` removes the [`Gate`]'s two fences, and -//! `a_transition_racing_an_opening_gate_is_never_missed` must red; //! `poll-fire-load-store`, the kernel's own control for the poll's one-shot //! answer, which the ring models below compile, must red every //! `*_completes_exactly_once` model here; and `fault-posted-before-it-is-set` @@ -38,7 +36,6 @@ //! compiled here, so the models count answers instead of writing them. //! //! [`TaskShared::notify`]: kernel_loom::sched::task::TaskShared::notify -//! [`Gate`]: kernel_loom::sched::watch::Gate //! [`Once`]: once::Once //! [`prepare`]: kernel_loom::sched::park::prepare //! [`Ring::fire`]: kernel_loom::sched::watch::Ring::fire @@ -62,7 +59,7 @@ use kernel_loom::sched::task::{ Claim, Refused, TaskKey, TaskShared, TaskState, WaitClass, WakeCause, WakeReason, }; use kernel_loom::sched::sync::CellLock; -use kernel_loom::sched::watch::{Fire, Gate, Poster, Ring, Waiters, Watch}; +use kernel_loom::sched::watch::{Fire, Poster, Ring, Waiters, Watch}; use kernel_loom::{inbox_polls as polls, poll_once as once}; @@ -541,76 +538,6 @@ fn a_revoke_racing_the_wait_loop_ends_it_in_every_arm() { }); } -/// **The machine's stop against one thread's park**, the gate included. The -/// stop opens its [`Gate`], registers on its watch and sweeps the thread's -/// word, parking if the thread is still running; the thread parks — the -/// transition the stop waits for — and then posts only if it finds the gate -/// open, which is `quiesce::note_progress`. Either the sweep sees the park or -/// the post reaches the stop: a stop parked over a thread that has parked, -/// with no post owed to it, sleeps out its whole budget. -#[test] -fn a_transition_racing_an_opening_gate_is_never_missed() { - model(|| { - let (world, mut rx) = world(); - let gate = Arc::new(Gate::new(0)); - let stopper = task(1); - let thread = Arc::new(TaskShared::::new(TaskKey(2), TaskState::Running(CPU1), CpuMask::ANY)); - - let poster = { - let world = world.clone(); - let gate = gate.clone(); - let thread = thread.clone(); - loom::thread::spawn(move || { - let ticket = - prepare(&CurrentTask::new(&thread, CPU1), Cancel::Answers, WaitClass::Other) - .expect("nothing posts to the stopped thread"); - assert!(matches!(ticket.commit(), Commit::Parked(_))); - if gate.after_write() != 0 { - world.post(); - } - }) - }; - let stop = { - let world = world.clone(); - let stopper = stopper.clone(); - loom::thread::spawn(move || { - gate.open(1); - world.watch.register(&stopper, 0); - // One post ends at most one iteration. - for _ in 0..3 { - if thread.stop_pending() || thread.stop_if_blocked() { - return false; - } - let Ok(ticket) = - prepare(&CurrentTask::new(&stopper, CPU0), Cancel::Ignores, WaitClass::Other) - else { - continue; - }; - match ticket.commit() { - Commit::Parked(_) => return true, - Commit::AlreadyWoken => continue, - Commit::Killed => unreachable!("an uncancellable wait"), - } - } - unreachable!("one post ended more than two iterations") - }) - }; - - let parked = stop.join().unwrap(); - poster.join().unwrap(); - let msgs = drain(&mut rx, &world.preempt); - - if parked { - assert_eq!( - msgs, - [Msg::Wake(TaskKey(1), WakeReason::Woken)], - "the stop parked over a thread that had parked, and nothing posted it", - ); - } - world.watch.unregister(&stopper); - }); -} - /// `inbox::process_watch`'s own sequence for one poll on two watches: admitted /// armed, withdrawing the older poll on the same handle, added to the object's /// read watch and then its write watch with no lock held between, then diff --git a/kernel/pure/sched/cpu.rs b/kernel/pure/sched/cpu.rs index 4e55c872d12..a53e8403172 100644 --- a/kernel/pure/sched/cpu.rs +++ b/kernel/pure/sched/cpu.rs @@ -279,13 +279,15 @@ pub struct CpuSched { /// so the bound above would be false for k > 1, which is the only k that /// makes it a bound at all. dying: VecDeque>, - /// Tasks carrying [`TaskShared::stop_pending`], which no pick serves. + /// Tasks the machine's stop banded at their Ring 3 boundary + /// ([`SchedPass::dispose_stop`]), which no pick serves. /// - /// **A band and not a queue**: nothing is ever taken out of it, so its - /// order carries no decision. It exists so that a task the machine has - /// stopped still has somewhere to *be* — invariant I1 wants every task - /// value in exactly one container, and a stopped task still owns a kernel - /// stack and an address space that only a reset will release. + /// **A band and not a queue**: nothing is ever taken out of it and no + /// message finds a task in it, so its order carries no decision. It + /// exists so that a task the machine has stopped still has somewhere to + /// *be* — invariant I1 wants every task value in exactly one container, + /// and a stopped task still owns a kernel stack and an address space that + /// only a reset will release. /// /// In no fair band, as [`Self::dying`]'s tasks are not; excluded from the /// published load, because nothing queues behind work that will never run. @@ -1075,16 +1077,6 @@ impl CpuSched { env: Env<'_, H, P>, now: Nanos, ) { - // **Ahead of the RT forward and of the kill check, because it outranks - // both.** This is the one route by which a stopped task can be offered - // to a CPU again — a wake, an adopt, or a retire that claimed it out of - // `parked` — and the band it goes to is the whole of the guarantee that - // it takes no further instruction. Forwarding it to a sibling or - // dispatching it to unwind would each undo that. - if task.shared().stop_pending() { - self.keep_stopped(task); - return; - } task.refresh_affinity(); let mask = task.affinity(); // A corpse unwinds where it is, as `hand_off` keeps it. @@ -1139,8 +1131,8 @@ impl CpuSched { self.dying.push_back(Corpse { since: now, task }); } - /// Band a stopped task: one that reached this CPU stopped, or the running - /// task at its own safe point. Like a dying task it is in no fair band. + /// Band the running task at its own safe point. Like a dying task it is + /// in no fair band. fn keep_stopped(&mut self, mut task: ReadyTask) { task.end_lend(); self.stopped.push_back(task); @@ -1254,7 +1246,8 @@ impl CpuSched { // which means an `Adopt` is on its way here: re-posting would spin // against the producer, and the sticky kill bit already guarantees the // adopter *dispatches* it on arrival, into its own dying list, where it - // unwinds and dies by its own `die`. + // unwinds and dies by its own `die`. A task in this CPU's stopped band + // names this CPU too, and stays in the band. match shared.state() { TaskState::Dead => {} state if home_of(state) == Some(self.id) => {} @@ -1744,11 +1737,9 @@ impl<'c, 'e, H: Hw, P: PreemptGuard> SchedPass<'c, 'e, H, P, Undisposed> { /// The current task has reached the point it may never run past, and is /// banded where no pick will find it. Does not return to the task. /// - /// **The mark is taken here and nowhere else for a running task.** A task - /// that is running may be holding a kernel lock anywhere below this frame; - /// only the caller knows it is standing somewhere it holds nothing, and - /// this is where that knowledge is spent. Everything else a stopped task - /// meets — a wake, an adopt, a retire — reads the bit in `place`. + /// A task that is running may be holding a kernel lock anywhere below + /// this frame; only the caller knows it is standing somewhere it holds + /// nothing. pub fn dispose_stop(mut self) -> SchedPass<'c, 'e, H, P, Disposed> { let current = self .cpu @@ -1757,7 +1748,6 @@ impl<'c, 'e, H: Hw, P: PreemptGuard> SchedPass<'c, 'e, H, P, Undisposed> { .expect("dispose_stop without a running task"); let mut task = current.preempt(self.cpu.id, self.now); self.cpu.rq.leave_running(task.key(), task.standing_mut()); - task.shared().mark_stop(); self.cpu.keep_stopped(task); self.settle(); self.dispose() @@ -3430,7 +3420,7 @@ mod tests { #[test] fn a_stopped_task_is_never_picked_again() { let mut w = World::new(1); - let (stopped, stopped_shared) = w.spawn(C0); + let (stopped, _stopped_shared) = w.spawn(C0); w.run_a_pass(C0); assert_eq!(w.cpus[0].running().map(|t| t.key()), Some(stopped)); let (other, _) = w.spawn(C0); @@ -3441,7 +3431,6 @@ mod tests { let _ = pass.dispose_stop().finish(); } - assert!(stopped_shared.stop_pending(), "the safe point takes the mark"); assert_eq!(w.cpus[0].stopped().count(), 1); assert_eq!(w.cpus[0].stopped[0].key(), stopped); assert_eq!( @@ -3463,80 +3452,24 @@ mod tests { w.abandon(); } - /// **The wake is the hole the band exists to close.** A userland thread - /// blocked in a syscall when the machine is stopped is marked where it - /// stands; what must never happen is that its deadline or a device wake - /// puts it back in the run queue, where a pick would dispatch it to finish - /// that syscall — and log — after the boot's last word. - /// - /// Deleting `place`'s stop arm leaves it in `rq`, running on the next pass, - /// which is exactly what this asserts against. - #[test] - fn a_wake_for_a_stopped_task_lands_in_the_band_and_not_the_run_queue() { - let mut w = World::new(1); - let (parked, parked_shared) = w.spawn(C0); - w.run_a_pass(C0); - assert_eq!(w.cpus[0].running().map(|t| t.key()), Some(parked)); - w.park_running(C0); - assert_eq!(parked_shared.state(), TaskState::Blocked(C0)); - - assert!(parked_shared.stop_if_blocked(), "a parked task takes the mark"); - assert!(parked_shared.stop_if_blocked(), "and idempotently"); - - // The waker comes after, as it does on a machine being stopped: a - // deadline or a device wake finding a thread the park already marked. - assert_eq!(parked_shared.claim_wake(), Claim::Parked(C0)); - assert!( - parked_shared.stop_pending(), - "the claim preserves the sticky mark", - ); - w.post_claimed_wake(C0, &parked_shared, WakeReason::Woken); - w.run_a_pass(C0); - - assert_eq!(w.cpus[0].stopped().count(), 1, "the wake reached the band"); - assert_eq!(w.cpus[0].stopped[0].key(), parked); - assert!(w.cpus[0].rq.is_empty(), "and never the run queue"); - assert!( - w.cpus[0].running().is_none(), - "so no pick could dispatch it", - ); - w.abandon(); - } - - /// A task that is *running* may hold a kernel lock anywhere below its - /// current frame, so the parked mark refuses it: it reaches the band - /// through its own safe point or not at all. - #[test] - fn a_running_task_refuses_the_parked_mark() { - let mut w = World::new(1); - let (key, shared) = w.spawn(C0); - assert_eq!(shared.state(), TaskState::Ready(C0)); - assert!(!shared.stop_if_blocked(), "ready is not parked"); - w.run_a_pass(C0); - assert_eq!(w.cpus[0].running().map(|t| t.key()), Some(key)); - assert!(!shared.stop_if_blocked(), "and neither is running"); - assert!(!shared.stop_pending(), "nothing was marked"); - w.abandon(); - } - - /// A task carrying both marks goes to the band and not the dying list. - /// Dispatching it to unwind is what the stop exists to prevent: the unwind - /// is what writes the `exit:` record this whole path is about. + /// A kill of a banded task finds it in no container a retire serves, so + /// it is never dispatched to unwind and stays where no pick finds it. #[test] - fn stopping_outranks_killing() { + fn a_retire_for_a_stopped_task_leaves_it_in_the_band() { let mut w = World::new(1); - let (key, shared) = w.spawn(C0); + let (stopped, shared) = w.spawn(C0); w.run_a_pass(C0); - w.park_running(C0); - assert!(shared.stop_if_blocked()); - shared.mark_kill(); + { + let (cpus, env) = w.split(); + let pass = SchedPass::begin(&mut cpus[0], env, NOW); + let _ = pass.dispose_stop().finish(); + } - assert_eq!(shared.claim_wake(), Claim::Parked(C0)); - w.post_claimed_wake(C0, &shared, WakeReason::Woken); + crate::sched::retire::begin(&shared).post(&w.handles, &w.hw, &NoPreempt); w.run_a_pass(C0); assert_eq!(w.cpus[0].stopped().count(), 1); - assert_eq!(w.cpus[0].stopped[0].key(), key); + assert_eq!(w.cpus[0].stopped[0].key(), stopped); assert_eq!(w.cpus[0].dying().count(), 0, "never dispatched to unwind"); assert!(w.cpus[0].running().is_none()); w.abandon(); diff --git a/kernel/pure/sched/invariants.rs b/kernel/pure/sched/invariants.rs index 71cbf9f314b..3328663af91 100644 --- a/kernel/pure/sched/invariants.rs +++ b/kernel/pure/sched/invariants.rs @@ -104,11 +104,6 @@ pub fn check_cpu(cpu: &CpuSched) { "stopped task {:?} disagrees with its state word", task.key(), ); - assert!( - task.shared().stop_pending(), - "a live task {:?} is in the stopped band", - task.key(), - ); } for task in cpu.rq().tasks() { @@ -118,11 +113,6 @@ pub fn check_cpu(cpu: &CpuSched) { "ready task {:?} disagrees with its state word", task.key(), ); - assert!( - !task.shared().stop_pending(), - "stopped task {:?} is in the run queue, where a pick will serve it", - task.key(), - ); } for parked in cpu.parked() { diff --git a/kernel/pure/sched/mailbox.rs b/kernel/pure/sched/mailbox.rs index 3c74849c876..4ba79bf5950 100644 --- a/kernel/pure/sched/mailbox.rs +++ b/kernel/pure/sched/mailbox.rs @@ -439,6 +439,8 @@ pub enum Kick { const KICK_PENDING: u32 = 1 << 0; const SLEEPING: u32 = 1 << 1; +/// A pass was demanded and none has begun since ([`Doorbell::demand_pass`]). +const PASS_DEMANDED: u32 = 1 << 2; /// The ordering [`Doorbell::ring`]'s read-modify-write carries. /// @@ -495,12 +497,31 @@ impl Doorbell { } } + /// Raise the edge with a mark only this CPU's next pass clears, for a + /// caller that waits on [`Self::pass_demanded`] to know a pass began after + /// this call. That pass's acquire reads this release, so what the CPU + /// reads after it follows everything the caller wrote before. + pub fn demand_pass(&self) -> Kick { + let prev = self.bits.fetch_or(KICK_PENDING | PASS_DEMANDED, KICK); + if prev & KICK_PENDING == 0 { + Kick::Send + } else { + Kick::Elide + } + } + + /// Whether a demanded pass has yet to begin. + pub fn pass_demanded(&self) -> bool { + self.bits.load(Ordering::Acquire) & PASS_DEMANDED != 0 + } + /// Consumer side, at pass start: clear the edge *before* draining, so a /// message posted after the drain re-raises it. Also clears - /// SLEEPING — a CPU running a pass is by definition awake. + /// SLEEPING — a CPU running a pass is by definition awake — and answers + /// a demand for this pass. pub fn begin_pass(&self) { self.bits - .fetch_and(!(KICK_PENDING | SLEEPING), Ordering::AcqRel); + .fetch_and(!(KICK_PENDING | SLEEPING | PASS_DEMANDED), Ordering::AcqRel); } /// Consumer side, idle path: publish SLEEPING *before* the final @@ -670,6 +691,23 @@ mod tests { assert!(!d.kick_pending() && !d.sleeping()); } + /// A demand stands until a pass begins, whoever else rings meanwhile. + #[test] + fn a_demanded_pass_is_answered_by_the_next_pass_and_nothing_else() { + let d = Doorbell::new(); + assert!(!d.pass_demanded()); + assert_eq!(d.demand_pass(), Kick::Send); + assert_eq!(d.ring(Urgency::Normal), Kick::Elide, "the demand raised the edge"); + d.arm_sleep().abandon(); + assert!(d.pass_demanded()); + d.begin_pass(); + assert!(!d.pass_demanded()); + // A demand behind an edge already raised sends nothing and still stands. + assert_eq!(d.ring(Urgency::Normal), Kick::Send); + assert_eq!(d.demand_pass(), Kick::Elide); + assert!(d.pass_demanded()); + } + #[test] fn sleep_needs_an_empty_mailbox_and_a_quiet_doorbell() { let (tx, mut rx) = mailbox::(); diff --git a/kernel/pure/sched/task.rs b/kernel/pure/sched/task.rs index d2d7d4a6209..c1fac1c2dbe 100644 --- a/kernel/pure/sched/task.rs +++ b/kernel/pure/sched/task.rs @@ -167,18 +167,7 @@ const GEN_MASK: u64 = (1 << GEN_BITS) - 1; const KILL: u64 = 1 << 62; /// Sticky: exactly one retirer may post the retire node. const RETIRE_QUEUED: u64 = 1 << 63; -/// Sticky: the task has reached a point it may never run past again. Every -/// route that would make it runnable puts it in its CPU's stopped band -/// instead, and no pick serves that band — so it executes no further -/// instruction in either ring, and nothing clears this. -/// -/// **The opposite of [`KILL`] and not a variant of it.** A killed task is -/// dispatched *sooner* so it can unwind and release what a retirer waits on; -/// a stopped one is never dispatched at all, because the machine is going away -/// and what it would do on the way out is the thing being prevented. Where a -/// task carries both, [`SafePoint`] is where that is decided. -const STOP: u64 = 1 << 61; -const STICKY: u64 = KILL | RETIRE_QUEUED | STOP; +const STICKY: u64 = KILL | RETIRE_QUEUED; /// A post reached this task while it was neither parked nor committing. The /// next [`TaskShared::begin_commit`] consumes it and refuses the park, so a post /// landing between a waiter's registration on a watch and its commit is a @@ -197,7 +186,9 @@ const KEPT: u64 = STICKY | NOTIFIED | REVOKED; /// userland. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum SafePoint { - /// Band it where it stands; it takes [`STOP`] here. + /// Band it where it stands, in its CPU's stopped band, which no pick + /// serves: the opposite of [`KILL`], whose task is dispatched sooner so it + /// can unwind. Stop, /// Let it unwind and die: [`KILL`]. Exit, @@ -413,19 +404,12 @@ impl TaskShared { self.state.load(Ordering::Acquire) & RETIRE_QUEUED != 0 } - pub fn stop_pending(&self) -> bool { - self.state.load(Ordering::Acquire) & STOP != 0 - } - - /// **The one rank of the two marks**, read by the kernel's Ring 3 boundary - /// and applied to a task that is not running by `CpuSched::place`: - /// [`SafePoint::Stop`] outranks [`SafePoint::Exit`], because the unwind a - /// killed task is dispatched for is what writes the record the machine's - /// stop exists to keep out from under the boot's last word. + /// **The one rank of the two**, read by the kernel's Ring 3 boundary: + /// [`SafePoint::Stop`] outranks [`SafePoint::Exit`], since a thread the + /// machine's stop names runs no further, its own end included. /// - /// `stopping` is a parameter and the kill is the word's: a running task may - /// not carry [`STOP`] while it might still hold a kernel lock, so it takes - /// that mark at this boundary and never before. + /// `stopping` is a parameter and the kill is the word's: a task is + /// stopped only at this boundary, where it holds no kernel lock. pub fn at_safe_point(&self, stopping: bool) -> Option { if stopping { return Some(SafePoint::Stop); @@ -433,39 +417,6 @@ impl TaskShared { self.kill_pending().then_some(SafePoint::Exit) } - /// Stop a task that is parked, and answer whether it now carries [`STOP`]. - /// - /// **One CAS, because the read and the mark may not come apart.** A task - /// that is running may hold a kernel lock, so marking it where it stands - /// would band it holding that lock; a running task reaches this bit at its - /// own safe point instead, through `SchedPass::dispose_stop`. `false` here - /// therefore means "not parked, and not this caller's to stop" — including - /// the task a waker claimed between the read and the exchange, which is on - /// its way to a CPU that will dispatch it to that safe point. - /// - /// Idempotent: a task already carrying the bit answers `true` without - /// writing. - pub fn stop_if_blocked(&self) -> bool { - let mut cur = self.state.load(Ordering::Acquire); - loop { - if cur & STOP != 0 { - return true; - } - if !matches!(unpack(cur), TaskState::Blocked(_)) { - return false; - } - match self.state.compare_exchange_weak( - cur, - cur | STOP, - Ordering::AcqRel, - Ordering::Acquire, - ) { - Ok(_) => return true, - Err(now) => cur = now, - } - } - } - /// Move the word from `from` to `to`, preserving the sticky bits. /// `false` means the word was no longer `from` — the caller lost a race /// and must re-read. @@ -681,14 +632,6 @@ impl TaskShared { pub fn mark_kill(&self) { self.state.fetch_or(KILL, Ordering::AcqRel); } - - /// Sticky [`STOP`] on a task that is *running on the calling CPU* and has - /// reached its safe point; `SchedPass::dispose_stop` is the only caller, - /// which is what makes the unconditional `fetch_or` sound where - /// [`Self::stop_if_blocked`]'s CAS is not. - pub(crate) fn mark_stop(&self) { - self.state.fetch_or(STOP, Ordering::AcqRel); - } } // The linear task value and its five lifecycle types @@ -1316,8 +1259,8 @@ mod tests { } /// The kernel's Ring 3 boundary reads this and nothing else, so a boundary - /// that ranked the two marks the other way would dispatch a killed thread - /// to unwind — and log — on a machine that is stopping. + /// that ranked the two the other way would dispatch a killed thread to + /// unwind on a machine that is stopping. #[test] fn stopping_outranks_killing_at_a_safe_point() { let s = running(C0); diff --git a/kernel/pure/sched/watch.rs b/kernel/pure/sched/watch.rs index 83a93437179..107153651e1 100644 --- a/kernel/pure/sched/watch.rs +++ b/kernel/pure/sched/watch.rs @@ -39,7 +39,7 @@ use crate::sched::cpu::CpuHandles; use crate::sched::hw::Kicker; use crate::sched::mailbox::{PreemptGuard, SchedMsg}; use crate::sched::park::{notify, revoke, Deferred}; -use crate::sched::sync::{fence, Arc, AtomicU32, CellLock, Ordering}; +use crate::sched::sync::{Arc, CellLock}; use crate::sched::task::{TaskShared, WakeCause}; /// Why a ring entry is posted. @@ -330,59 +330,6 @@ fn take_dead(rings: &mut Vec, dead: &mut [Option; FEW]) -> usize taken } -/// A word in front of a watch that its posters read to learn whether a post is -/// owed at all, for a waiter whose condition is a sweep of words the posters -/// write: the machine's stop, whose posters are every thread's park, band and -/// exit, and whose watch no poster may take a lock for until a stop begins. -/// -/// **Each side is a store followed by a load of another location**: the waiter -/// opens the gate and then sweeps the posters' words; a poster writes its own -/// word and then reads the gate. So each side puts a `SeqCst` fence between the -/// two, and either the poster sees the gate open and posts, or the sweep sees -/// the poster's write. Without the fences both may read the old value, which -/// x86's locked read-modify-writes hide and ARM64 does not. -/// `loom_watch`'s `a_transition_racing_an_opening_gate_is_never_missed` is the -/// model and `gate-fence-off` its control. -pub struct Gate(AtomicU32); - -impl Gate { - #[cfg(not(feature = "loom"))] - pub const fn new(value: u32) -> Self { - Self(AtomicU32::new(value)) - } - - // Loom's atomics have no const constructor, so this arm alone drops `const`. - #[cfg(feature = "loom")] - pub fn new(value: u32) -> Self { - Self(AtomicU32::new(value)) - } - - /// The waiter's side: publish `value`, then sweep. - pub fn open(&self, value: u32) { - self.0.store(value, Ordering::Release); - gate_fence(); - } - - /// The poster's side, after the write its waiter sweeps for. - pub fn after_write(&self) -> u32 { - gate_fence(); - self.0.load(Ordering::Acquire) - } - - /// A reader that owes nobody a post. - pub fn read(&self) -> u32 { - self.0.load(Ordering::Acquire) - } -} - -fn gate_fence() { - // `gate-fence-off` is the negative control: without the fence either side - // may read the other's old value, and `loom_watch` reds. - if cfg!(not(feature = "gate-fence-off")) { - fence(Ordering::SeqCst); - } -} - /// An object that ends with polls still registered answers them: dropping a /// watch fires every live ring entry as [`Fire::Gone`]. impl>> Drop for Watch { diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index 4af4218ed80..69d53dfe2be 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -52,9 +52,6 @@ actuators! { /// name. Judged by `isa_claim_refused_on_a_kernel_port`. isa_port_declared = "isa-port-declared"; - /// Give the machine's stop no time: one sweep, and a thread not parked at - /// it outlasts the stop. Judged by `machine_shutdown_short_stop`. - stop_budget_spent = "stop-budget-spent"; /// Send one NMI from the idle loop, and return from its handler via `iretq` with a second NMI already pending. nmi_nested = "nmi-nested"; diff --git a/kernel/src/arch/x86_64/pio.rs b/kernel/src/arch/x86_64/pio.rs index 360c9d4d4f8..e5a7cc0fc6a 100644 --- a/kernel/src/arch/x86_64/pio.rs +++ b/kernel/src/arch/x86_64/pio.rs @@ -140,13 +140,10 @@ pub fn mediated(passed: &PortAt) -> Port { /// Every row's ports, the kernel's again: the power-off's, and nobody else's. pub struct TakenBack(()); -/// Take every row's ports back from whoever holds them, whatever the stop's -/// record says. Once `stopping` exists a thread enters Ring 3 only past -/// `scheduler::leave_user_if_due`, which stops all but the stop's caller; the -/// shootdown returns only once every other CPU has answered it from Ring 0, -/// so no thread that was in Ring 3 before is there still. +/// Every row's ports are the kernel's again, with nothing to undo: `stopping` +/// says no thread but the stop's caller is in Ring 3 or returns to it, so no +/// holder reaches its row. pub fn take_back(_stopping: &crate::quiesce::Stopping) -> TakenBack { - super::tlb::shootdown(crate::invalidation::Origin::Stop, crate::invalidation::Half::User); TakenBack(()) } diff --git a/kernel/src/invalidation.rs b/kernel/src/invalidation.rs index 1428b497497..004dc6c6458 100644 --- a/kernel/src/invalidation.rs +++ b/kernel/src/invalidation.rs @@ -7,8 +7,7 @@ /// CPU's own stack's guard page taken out of the direct map at bring-up), /// `Stack` (retired thread-stack slots made clean for reuse), `Staged` (a test actuator's), `Bench` /// (`arch::tlb::bench`'s own, so a measured shootdown is never counted as one -/// a path in this kernel needed), `Stop` (the power-off's, for the answer from -/// Ring 0 every other CPU owes it, not for a flush). +/// a path in this kernel needed). #[derive(Clone, Copy)] #[repr(usize)] pub enum Origin { @@ -21,9 +20,6 @@ pub enum Origin { Staged, #[cfg_attr(not(feature = "boot-actuators"), allow(dead_code))] Bench, - // Ports are x86-64's alone, so AArch64 builds a variant it never issues. - #[allow(dead_code)] - Stop, } /// Which half of every address space a shootdown's write changed: x86-64's @@ -38,8 +34,8 @@ pub enum Half { } impl Origin { - pub const COUNT: usize = 8; + pub const COUNT: usize = 7; /// Order matches the variants. pub const NAMES: [&'static str; Self::COUNT] = - ["pcid", "mmio", "unmap", "guard", "stack", "staged", "bench", "stop"]; + ["pcid", "mmio", "unmap", "guard", "stack", "staged", "bench"]; } diff --git a/kernel/src/log/console.rs b/kernel/src/log/console.rs index 8bc057d3212..c977a60aa66 100644 --- a/kernel/src/log/console.rs +++ b/kernel/src/log/console.rs @@ -16,8 +16,7 @@ //! **One writer at a time**: the boot's, whose drains take turns through a //! claim on the wire, which the hand-over takes for good; then the holder's; //! then the stop's, once userland is stopped, or a fatal path's, once every -//! other CPU is. A holder the stop did not stop within its budget, which the -//! stop's record names, may still write over it. +//! other CPU is. use core::sync::atomic::{AtomicBool, Ordering}; @@ -119,7 +118,7 @@ impl StopWire { /// The UART, for the stop once it has stopped userland: from a line of its /// own, since the holder may have been stopped mid-line or behind the log. -pub fn take_for_the_stop() -> StopWire { +pub fn take_for_the_stop(_stopping: &crate::quiesce::Stopping) -> StopWire { after_the_holder(serial::write); StopWire(()) } diff --git a/kernel/src/power.rs b/kernel/src/power.rs index 5bfb54e1b8f..af1292dcd72 100644 --- a/kernel/src/power.rs +++ b/kernel/src/power.rs @@ -41,10 +41,10 @@ pub fn reboot(mut wire: StopWire) -> ! { /// Reset the machine and do nothing else. /// /// **[`reboot`] is not reachable from a wedge**, which is why this exists -/// beside it: that path is the stop's, which waits on userland's stop and the -/// console's registers, and a `BackendGuard` masks interrupts for its whole -/// life — so a CPU stuck inside one holds what the stop would wait for, on -/// exactly the boots `crate::deadline` exists for. This takes no lock. +/// beside it: that path is the stop's, which waits for every other CPU's pass +/// and the console's registers, and a `BackendGuard` masks interrupts for its +/// whole life — so a CPU stuck inside one holds what the stop would wait for, +/// on exactly the boots `crate::deadline` exists for. This takes no lock. /// /// A machine with no reset halts here rather than returning: holding is what /// such a machine has always done. diff --git a/kernel/src/process.rs b/kernel/src/process.rs index 01b96fef2db..8ccbee7b36c 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -745,7 +745,7 @@ pub fn stats_from( } /// The last thread out's teardown of its process, on its own stack: free what the process holds, run every release its handles owed, then keep its exit until every end below it is published. Every other thread has left, so none can run in what is freed. -/// The thread is in the process until the last table section marks it, so a machine stop waits for every record and release here. +/// The thread is in the process until the last table section marks it. /// Waits on nothing: it runs on a killed thread whose one cancel may be spent, and at the exit boundary's preempt depth, where a park asserts. /// The section that marks it also lets the teardown's own hold on the publication go: the exit is published here when nothing below it is left, and otherwise by the last end below it. /// The supervisor's end, whatever ended it, ends the machine: nothing starts it again. @@ -772,8 +772,6 @@ fn teardown(pid: Pid, tid: Tid, end: End, mark: i32, process_data: &Arc crate::quiesce::note_progress(), + proclife::Leave::NotLast => {} proclife::Leave::Last { end, mark } => teardown(pid, tid, end, mark, &process_data), } } diff --git a/kernel/src/quiesce.rs b/kernel/src/quiesce.rs index d9549277d50..b797dde9b84 100644 --- a/kernel/src/quiesce.rs +++ b/kernel/src/quiesce.rs @@ -1,103 +1,60 @@ -//! Stopping this machine's userland, so the shutdown's claims about it are -//! claims about a machine that has stopped. +//! The machine's stop: once it has begun, no thread but its caller returns +//! to Ring 3. //! -//! Every decision here is [`toyos_quiesce`]'s; this module marks threads and -//! spends the time. +//! The kernel's last sequence takes hardware back from the programs it was +//! handed to, settles what they left in flight and cuts power, and no program +//! can vouch that nothing runs in Ring 3 across it; so the gate is here. +//! **The stop waits for no thread.** A thread inside a syscall, or parked in +//! one and woken later, runs on to the boundary, and what it logs may follow +//! the last word; what had to finish first is the caller's to establish +//! before it asks. //! -//! # Where the stop is taken, and why there +//! [`stops_this_thread`] is read by `scheduler::leave_user_if_due`, which +//! every return to Ring 3 passes: the syscall gate, every interrupt and trap +//! epilogue, and a task's first dispatch. A thread standing there holds no +//! kernel lock, and is banded where no pick serves it +//! (`SchedPass::dispose_stop`). //! -//! [`stops_this_thread`] is read by `scheduler::leave_user_if_due`, called -//! from `kernel_exit_to_user_check` — the one function every return to Ring 3 -//! in this kernel passes through: the syscall gate, every device interrupt, the -//! timer, the TLB shootdown IPI, the general trap epilogue and a task's first -//! dispatch. A thread standing there holds no kernel lock, has nothing in the -//! block layer and nothing in flight on any controller; it is between two -//! userland instructions, and every record this kernel writes with a userland -//! author is written from inside a syscall. -//! -//! # What keeps running -//! -//! **Tasks stop; CPUs do not.** Every CPU keeps `IF` set, keeps taking its -//! LAPIC timer and every device interrupt, and keeps taking scheduler passes — -//! it simply has no userland left to dispatch. That is what the USB stop below -//! the boot's last word needs. Freezing CPUs inside a pass instead would strand -//! whatever lock the thread on that CPU was holding. -//! -//! # What the stop waits on -//! -//! [`PROGRESS`], posted by [`note_progress`] from the three transitions that -//! turn a thread a sweep counted as running into one it does not: the pass -//! that bands it at its safe point, the pass that parks it, and its exit. Each -//! posts after its own transition, so the sweep it wakes finds it done; a -//! thread still running when the budget is spent is the record's shortfall. -//! -//! Lock order: [`process::PROCESS_TABLE`] alone. - -use core::sync::atomic::{AtomicBool, AtomicU32, Ordering::AcqRel, Ordering::Relaxed}; +//! **Tasks stop; CPUs do not.** Every CPU keeps taking interrupts and +//! scheduler passes; freezing one inside a pass would strand whatever lock +//! the thread on it held. -use toyos_quiesce::{must_stop, Record, Sweep, ThreadId}; -use kernel::sched::task::WaitClass; -use kernel::sched::watch::Gate; +use core::sync::atomic::{AtomicBool, AtomicU64, Ordering::AcqRel, Ordering::Acquire, Ordering::Release}; use crate::arch::percpu; -use crate::watch::{self, Watch}; -use crate::process; -use crate::time::{Budget, Deadline, Duration}; - -/// How long the machine gets to stop. -/// -/// **A budget, and not a bound the kernel can prove.** One `QUANTUM_NS` is -/// what a thread running in Ring 3 needs to reach the boundary, and as long -/// again covers one inside a syscall that does not park. A thread can -/// therefore outlast this, which is why its expiry is a clause in the record. -pub(crate) const PARK: Budget = Budget::of( - Duration::from_nanos(2 * kernel::sched::task::QUANTUM_NS), - "the reset lands wherever the threads that never reached a safe point are, and \ - the record names how many", -); - -const RUNNING: u32 = 0; -const STOPPING: u32 = 1; -/// Whether the machine is stopping. **The one word every other read here hangs -/// off**: it is opened last and read first, so a reader that sees it sees the -/// caller that goes with it. A [`Gate`], because [`note_progress`] reads it -/// after the transition [`stop`]'s sweep reads, and the stop opens it before -/// that sweep: the gate's fences are what keep both from reading stale. -static STAGE: Gate = Gate::new(RUNNING); +/// No thread: the stop has not begun. +const NOBODY: u64 = u64::MAX; -/// The thread running the stop, which never stops itself; no thread at all -/// until [`stop`] stores one, spelt as `percpu` spells idle. -static CALLER_PID: AtomicU32 = AtomicU32::new(u32::MAX); -static CALLER_TID: AtomicU32 = AtomicU32::new(u32::MAX); +/// The thread running the stop, which never stops itself, as +/// [`thread_word`] spells it; [`NOBODY`] until [`stop`] begins. +static CALLER: AtomicU64 = AtomicU64::new(NOBODY); -fn caller() -> ThreadId { - ThreadId { pid: CALLER_PID.load(Relaxed), tid: CALLER_TID.load(Relaxed) } +fn thread_word(pid: u32, tid: u32) -> u64 { + u64::from(pid) << 32 | u64::from(tid) } /// Whether the machine's stop names the running thread, for the one Ring 3 -/// boundary that ranks this against the kill mark. +/// boundary that ranks this against the kill mark. A sibling thread of the +/// caller's process stops like any other. pub fn stops_this_thread() -> bool { - stops(STAGE.read()) -} - -/// Whether this boot's stop has begun; once it has, it never ends. -pub fn begun() -> bool { - STAGE.read() == STOPPING -} - -fn stops(stage: u32) -> bool { - if stage != STOPPING { + let caller = CALLER.load(Acquire); + if caller == NOBODY { return false; } let (Some(pid), Some(tid)) = (percpu::current_pid(), percpu::current_tid()) else { return false; }; - must_stop(ThreadId { pid: pid.raw(), tid: tid.raw() }, caller()) + thread_word(pid.raw(), tid.raw()) != caller } -/// Whether this thread is the one shutdown this boot gets: a second caller's -/// sweep would band the first where it parks inside its own sync. +/// Whether this boot's stop has begun; once it has, it never ends. +pub fn begun() -> bool { + CALLER.load(Acquire) != NOBODY +} + +/// Whether this thread is the one shutdown this boot gets: a second caller +/// stops with the rest. pub fn claim_the_shutdown() -> bool { // One exchange: a read and a write that can come apart let two callers in. !CLAIMED.swap(true, AcqRel) @@ -105,112 +62,19 @@ pub fn claim_the_shutdown() -> bool { static CLAIMED: AtomicBool = AtomicBool::new(false); -/// What the stop's caller parks on between two sweeps. -static PROGRESS: Watch = Watch::new(); - -/// Called just after the running thread made a transition that can end the -/// stop's wait on it — banded at its safe point, parked, or exited — and never -/// before one: a sweep woken first would find it still running and wait for a -/// post that has already come. -pub fn note_progress() { - if stops(STAGE.after_write()) { - PROGRESS.post(); - } -} - -/// This boot's stop has begun: from here every thread but its caller that -/// returns to Ring 3 is stopped at that boundary, whatever the [`Record`] says. +/// This boot's stop has begun and every other CPU has been through Ring 0 +/// since: no thread but the caller is in Ring 3, and none returns to it. pub struct Stopping(()); -/// Stop every userland thread but the caller, and answer with what it took. -/// -/// Returns when the machine is stopped or when [`PARK`] is spent, never -/// otherwise: an expiry is a line in the record and a reset that lands where -/// it lands, because a machine nobody can turn off is worse than one whose -/// last word overlapped somebody's syscall. +/// Begin the stop. Returns once every other CPU has begun a scheduler pass: +/// whatever it returns to Ring 3 after that pass reads the stop begun. #[must_use] -pub fn stop() -> (Record, Stopping) { +pub fn stop() -> Stopping { // Refused by name rather than defaulted: a caller with no task identity is // not a reboot syscall. - let caller = ThreadId { - pid: percpu::current_pid().expect("quiesce::stop: the caller holds no process").raw(), - tid: percpu::current_tid().expect("quiesce::stop: the caller holds no thread").raw(), - }; - // Armed before the first sweep, so a transition landing between a sweep - // and the park after it leaves a record that park returns on at once; and - // before the stage opens, so nothing between the opening and the first - // sweep is a point where a pass can take this CPU. - let parkable = crate::scheduler::Parkable::at_entry(); - let armed = watch::arm(&PROGRESS, 0, WaitClass::Other) - .expect("quiesce::stop: the caller holds no task to park"); - CALLER_PID.store(caller.pid, Relaxed); - CALLER_TID.store(caller.tid, Relaxed); - // Last: a gate that sees the stop sees the caller it must not stop. - STAGE.open(STOPPING); - - crate::sched::driver::ring_all_but_self(); - let cpus = crate::smp::cpu_count(); - - let began = crate::clock::now(); - let budget = if crate::actuator::stop_budget_spent() { Duration::from_nanos(0) } else { PARK.duration() }; - let deadline = Deadline::at(began + budget); - let mut sweeps = 0; - loop { - let swept = sweep(caller); - sweeps += 1; - let elapsed = (crate::clock::now() - began).nanos(); - if swept.keep_waiting(elapsed, budget.nanos()) { - // Uncancellable: the claim is taken, and a caller that left here - // would leave a machine nothing else may turn off. The deadline is - // `keep_waiting`'s own, so an expiry ends the loop at the next sweep. - watch::wait_uncancellable(&parkable, &armed, deadline); - continue; - } - // Read here and not by the caller: the question is what was open at the - // moment the stop ended, and every line between here and the record's - // own would open more. - return ( - Record { - sweep: swept, - elapsed_ms: elapsed / 1_000_000, - budget_ms: budget.nanos() / 1_000_000, - sweeps, - cpus, - }, - Stopping(()), - ); - } -} - -/// Mark every parked thread but the caller and count the rest. -/// -/// The table lock is held for the walk and given up before the park: a thread -/// on another CPU finishing its own teardown takes this same lock. -fn sweep(caller: ThreadId) -> Sweep { - let mut out = Sweep::default(); - let guard = process::PROCESS_TABLE.lock(); - let Some(table) = guard.as_ref() else { return out }; - for (_, proc) in table.iter() { - let pid = proc.pid(); - for (&tid, thread) in proc.threads() { - if !must_stop(ThreadId { pid: pid.raw(), tid: tid.raw() }, caller) { - continue; - } - if matches!(thread.state(), process::ThreadLocation::Zombie(_)) { - continue; - } - let sched = thread - .sched() - .expect("quiesce::sweep: a live thread in the table with no task"); - // `stop_if_blocked` refuses a running thread: it has to reach its - // own safe point, and until it does it is what this sweep is - // waiting for. - if sched.shared.stop_pending() || sched.shared.stop_if_blocked() { - out.stopped += 1; - } else { - out.running += 1; - } - } - } - out + let pid = percpu::current_pid().expect("quiesce::stop: the caller holds no process"); + let tid = percpu::current_tid().expect("quiesce::stop: the caller holds no thread"); + CALLER.store(thread_word(pid.raw(), tid.raw()), Release); + crate::sched::driver::pass_on_every_other_cpu(); + Stopping(()) } diff --git a/kernel/src/sched/driver.rs b/kernel/src/sched/driver.rs index 1b3ce5d8324..2e13b92f058 100644 --- a/kernel/src/sched/driver.rs +++ b/kernel/src/sched/driver.rs @@ -504,7 +504,6 @@ pub fn pass(dispose: Dispose) { maybe_sweep(now); #[cfg(feature = "pass-spin")] maybe_hold(now); - let stops = matches!(dispose, Dispose::Stop); let action = with_cpu(|cpu| { let pass = SchedPass::begin(cpu, env(&PreemptOff(())), now); if let Some(current) = pass.cpu().running() { @@ -531,10 +530,6 @@ pub fn pass(dispose: Dispose) { // while idling, so `pass_block` need not be a second one. #[cfg(feature = "sched-check")] report_pass_costs(now); - // After the band, before the switch: the running task is still this one. - if stops { - crate::quiesce::note_progress(); - } execute(action); crate::preempt::enable_no_resched(); } @@ -577,18 +572,18 @@ pub fn pass_block(ticket: Ticket, deadline: Option) { let ticket = ticket.into_raw(); crate::preempt::clear_need_resched(); let now = HW.now(); - let (action, parked) = with_cpu(|cpu| { + let action = with_cpu(|cpu| { let pass = SchedPass::begin(cpu, env(&PreemptOff(())), now); if let Some(current) = pass.cpu().running() { check_stack(current.ext()); current.ext().handle.publish(current.acct(), None); } match ticket.commit() { - Commit::Parked(committed) => (pass.dispose_block(committed, deadline).finish(), true), + Commit::Parked(committed) => pass.dispose_block(committed, deadline).finish(), // A post landed between phase 1 and the commit: do not park; the quantum may still have expired. - Commit::AlreadyWoken => (pass.dispose_none().finish(), false), + Commit::AlreadyWoken => pass.dispose_none().finish(), // A retire landed while deciding to park: the thread keeps running and unwinds rather than exit through a dead switch. - Commit::Killed => (pass.dispose_none().finish(), false), + Commit::Killed => pass.dispose_none().finish(), } }); #[cfg(feature = "mask-windows")] @@ -599,10 +594,6 @@ pub fn pass_block(ticket: Ticket, deadline: Option) { current.ext().handle.publish(current.acct(), Some(now)); } }); - // After the commit, before the switch: the running task is still this one. - if parked { - crate::quiesce::note_progress(); - } execute(action); crate::preempt::enable_no_resched(); } @@ -731,17 +722,30 @@ pub fn pass_owed() -> bool { cpus().get(current_cpu()).doorbell().kick_pending() } -/// Ring every other CPU's doorbell with nothing behind it and kick each whose -/// edge this raised, so each runs a pass: what a machine-wide stop needs of a -/// CPU halted with its timer stopped, or running a thread that never enters -/// the kernel. -pub fn ring_all_but_self() { +/// Demand a pass of every other CPU and return once each has begun one. A +/// pass runs in Ring 0, so a thread a CPU returns to Ring 3 after that pass +/// reads what the caller wrote before the call. A CPU that begins none inside +/// [`crate::time::DEAF_CPU`] is wedged, and a panic. +pub fn pass_on_every_other_cpu() { let me = current_cpu(); - for cpu in (0..cpus().len() as u32).map(CpuId).filter(|&cpu| cpu != me) { - if cpus().get(cpu).poke() == Kick::Send { + let others = || (0..cpus().len() as u32).map(CpuId).filter(move |&cpu| cpu != me); + for cpu in others() { + if cpus().get(cpu).doorbell().demand_pass() == Kick::Send { HW.kick(cpu); } } + let deadline = crate::time::Deadline::at(crate::clock::now() + crate::time::Duration::from_nanos(crate::time::DEAF_CPU.nanos())); + for cpu in others() { + while cpus().get(cpu).doorbell().pass_demanded() { + assert!( + !deadline.reached(crate::clock::now()), + "sched: cpu{} began no pass in {} of being kicked for one", + cpu.0, + crate::time::DEAF_CPU, + ); + core::hint::spin_loop(); + } + } } /// Count the running thread's syscall `num`, which took `ticks` of the counter. diff --git a/kernel/src/scheduler.rs b/kernel/src/scheduler.rs index 351d718fbe7..352c8cdb291 100644 --- a/kernel/src/scheduler.rs +++ b/kernel/src/scheduler.rs @@ -140,13 +140,13 @@ pub fn do_preempt() { driver::pass(Dispose::None); } -/// The last thing a thread does before returning to user mode, if either mark it -/// can carry says it never does. [`exit_to_user`] is the one caller; -/// `kernel/src/quiesce.rs`'s header says why that boundary is the safe point. +/// The last thing a thread does before returning to user mode, if the +/// machine's stop or the thread's kill says it never does. [`exit_to_user`] +/// is the one caller; `kernel/src/quiesce.rs`'s header says why that boundary +/// is the safe point. /// -/// **One call and one match, so the two marks have no order to disagree -/// about**: `kernel::sched::task::SafePoint` ranks them, here and in -/// `CpuSched::place` alike. +/// **One call and one match, so the two have no order to disagree about**: +/// `kernel::sched::task::SafePoint` ranks them. #[track_caller] pub fn leave_user_if_due() { let Some(due) = driver::current_safe_point(crate::quiesce::stops_this_thread()) else { diff --git a/kernel/src/syscall/machine.rs b/kernel/src/syscall/machine.rs index 8f9f540676b..228d809931d 100644 --- a/kernel/src/syscall/machine.rs +++ b/kernel/src/syscall/machine.rs @@ -132,25 +132,18 @@ fn quiesce(last: &str) -> Result<(crate::quiesce::Stopping, StopWire), SyscallEr log!("power: this machine is already stopping, so this caller stops with the rest"); return Err(SyscallError::AlreadyExists); } - // First: what follows outlasts a feed cadence, and no pass runs to feed again. + // First: what follows may outlast a feed cadence. crate::arch::watchdog::disarm(); - // Every userland thread stops here, the log's writer and the UART's holder - // with the rest: the supervisor had the log flush before it asked for this stop. - let (stopped, stopping) = crate::quiesce::stop(); + // No thread returns to Ring 3 from here, the log's writer and the UART's + // holder with the rest: the supervisor had the log flush before it asked. + let stopping = crate::quiesce::stop(); // Held to the machine's end: every record below reaches the UART before a // CPU is taken down. - let mut wire = crate::log::console::take_for_the_stop(); + let mut wire = crate::log::console::take_for_the_stop(&stopping); // The machine's census, which no process's start or end takes. crate::census::log(); #[cfg(feature = "mask-windows")] crate::windows::report(); - // A shortfall is the budget spent, not the reset refused: it is said at - // alert level, and the reset lands anyway. - if stopped.stopped_the_machine() { - log!("{stopped}"); - } else { - crate::alert!("{stopped}"); - } log!("{last}"); wire.drain(); Ok((stopping, wire)) diff --git a/kernel/src/watch.rs b/kernel/src/watch.rs index c07c1fd7560..f1b4e8ef5d9 100644 --- a/kernel/src/watch.rs +++ b/kernel/src/watch.rs @@ -193,6 +193,7 @@ enum Ended { } /// Only a futex's watch is revoked, and only [`wait_until`] waits on one. +#[cfg(feature = "boot-actuators")] #[track_caller] fn not_revocable() -> ! { panic!("watch: a revoke reached a wait that does not end on one"); @@ -200,7 +201,9 @@ fn not_revocable() -> ! { /// Park once, for a wait a kill may not end: until a post reaches this /// registration or the deadline passes. A return is not an answer — the -/// caller re-reads its condition. +/// caller re-reads its condition. The SYSRET probe's alone +/// (`arch::hw::sysret_ss_probe`). +#[cfg(feature = "boot-actuators")] #[track_caller] pub fn wait_uncancellable>(p: &Parkable, armed: &Armed<'_, L>, deadline: Deadline) { match wait_inner(p, armed, deadline, Cancel::Ignores) { diff --git a/lib/quiesce/Cargo.toml b/lib/quiesce/Cargo.toml index 3674884906c..6ab5fb1018b 100644 --- a/lib/quiesce/Cargo.toml +++ b/lib/quiesce/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toyos-quiesce" -description = "Every decision the machine's stop makes, and none of its effects." +description = "The bounds of the supervisor's orderly stop, shared with the programs it stops." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/lib/quiesce/src/lib.rs b/lib/quiesce/src/lib.rs index bdb2cb5b2ef..bad00d85af6 100644 --- a/lib/quiesce/src/lib.rs +++ b/lib/quiesce/src/lib.rs @@ -1,71 +1,10 @@ -//! Every decision the machine's stop makes, and none of its effects. -//! -//! The shutdown path's two claims — every filesystem is synced, and -//! `Rebooting.` is the last thing that happened — are claims about a machine, -//! and they are true only of a machine that has stopped. Stopping it is two -//! questions: *which* thread may still run, and *when* is it over. Both are -//! here; `kernel/src/quiesce.rs` marks the threads and spends the time. -//! -//! **Nothing is carved out.** The log's writer stops with every other thread: -//! the supervisor has it flush before it asks for the stop, and what the -//! kernel says after that goes to its console, never to a process it would -//! have to keep running. -//! -//! [`Record`] is written by the kernel and read back off a console by the -//! harness, so its wire form is rendered and parsed here and nowhere else. +//! The supervisor's stop: how long it waits on each step before it asks the +//! kernel to end the machine, and what a disk driver is told the machine +//! stops by. The kernel waits for none of it. #![no_std] #![forbid(unsafe_code)] -use core::fmt; - -/// One thread, as the kernel's task ids spell it. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct ThreadId { - pub pid: u32, - pub tid: u32, -} - -/// Whether `thread` must stop at its next safe point. -/// -/// **`caller` is never stopped**: it is the thread running the stop, and it has -/// the rest of the shutdown to perform. Its own process is not exempt — a -/// sibling thread of the process that asked for the reboot stops like any -/// other, because what the reset must outlast is one thread's remaining work -/// and not one program's. -pub fn must_stop(thread: ThreadId, caller: ThreadId) -> bool { - thread != caller -} - -/// What one sweep of the machine's userland threads found. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] -pub struct Sweep { - /// Threads that must stop and have: banded at a safe point, or parked and - /// marked so a wake can only band them. - pub stopped: u32, - /// Threads that must stop and are still running or runnable. The stop is - /// not over while this is non-zero. - pub running: u32, -} - -impl Sweep { - pub fn total(self) -> u32 { - self.stopped + self.running - } - - /// Whether the caller sweeps again. - /// - /// **A spent budget ends the wait; it never ends the reset.** What is left - /// running when this answers `false` is what [`Record`]'s shortfall clause - /// names, because a machine nobody can turn off is worse than a reset that - /// lands inside somebody's syscall. A sweep that finished the stop on the - /// very tick the budget ran out finished it: the running count is read - /// first. - pub fn keep_waiting(self, elapsed_ns: u64, budget_ns: u64) -> bool { - self.running != 0 && elapsed_ns < budget_ns - } -} - /// A policy number: how long the supervisor waits on one call into a file server that /// is alive and has not answered — and so how long a stop request can wait /// behind one, since the supervisor serves `power` between its loop's passes. @@ -88,207 +27,3 @@ pub const DRIVERS_MS: u64 = 3_000; /// readable from then. The driver hands its devices back, leaves its /// controller as firmware would find it, and ends. pub const DRIVER_STOP: &str = "stop"; - -/// The thread the kernel's quiesce-last actuators hold, by the name its -/// program gives it: held until it is the one thread the stop still waits -/// on, so the transition it makes next is the stop's last. -pub const LAST_THREAD: &str = "quiesce-last"; - -/// What a line of kernel log carrying a [`Record`] begins with. -pub const STOPPED: &str = "stop: "; - -const OF: &str = " of "; -const THREADS: &str = " userland thread(s) stopped across "; -const CPUS: &str = " cpu(s) in "; -const BUDGET: &str = " ms of a "; -const MS: &str = " ms budget over "; -const SWEEPS: &str = " sweep(s)"; -const SHORTFALL: &str = "; this reset lands wherever the other "; -const ARE: &str = " are"; - -/// What the stop did, as the one line it writes beside the boot's other -/// closing censuses. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Record { - pub sweep: Sweep, - pub elapsed_ms: u64, - /// What the stop was given, declared by the kernel alone: a reader that - /// spelt it again would pass a stop that slept out a budget the kernel had - /// since lowered. - pub budget_ms: u64, - pub sweeps: u32, - pub cpus: u32, -} - -impl Record { - /// The [`Record`] a line of kernel log carries, or `None` for a line that - /// is not one — every boot that reset without going through the stop. - /// - /// **The shortfall is said twice and checked against itself**: a line - /// whose clause disagrees with `total - stopped` is not one this kernel - /// wrote, and is refused rather than half-read. - pub fn parse(line: &str) -> Option { - let (_, rest) = line.trim_end().split_once(STOPPED)?; - let (stopped, rest) = rest.split_once(OF)?; - let (total, rest) = rest.split_once(THREADS)?; - let (cpus, rest) = rest.split_once(CPUS)?; - let (elapsed_ms, rest) = rest.split_once(BUDGET)?; - let (budget_ms, rest) = rest.split_once(MS)?; - let (sweeps, rest) = rest.split_once(SWEEPS)?; - - let stopped: u32 = stopped.parse().ok()?; - let running = total.parse::().ok()?.checked_sub(stopped)?; - if running > 0 { - let said: u32 = rest.strip_prefix(SHORTFALL)?.strip_suffix(ARE)?.parse().ok()?; - if said != running { - return None; - } - } else if !rest.is_empty() { - return None; - } - Some(Record { - sweep: Sweep { stopped, running }, - elapsed_ms: elapsed_ms.parse().ok()?, - budget_ms: budget_ms.parse().ok()?, - sweeps: sweeps.parse().ok()?, - cpus: cpus.parse().ok()?, - }) - } - - /// Whether every thread that had to stop did. - pub fn stopped_the_machine(self) -> bool { - self.sweep.running == 0 - } -} - -impl fmt::Display for Record { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{STOPPED}{}{OF}{}{THREADS}{}{CPUS}{}{BUDGET}{}{MS}{}{SWEEPS}", - self.sweep.stopped, - self.sweep.total(), - self.cpus, - self.elapsed_ms, - self.budget_ms, - self.sweeps, - )?; - if self.sweep.running > 0 { - write!(f, "{SHORTFALL}{}{ARE}", self.sweep.running)?; - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - extern crate alloc; - - use super::*; - - const CALLER: ThreadId = ThreadId { pid: 10, tid: 0 }; - - fn id(pid: u32, tid: u32) -> ThreadId { - ThreadId { pid, tid } - } - - #[test] - fn the_caller_is_never_stopped_and_its_siblings_always_are() { - assert!(!must_stop(id(10, 0), CALLER)); - assert!(must_stop(id(10, 1), CALLER), "a sibling thread of the caller's process is not the caller"); - } - - /// **The log's writer is not carved out**: `logkeeper`'s threads stop like any - /// other process's, whatever it holds. - #[test] - fn every_other_thread_stops_the_log_writer_included() { - const LOGKEEPER: u32 = 2; - for tid in 0..4 { - assert!(must_stop(id(LOGKEEPER, tid), CALLER)); - } - } - - #[test] - fn a_sweep_with_nothing_left_running_ends_the_wait_however_long_it_took() { - let done = Sweep { stopped: 6, running: 0 }; - assert!(!done.keep_waiting(0, 2_000)); - assert!(!done.keep_waiting(2_000, 2_000)); - assert!(!done.keep_waiting(u64::MAX, 2_000)); - } - - #[test] - fn the_budget_ends_the_wait_and_the_boundary_is_inclusive() { - let left = Sweep { stopped: 4, running: 2 }; - assert!(left.keep_waiting(0, 2_000)); - assert!(left.keep_waiting(1_999, 2_000)); - assert!(!left.keep_waiting(2_000, 2_000)); - } - - /// A machine with no userland left at all — every process already exited — - /// is stopped, not waiting. - #[test] - fn an_empty_machine_is_already_stopped() { - assert!(!Sweep::default().keep_waiting(0, 2_000)); - } - - const WHOLE: Record = Record { - sweep: Sweep { stopped: 6, running: 0 }, - elapsed_ms: 11, - budget_ms: 2010, - sweeps: 3, - cpus: 8, - }; - - fn short() -> Record { - Record { sweep: Sweep { stopped: 4, running: 2 }, ..WHOLE } - } - - #[test] - fn the_record_names_the_shortfall_only_when_there_is_one() { - assert_eq!( - alloc::format!("{WHOLE}"), - "stop: 6 of 6 userland thread(s) stopped across 8 cpu(s) in 11 ms of a 2010 ms \ - budget over 3 sweep(s)", - ); - assert_eq!( - alloc::format!("{}", short()), - "stop: 4 of 6 userland thread(s) stopped across 8 cpu(s) in 11 ms of a 2010 ms \ - budget over 3 sweep(s); this \ - reset lands wherever the other 2 are", - ); - assert!(WHOLE.stopped_the_machine()); - assert!(!short().stopped_the_machine()); - } - - /// **The one declaration, checked both ways.** A reworded arm of `Display` - /// that the readers no longer match is what this crate exists to make - /// impossible. - #[test] - fn every_record_reads_back_as_itself_off_a_line_of_log() { - for record in [WHOLE, short(), Record { sweep: Sweep::default(), ..WHOLE }] { - let line = alloc::format!("[2026-09-14 20:38:12 7.955 cpu4 kernel] {record}\r"); - assert_eq!(Record::parse(&line), Some(record), "{line}"); - } - } - - #[test] - fn a_line_that_is_not_a_record_is_refused_rather_than_half_read() { - for line in [ - "[stamp] Rebooting.", - "[stamp] stop: 6 of 6 userland thread(s) stopped across 8 cpu(s)", - // The shortfall clause disagreeing with the counts it restates. - "[stamp] stop: 4 of 6 userland thread(s) stopped across 8 cpu(s) in 11 ms of a 2010 \ - ms budget over 3 sweep(s); this \ - reset lands wherever the other 9 are", - // A shortfall clause on a record that claims to have stopped. - "[stamp] stop: 6 of 6 userland thread(s) stopped across 8 cpu(s) in 11 ms of a 2010 \ - ms budget over 3 sweep(s); this \ - reset lands wherever the other 2 are", - // More threads stopped than there were. - "[stamp] stop: 7 of 6 userland thread(s) stopped across 8 cpu(s) in 11 ms of a 2010 \ - ms budget over 3 sweep(s)", - ] { - assert_eq!(Record::parse(line), None, "{line}"); - } - } -} diff --git a/tests/common/power.rs b/tests/common/power.rs index 309b6b486c8..34b3d555aaa 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -45,23 +45,22 @@ pub fn ended( Ok(()) } -/// A stop that ends with a userland thread still running is followed by a -/// power-off all the same, with the sleep type `acpiserver` handed the -/// kernel: q35 hands over in ACPI mode, so the power-off -/// quiets the events `acpiserver` enabled, and QEMU stops for -/// `guest-shutdown`. One CPU and `stop-budget-spent`, so the stop's one sweep -/// finds `stop_short`'s spinner queued behind the stop's caller. -pub fn machine_shutdown_short_stop(test_config: &Path) -> Result<(), String> { - let short = qemu::build_toyos_bin( +/// A stop asked for while a thread spins in userland on another CPU ends in +/// a power-off, with the sleep type `acpiserver` handed the kernel: the stop +/// returns once that CPU has answered its kick, q35 hands over in ACPI mode, +/// so the power-off quiets the events `acpiserver` enabled, and QEMU stops +/// for `guest-shutdown`. Two CPUs, so the spinner is in Ring 3 when the stop +/// begins. +pub fn machine_shutdown_with_a_thread_spinning(test_config: &Path) -> Result<(), String> { + let spinning = qemu::build_toyos_bin( qemu::SUITE_ARCH, &super::compile::repo_root().join("tests/guest/rust-tests"), - "stop_short", + "stop_spinning", ); let options = BootOptions { qmp: true, - smp: 1, - kernel_params: &["stop-budget-spent"], - extra_root_files: vec![("bin/test_rs_stop_short".to_string(), short)], + smp: 2, + extra_root_files: vec![("bin/test_rs_stop_spinning".to_string(), spinning)], ..Default::default() }; let mut qemu = QemuInstance::boot_with_options(test_config, &[], &[], options); @@ -77,22 +76,11 @@ pub fn machine_shutdown_short_stop(test_config: &Path) -> Result<(), String> { serial::Serial::named("boot", console.clone()).must_say(HANDED_OVER_IN_ACPI_MODE)?; let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(qemu::GUEST_QUIET)); - writeln!(qemu.stdin_mut(), "run test_rs_stop_short").expect("write to QEMU stdin"); + writeln!(qemu.stdin_mut(), "run test_rs_stop_spinning").expect("write to QEMU stdin"); qemu.flush_stdin(); let asked_at = console.len(); ended(&mut qemu, &mut stop, &mut console, SHUTTING_DOWN, "guest-shutdown")?; - let after = serial::Serial::named("the short stop", console[asked_at..].to_string()); - after.must_be_clean()?; - let record = after - .text() - .lines() - .find_map(toyos_quiesce::Record::parse) - .ok_or_else(|| format!("no stop record:\n{}", after.text()))?; - if record.stopped_the_machine() { - return Err(format!("the stop left no thread running, so this boot staged no short stop: {record}")); - } - eprintln!(" [power] a short stop, then the power-off: {record}"); - Ok(()) + serial::Serial::named("the stop", console[asked_at..].to_string()).must_be_clean() } /// The loader's lines about the proof, in `loader/src/proof.rs`. diff --git a/tests/guest/rust-tests/src/bin/stop_short.rs b/tests/guest/rust-tests/src/bin/stop_short.rs deleted file mode 100644 index 567b906dacc..00000000000 --- a/tests/guest/rust-tests/src/bin/stop_short.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Ask for the power-off with a thread of this process still spinning: on a -//! one-CPU guest whose stop sweeps once (`stop-budget-spent`), that thread is -//! queued behind the stop's caller, and the stop ends with it running. -//! `machine_shutdown_short_stop` runs it. - -use toyos::power::{stop, Mode, Pending, Stop}; - -fn main() { - std::thread::spawn(|| loop { - std::hint::spin_loop(); - }); - let refused = stop(Stop::Shutdown, Mode::Terminate).map(Pending::outcome); - panic!("stop_short: the power-off was refused: {refused:?}"); -} diff --git a/tests/guest/rust-tests/src/bin/stop_spinning.rs b/tests/guest/rust-tests/src/bin/stop_spinning.rs new file mode 100644 index 00000000000..93e87ac4531 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/stop_spinning.rs @@ -0,0 +1,13 @@ +//! Ask for the power-off with a thread of this process spinning in userland +//! on another CPU, which enters the kernel only when the stop kicks it. +//! `machine_shutdown_with_a_thread_spinning` runs it. + +use toyos::power::{stop, Mode, Pending, Stop}; + +fn main() { + std::thread::spawn(|| loop { + std::hint::spin_loop(); + }); + let refused = stop(Stop::Shutdown, Mode::Terminate).map(Pending::outcome); + panic!("stop_spinning: the power-off was refused: {refused:?}"); +} diff --git a/tests/toyos.rs b/tests/toyos.rs index 111737d341b..4de005753b9 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -235,8 +235,9 @@ const RUST_SKIP: &[&str] = &[ // needs a boot with no ACPI server: // `a_claim_before_the_declaration_is_refused_at_once` runs it. "pci_undeclared", - // It powers the machine off: `machine_shutdown_short_stop` runs it. - "stop_short", + // It powers the machine off: `machine_shutdown_with_a_thread_spinning` + // runs it. + "stop_spinning", // It claims QEMU's virtio NIC, which the T14 has none of: `bar_map_again` // runs it. "bar_map_again", @@ -630,10 +631,11 @@ const MACHINE_TESTS: &[&str] = &[ // by stopping. No host test reaches either, and the T14 is never asked to // power off. "acpi_supply_outlives_holder", - // The power-off after a stop that left a thread running, in ACPI mode: it - // ends the machine, so only one QEMU reports stopping can be asked, and - // the T14 hands over in legacy mode, where no holder means no quieting. - "machine_shutdown_short_stop", + // The power-off asked for with a thread spinning in userland on another + // CPU, in ACPI mode: it ends the machine, so only one QEMU reports + // stopping can be asked, and the T14 hands over in legacy mode, where no + // holder means no quieting. + "machine_shutdown_with_a_thread_spinning", // A clean stop proves its image: the stop mark `logkeeper` writes at the // stop's flush, and the next loader pass's floor raise. x86-64 q35: it // takes the loader's second pass on the same disk and firmware variables, @@ -3359,7 +3361,7 @@ fn a_bent_supervisor_is_refused_and_nothing_boots(profile: qemu::Profile, test_c /// judge's verdict: this boot's `logkeeper` files hold `Boot: complete` and the /// supervisor's stop line, so the log went through usbd's disk to the stick and /// was made durable before the power went. Then usbd handed the stick back -/// with a port reset and left its controller reset, before the kernel's stop +/// with a port reset and left its controller reset, before the kernel's last word /// ([`usb_stopped_before_the_reset`]). fn the_log_lands_on_the_boot_stick(test_config: &Path) -> Result<(), String> { let options = BootOptions { @@ -3417,7 +3419,7 @@ fn stick_load_line(line: &str) -> Option { /// usbd ended for the stop with every connected port's reset finished and its /// controller halted and reset (its exit code, which the kernel records, /// because its own line may reach no log), and it ended before the kernel's -/// stop: the supervisor waited for it. +/// last word: the supervisor waited for it. fn usb_stopped_before_the_reset(log: &str, console: &str) -> Result<(), String> { // On the boot's one clock: the request is in the file (its line can be // one the console dropped under the load), and what follows the flush @@ -3430,12 +3432,12 @@ fn usb_stopped_before_the_reset(log: &str, console: &str) -> Result<(), String> }; let asked = at(log, Source::Program("supervisor"), "supervisor: power: the machine stops"); let ended = at(console, Source::Kernel, "exit: usbd pid="); - let stopped = at(console, Source::Kernel, toyos_quiesce::STOPPED); + let stopped = at(console, Source::Kernel, power::SHUTTING_DOWN); let (Some((Some(asked), _)), Some((Some(ended), line)), Some((Some(stopped), _))) = (asked, ended, stopped) else { - return Err(format!("no stop request in the file, or no usbd end and kernel stop on the console\n{console}")); + return Err(format!("no stop request in the file, or no usbd end and kernel last word on the console\n{console}")); }; if !(asked <= ended && ended <= stopped) { - return Err(format!("usbd ended at {ended} ms, not between the stop's request ({asked} ms) and the kernel's stop ({stopped} ms)")); + return Err(format!("usbd ended at {ended} ms, not between the stop's request ({asked} ms) and the kernel's last word ({stopped} ms)")); } if !line.contains(" code=0 ") { return Err(format!("usbd's stop left a port in reset or its controller running: {line}")); @@ -4160,17 +4162,10 @@ fn virt_smp(profile: qemu::Profile, conduit: &str, el: u32) -> Result<(), String judge_virt_job(&mut qemu, &mut serial, "test_rs_counters_read", COUNTERS_READ_SAID)?; judge_virt_job(&mut qemu, &mut serial, "test_rs_trace_read", TRACE_READ_SAID)?; judge_virt_job(&mut qemu, &mut serial, "test_rs_timing_protection", TIMING_DIT_SAID)?; - let (console, calls) = + let (_, calls) = ended_through_psci(&mut qemu, &mut stop, serial, power::SHUTTING_DOWN, "guest-shutdown", &trace, |_| Vec::new())?; - let record = console - .lines() - .find_map(toyos_quiesce::Record::parse) - .ok_or_else(|| format!("the kernel wrote no stop record\n{console}"))?; - if record.cpus != VIRT_CPUS { - return Err(format!("the stop ran across {} CPUs, not {VIRT_CPUS}: {record}\n{console}", record.cpus)); - } let last = psci_powered_off(&calls, VIRT_CPUS, &[])?; - eprintln!(" [virt] {record}; every CPU but {last:#x} called CPU_OFF, then {last:#x} SYSTEM_OFF"); + eprintln!(" [virt] every CPU but {last:#x} called CPU_OFF, then {last:#x} SYSTEM_OFF"); Ok(()) } @@ -4948,7 +4943,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "acpi_mediated_access" => acpi_mediated_access(2), "acpi_lock_given_back_on_one_cpu" => acpi_mediated_access(1), "acpi_supply_outlives_holder" => acpi_supply_outlives_holder(), - "machine_shutdown_short_stop" => power::machine_shutdown_short_stop(test_config), + "machine_shutdown_with_a_thread_spinning" => power::machine_shutdown_with_a_thread_spinning(test_config), "a_clean_stop_proves_its_image" => power::a_clean_stop_proves_its_image(test_config), "bar_map_again" => bar_map_again(test_config), "bar_map_behind_a_bridge" => bar_map_behind_a_bridge(test_config),