Repository navigation
kernel: no kernel thread but the idle loop; the console UART is a userland driver's (K4, K6) - #1175
Conversation
Review r4 of #1175 (follow-up to r1–r3 on #1165), head 152cae5The fix is in the right place, and the rule that selects the rows is the right one. On a boot that can declare, Selection by held grant. No-declarer path. Test. Comments and record. The new doc comments are short and accurate. The rewording in the issue and in Evidence. Host CI at 152cae5 exited 0. The console/session rows on the remote KVM host show the UART claim going to pid 4, next to logkeeper, which matches the new order. The two aarch64 rows still owed on a quiet Mac are outstanding as stated. They are not affected by this change, whose logic is architecture-independent and covered by the host test. FindingsNone. LAND |
…rland driver's The small-kernel track's K4 and K6, by the owner's ruling that the kernel creates no thread but each CPU's idle loop. `klogd`, the one kernel thread, was the console's writer. It is gone, and so is every provision kernel threads had: `sched/kthread.rs` and its row table, `is_kernel_task` and `current_is_kernel_thread`, quiesce's exemption, the scheduler's zero baseline, `kernel_start`, `Start::Kernel`, a kernel stack without a user area, `Runs::Kernel`, the kernel address space object with its reserved PCID and ASID, `ElfInfo::none` and an `Option` on every task's image. The console UART - The kernel writes the UART only where no process can: inline as each record commits until `log::console::hand_over`, before userland runs; from the machine's stop, once userland is stopped; and on every fatal path. - Between them it is `/system/bin/uart`'s, which holds a `uart` claim (a new device class: COM1's ports and IRQ 4 on x86, as an `isa` row; the SPCR's PL011 frame and its level SPI on AArch64, which needed SPI routing to a row) and carries the log `logkeeper` feeds it on `log-wire`, as the virtio console's driver carries it on `log-carrier`: one carrier serving in logkeeper, two channels. The UART is quiet while the port carries, says so on itself, and is fed again when the port goes, stalls or the machine stops. - The kernel and logkeeper share one word: the newest stamp the console has spoken (`SYS_LOG_SPOKEN`, which replaces `SYS_CONSOLE_CARRIED`). The inline drain sets it; logkeeper advances it as a channel delivers whole rounds. The UART's driver starts after it, so the UART carries the boot once; the stop and a fatal path put every record after it on the UART. - The kernel's console object is gone with its queue: the supervisor makes the console's input a pipe, whose read end is every program's stdin and whose write end the UART's driver writes what the UART receives into. Its read waits on the pipe's readiness, not a 10 ms repoll. - Without a thread to post it, the log's readiness is owed by `emit` and posted at the next syscall's end or scheduler pass. Thread lifecycle - `SYS_YIELD`, and std's `yield_now` on it. - A detached thread's place goes as it ends (`proclife::leave_detached`), and `SYS_THREAD_EXIT`'s detached form releases its stack once the thread no longer runs on it; std's and libc's `REAPED` lists are gone. Deleted with the thread: `log/handoff.rs`, `kernel::carrier`, the sleep lock (its one user was `klogd`), the wire-holding actuators and their tests, the loom models of the handoff, the log waiter and the sleep lock, and their negative controls. Rebased onto main 2c2f957: #1171 added an owner and a citation of K4 (this change, then #1165) to issues/a-holders-queued-line-can-reach-the-console-after-the-last-word.md, which this change deletes. The record stays deleted: the kernel's holders' queue and klogd's drain it describes are gone, so no holder's queued line exists to follow the last word; the residual it named, a holder the stop did not stop, is the stop's shortfall, which its record reports by name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`images/diag.toml` is for a machine with no serial port and claims no device, which `no_diag_program_claims_the_screen` holds; the check that every image running logkeeper drives the console UART names it as the one exception. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`tests/images/virtpancase.toml` landed on main beside this branch and runs logkeeper, so it starts the UART's driver as every such image does (`every_boot_config_runs_logkeeper`). The UART's carrier records how far it passed over the replay while the port carried it, so the stop's wait reads where it stands rather than where it was muted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hat holds them The supervisor endowed the log's origins acceptor to the row named `logkeeper` and the console-input pipe's write end to the row named `uart`. A name grants no authority: the holder of the origins names every line's writer, and the holder of the console's input types at every program's standard input. A `[programs]` row now says `holds = ["log-origins"]` or `holds = ["console-input"]`; the manifest carries it as a `hold` line, and the supervisor endows the row that holds each. The build refuses a row that holds either unless it is that object's one holder (`HOLDINGS`, checked with `held_by_their_holders_alone` on every rendered manifest), and every boot config that runs logkeeper must give it the origins and its UART driver the console's input. The UART driver refuses to start without the console's input rather than drop every byte it receives unsaid. The issue on a supervisor that fails before logkeeper runs now records what the kernel's inline drain can and cannot carry: holding it past the hand-over would carry no line of the supervisor's, which is not a kernel record. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… end says why What the console has spoken was a stamp, compared across shards: a round logkeeper delivered could carry a record stamped after one another CPU had stamped and not yet committed, and every fatal path then left that record off the UART. It is now a read position per shard. The kernel's drain position is the mark: the inline drain moves it, SYS_LOG_SPOKEN moves it to at least the positions logkeeper's read reached through the rounds a channel delivered whole (taking a cursor, as SYS_LOG_READ does), and the stop and the fatal paths write every record past it. logkeeper holds a round's positions back to each shard's first record still waiting for a later round, so a round speaks only for what it wrote, and a channel handed a gap in the replay speaks for nothing after it. Where the UART's carrier starts is decided by position too: every line of the rounds before the hand-over but the kernel records the kernel put on the UART itself. SYS_LOG_SPOKEN needs a new right, Rights::CONSOLE (the `console` syscap), which the build gives logkeeper alone, since what it moves is kept off every fatal path's output; a log reader can no longer mute it. A UART carrier started again resumes where its channel left off, or passed over while the port carried the log, rather than at the hand-over, which replayed up to the whole replay at the UART's rate after every restart. The hand-over takes the boot drains' claim on the wire and never gives it back, so a drain still walking on another CPU finishes first and none starts after: one writer by construction. The supervisor's end is the machine's: the kernel ends the machine on it through the panic path, naming how it ended. Before it does, the supervisor's panic hook says the panic's message as its last words (SYS_LOG_LAST_WORDS, one alert record, the boot's first call alone, gated by Rights::LOG, one line of at most 512 bytes of text with no control character, validated at the boundary). The fatal path puts both on the UART, the panel and the black box, so a supervisor that fails before logkeeper runs says why again. The test harness can stage a ROOT without one of its files (Plan::root_omits), and a new QEMU row boots a /system without its release file and finds the reason on the UART before the kernel's panic. Also: AArch64 routes every SPI to the boot CPU, as the comments said, not to whichever CPU made the claim; the SPCR's Interrupt Type is read, and a UART whose interrupt is not a GIC's gets no interrupt row. On x86 COM1 stays the kernel's, Kept, where no I/O APIC carries its line and no row is filled. A thread spawn whose process has lost its image is an invariant violation, not a silent None. The thread table's forget contract names the detached leave. The guard-page record's hold on K6 is lifted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stop and every fatal path take the console UART back from its driver, which may be behind the log: on a fast guest whose console port carried the log, the UART's driver was still sending boot lines from its pipe when the machine stopped, and had not yet said the log was carried elsewhere. The harness shows a virtio machine's UART only from that word or a kernel's death, so the stop's lines, which only the UART had, were hidden, and machine_shutdown_short_stop stalled waiting for its last word under KVM. The kernel now starts its own lines there with a line that says it writes the UART from here and that what its driver had not sent is in /log (toyos_abi::log::KERNEL_TAKES_THE_UART), and the harness's UART filter shows the UART from it on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…keover line says only what is true SYS_LOG_LAST_WORDS was gated by Rights::LOG, which logkeeper and test-runner hold to read the log: either could write a record every fatal path shows, or spend the boot's one slot and leave the supervisor's real reason refused. It now needs Rights::LAST_WORDS, which the kernel mints on the supervisor's system capability and no manifest syscap name maps to. The kernel's takeover line claimed what its driver had not sent was in /log; at the stop the held program lines are not, and a fatal path can come where no /log exists. It now says only "console: the kernel writes the UART from here". A refused stop's lines reach the UART twice; the test's comment said once. The comment says so, and the console-loss record records it with its exit. The IOMMU event SPI's comment names the boot CPU, and the guard-page record drops the history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…before the declaration Main's start plan (#1164) put every row that neither keeps the log, holds the acpi claim nor may mint it after the declaration, storage and the homes, and the UART's driver among them. The kernel lets go of the UART at its hand-over, before userland runs, so a machine whose only console is its UART said nothing from userland's start until storage was up, and nothing at all where storage hung. The driver needs neither the declaration nor a home. The plan starts first the rows that carry the log, by what they hold: the log's origins (logkeeper) and the console's input (the UART's driver), in place of logkeeper's name. A host test pins the UART's driver in `first`; the plan's old rule reds it. The refused-stop comment and record now say only the lines the UART had already taken repeat, and the record's exit paragraph is wrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`watch::wait` went with klogd; the cancellable wait is `watch::wait_until`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
152cae5 to
98d6617
Compare
|
Orchestrator verification of the rebase, recorded after the merge. I armed auto-merge while the PR still conflicted, so it merged through the queue as soon as the rebased head passed the required checks. That happened before this record was posted.
No code changed relative to the reviewed head. At |
Merged rather than rebased: a linear rebase replays #1144's and #1141's merged-in history onto a main they were never written for, and stopped at the sixth of 69 commits on a syscall a later commit here supersedes. Conflicts, each kept to what neither side deleted: - ObjectKind: this branch removed File, K4 removed Console. Neither is kept and the kinds after them renumber densely (SysCap 9 .. DeviceSet 19), as `from_word` indexes `ALL` by the word. - object/mod.rs and object/ops.rs: the kobject list and every match arm lose both Console and File; ops.rs drops the serial import that only the console object used. - Syscalls: main's SYS_YIELD 168 and SYS_LOG_LAST_WORDS 169 stay; SYS_NAMESPACE_NAMES moves to 170, the profile to 172 bins, its bound asserted on the new top. pread/pwrite stay deleted with the kernel file objects; thread_exit takes main's doc. - kernel/CLAUDE.md: main's direction-flag/AC and BackendGuard rules; the blocking-syscall pattern keeps this branch's paged-object reads, as SYS_FSYNC went with the file objects. - libccase.toml: main's uart beside this branch's MEMORY file server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…origins K4 (#1175) moved the UART to a userland driver fed by logkeeper's wire and made `log-origins` logkeeper's to hold; `every_boot_config_runs_logkeeper` refuses an image without it. datacase.toml and formatcase.toml, which this branch added, take designatecase.toml's rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Continues #1165, which GitHub would not reopen after a resync attempt; its review rounds r1–r3 and their responses are there.
Small-kernel track, stages K4 and K6, by the owner's ruling that the kernel creates no thread but each CPU's idle loop; and, in the same thread-lifecycle area, a yield that yields and detached threads that free themselves.
Problem
klogd(kernel/src/log/console.rs) was the last kernel thread: the console UART's one writer between the boot and the stop, draining the record ring and the lineslogkeeperqueued on the kernel console. Every kernel-thread provision existed for it:sched/kthread.rsand its row table,is_kernel_task/current_is_kernel_thread, quiesce's exemption, the scheduler's zero baseline, a kernel-space task and its reserved PCID/ASID. std'syield_nowwas a spin hint, and a detached thread's stack and table place waited for the process's next spawn.Now
The kernel writes the UART only where no process can: inline as each record commits until
log::console::hand_over(before userland can claim the UART), from the machine's stop once userland is stopped, and on every fatal path. Between them the UART is/system/bin/uart's.The UART is a claim like any device: a new
uartdevice class (toyos_abi::uart). On x86 it is anisarow, COM1's eight ports bound to the reading process and IRQ 4 as records; COM1 leavespio::FIXEDand is that row's (the ACPI mediation now answersClaimedPortfor it). On AArch64 the row is the SPCR's PL011: its register frame installed as an uncached aperture with the description, and its interrupt a level SPI routed to the row (irqchip::route_spi/set_spi_masked,arch::aarch64::pio::Wire), masked by the handler until the holder acknowledges.One carrier serving, two channels (
system/logkeeper/src/carrier.rs). The UART's driver asks onlog-wireexactly as the virtio console's asks onlog-carrier: admitted alone, fed from where its channel starts, saysDELIVERED. The port'sCarriagedecides the UART's quiet as before; the UART's feed writesCARRIED_ELSEWHERE/BACK_ON_THE_UARTon itself (the harness's merge keys on them, nowtoyos_logstream's) and skips what the port carries. Lines said while the machine stops reach the UART's carrier, held back from the file as before.What the console has spoken is a read position per log shard, the kernel's drain position (
log::console). The inline drain moves it.SYS_LOG_SPOKEN(replacingSYS_CONSOLE_CARRIED) moves it to at least the positions logkeeper's own read reached through the rounds a channel delivered whole, taking a cursor asSYS_LOG_READdoes. It needs the newRights::CONSOLE(theconsolesyscap), which the build lets onlylogkeeperhold, because what it moves is left off every fatal path's output. logkeeper holds a round's positions back to each shard's first record still waiting for a later round, so a round speaks only for what it wrote, and a channel handed a gap in the replay speaks for nothing after it. The stop and every fatal path write every record past the mark: they may repeat what a channel delivered and had not said, and miss none of the kernel's records, whatever their stamps. The UART's first carrier is handed every line before the hand-over except the kernel records the kernel put on the UART itself (decided by position), so the boot is on the UART once. A carrier started again resumes where its channel left off, or where it passed over while the port carried the log.The hand-over takes the boot drains' claim on the wire for good: a drain still walking on another CPU finishes first, and none starts after, so there is one writer by construction (bounded by a 60 s tripwire).
The supervisor's end is the machine's. The kernel ends the machine on the supervisor's end through its panic path, naming how it ended (
the supervisor ended, code=101, and nothing starts it again). Before that, the supervisor's panic hook says the panic's message as its last words throughSYS_LOG_LAST_WORDS. The call writes one alert record, accepts only the boot's first call (AlreadyExistsafter), needs the newRights::LAST_WORDS, which the kernel mints on the supervisor's system capability and no manifestsyscapname maps to, and takes one line of at most 512 bytes of UTF-8 with no control character, checked at the boundary (toyos_abi::log::last_words; the SDK fits text to it first). The fatal path puts both on the UART, the panel and the black box. A supervisor that fails beforelogkeeperruns therefore says why again, and a machine whose supervisor dies no longer idles.The kernel console object is gone, with its line queue,
ConsoleLine,FileType::Serialand the 10 ms console repoll. The supervisor makes the console's input a pipe: its read end is every program's stdin (test-runner's commands arrive through it), and its write end is endowed to the UART's driver (toyos_logstream::CONSOLE_INPUT), which writes what the UART receives into it and refuses to start without it.The log's origins and the console's input are grants, not names: a
[programs]row saysholds = ["log-origins"]orholds = ["console-input"](the manifest'sholdline), and the supervisor endows the row that holds each. The build refuses a row that holds either but its one holder (HOLDINGS, inheld_by_their_holders_alone, on every rendered manifest), and every boot config that runslogkeepermust give it the origins and its UART driver the input.The log's readiness without a thread:
emitowes it (log::user::owe_readiness, one release store) and the next syscall's end or scheduler pass posts it.K6:
sched/kthread.rs,kthread::spawn, the row table,is_kernel_task,current_is_kernel_thread,kernel_start/kernel_thread_returned,Start::Kernel, the user-less kernel stack,Runs::Kernel,mm::space::kernelwithPcidHandle::Kernel/Asid::Kernel,ElfInfo::none, quiesce's two exemptions and the scheduler'sblocking_baselineare deleted; a task's image is no longer anOption.git grep -n 'kthread::spawn(' -- kernel/srcfinds nothing, and no kernel code creates a schedulable task but the idle loop.Thread lifecycle:
SYS_YIELDgives the CPU to another runnable thread of the band and is std'syield_now;osbench'stime.yieldnow times it.SYS_THREAD_EXIT's detached form (thread_exit_detached) releases the thread's stack once it no longer runs on it, andproclife::leave_detachedforgets the thread's place as it leaves (a teardown in progress keeps it). std's and libc'sREAPEDlists are gone.Deleted with
klogd:log/handoff.rs,kernel::carrier, the log waiter, the sleep lock (its one user), the three wire-holding actuators and the five tests that staged them (machine_shutdown_wire_held/_kept/_at_the_seal,virt_reboot_wire_held/_kept), the loom modelsconsole_handoff,log_wakeandsleep_lockwith their negative controls, and the metal rowklogd_hosted, replaced byconsole_uart_driven.Decisions to review
irqchip::init), as x86 routes every device line to one destination.Checks
Head
98d66179eon main2c2f957cb.Round 9, rebased onto main
2c2f957cb, at98d66179e(evidence):--ci hostexit 0, 108 green. The six console rows passed on the x86 guest host.issues/a-holders-queued-line-can-reach-the-console-after-the-last-word.mdstays deleted: the kernel's holders' queue it describes is gone. issues: close the load-only records whose tests are deleted #1171's owner and K4 citation in it are accounted for in the K4 commit. The rest of sched: a waiter a kill ends passes on the bounded post it may hold #1168's and sched: a steal probe is a bit in its victim's word, which a thief can take back #1170's scheduler changes merged cleanly, and one comment naming the deletedwatch::waitwas fixed. Every log, patch and command, masked, with its revision, host and exit: round 1, round 3, rounds 4 and 5 round 6 and round 7 (README in each).Round 7, at
152cae5d1: main's start plan (Supervisor times PCI claims: the declaration in the device set, and storage and homes before the rows that need them #1164) had put the UART's driver after the declaration, storage and the homes, leaving a UART-only machine silent from userland's start until storage was up. The plan now starts first the rows that carry the log, by what they hold (the log's origins and the console's input), beside the declaration's holder. Pinned bythe_uart_starts_with_the_log_before_the_declaration, which the plan's old rule reds (exit 101).--ci hostexit 0, 108 green. On the x86 guest host, 6 rows passed: the new supervisor row, both UART rows,machine_shutdown_short_stop,console_image_bootsandsession_lifecycle. The UART's claim is now pid 4's, beside logkeeper. Where nothing can declare, both still start with every row in main's storage order.Round 6, review r2's fixes rebased onto main
783569506, atd4c43fd35:--ci hostexit 0, 108 green;--build-onlyx86 and AArch64 exit 0;a_supervisor_that_fails_early_says_why_on_the_uarton the x86 guest host passed, with last words now gated byRights::LAST_WORDS. The rebase resolvedtests/images/acpicase.toml(main's storage rows beside this change's UART row) andspawn_thread(main'snearplacement beside the imageexpect); the r5 rows and metal at8ac4f2187cover what r6 leaves untouched.Host, at
8ac4f2187:cargo run -- --ci hostexit 0, 108 steps green;--build-onlyx86 and AArch64 exit 0. New host tests: the ABI's last-words rule and fitting; logkeeper'sa_round_is_through_the_cursor_but_what_waits,what_the_uart_carried_is_told_by_position,a_uart_carrier_started_again_resumes_where_its_channel_left_offand the hub's positions; the build'sconsolesyscap gate; the harness filter'sthe_kernels_taking_the_uart_is_shown_from.x86 guests on the x86 guest host (KVM), at
8ac4f2187: 10 passed:a_supervisor_that_fails_early_says_why_on_the_uart(new),the_uart_carries_the_boot_once_across_the_hand_over,the_uart_carries_the_log_again_once_the_console_carrier_goes,machine_shutdown_short_stop,std_threads_count_the_cpus_and_give_back_ended_threads,session_lifecycle,console_image_boots,acpi_lock_given_back_on_one_cpu,screen_fatal_halt_composited,nested_nmi_is_loud.That host found a defect at
8678b19c7:machine_shutdown_short_stopstalled there twice and passed on main. The UART capture held the stop's lines andShutting down., but they came after the UART driver's boot backlog and before the driver ever said the port carried the log. The harness shows a virtio machine's UART only from that line, so it never saw them.8ac4f2187has the kernel start its stop and fatal lines withconsole: the kernel writes the UART from here(toyos_abi::log::KERNEL_TAKES_THE_UART), and the harness filter shows the UART from it on. The failing runs are the control for that commit.AArch64 guests, at
152cae5d1on a quiet Mac:virt_jobs_at_el2andvirt_fatal_halts_the_others_first, exit 0, 2 passed (round 8): no other qemu, rustc or cargo process ran, and the one-minute load was under 4 for three minutes before the run.Controls, each red as it should be:
Metal, the readbacks of
08e4bb8f0judged. Both benches'testcases-watchdogboots are green:loader_watchdog_armsandwatchdog_fed, the log whole to its stop. On bothtestcasesbootsstd_threadsexited 0.console_uart_drivenheld its own claim on both (uart: no console UART claim: neither bench has a COM1, so the 16550 and IRQ 4 paths reach no hardware we have). It is red only onmust_be_cleanfinding another row's panic: on the AMD bench,counters_metal's wait for an EC query that machine never raises (metal: the ACPI rows wait from their own start, and counters admits the machine's own events #1163 fixes it); on the T14,usbd_spare's (issues/a-metal-row-names-no-machine-so-another-benchs-boot-carries-it.md). The other reds are the other bench's rows riding the boot,i219_msi's own record and, on the T14, rows needing its DATA, which an unrelated incident damaged.Metal staged at
8ac4f2187, not run:boot:testcasesper bench, which bringstestcases-watchdog.Earlier rounds' rows (rounds 1 and 3) stand for what later commits did not touch. The detached-thread, yield, stop and fatal-path controls of round 1 are unchanged in subject.
Kernel size,
kernel/src: 233 files, 60,378 lines on the original base → 230 files, 58,979 lines at8ac4f2187. All ofkernel/(pure, loom, proclife, sim): 380 files, 112,027 lines → 373 files, 110,056 lines.Risks and gaps
ConsoleObjectand the console read path, both deleted here, and usedkernel/src/sleeplock.rs, which this deletes as dead. By the landing order (Consolidate storage around native bcachefs and userland filesystems #1152, this, then kernel: user copies go through the process's own mapping, and none runs under a lock (S0-a's lock half) #1150), kernel: user copies go through the process's own mapping, and none runs under a lock (S0-a's lock half) #1150 adapts.issues/the-console-loses-what-its-carrier-had-not-delivered.md). At the stop the kernel now says so on the UART.sync-*threads ends only that thread: its last words are said, a stray alert record on a machine that carries on, and a later end of the supervisor finds the slot spent. Its other threads' panics become a panic of its main thread, whose reason is the first panic's.time.yieldagainst Linux on both benches is owed:issues/toyos-yield-is-not-yet-measured-against-linux.md.🤖 Generated with Claude Code