Repository navigation
A child's end is readiness on its Process handle: OP_WATCH answers at the published exit, and no close ends another handle's watch - #648
Conversation
Stage 1 of issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md, as ruled: an OP_WATCH READABLE on a Process completes once the exit is published, closing one handle ends no other's watch, and the ABI gains nothing. - ProcessObject's watch becomes an Arc<Watch>, as a port's is, so ops::read_watch can answer a share of it; SYS_PROCESS_WAIT arms on the same watch and the exit's publish is still its one post. - ops::read_watch answers a Process with that watch and ops::has_data with ProcessObject::finished, so a registration on a child already gone completes in the submit that makes it and one before the end is completed by the publish. - ops::close_ends_polls answers false for a Process: a process is ended by its own end, never by one holder letting a handle go. process_lifecycle gains four arms: three held children in one poller, let go one at a time, each completion naming the child just released and its code there for try_wait; a watch on a child already gone completing at once beside a held one that does not; a kill completing a watch; and a second handle closed while the first is watched, a non-blocking submit finding nothing until the child is let go. init's service waiters still park a thread each on SYS_PROCESS_WAIT; the sentence giving the reason, that no poll could watch a process's end, is no longer true and goes. Moving them onto init's poller is the rest of stage 1. Negative control: the change reverted whole, where a watch on a Process is refused NotSupported and every watch in the first arm completes at the first submit. Mutations: close_ends_polls answering true for a Process reds the close arm; has_data answering false reds the already-gone arm. Oracle: pidfd_open(2), whose descriptor polls readable once the process it refers to terminates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
The branch was at 649ea51 (#641). Three landings since sit under it: #642 (dc8212c), #659 (c1c5048) and #655 (5daab30). Two content conflicts, each main deleting what this branch's hunk stood beside: - kernel/src/object/ops.rs, close_ends_polls: #655 deleted the log's and the keyboard's close actuators, whose two arms this branch's `Process(_) => false` sat between. Main's two `false` arms stand and the process's is a third. - tests/toyos-rust-tests/src/bin/process_lifecycle.rs, the imports: #642 deleted `toyos::AsHandle` with the pid arm, its one user; this branch's `toyos::poller` import stands alone. Everything else merged by itself: #642's deletions in kernel/src/object/process.rs beside this branch's `Arc<Watch>`, #659's init changes beside the one doc sentence this branch deletes, and the `rust` gitlink at main's 95960d6c214. This commit is the resolution and nothing else. What #655's contract changes in this branch's own lines is the next commit's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
60ec86d was written against a kernel where an object's post completed every poll on its watch: the publish's post was the answer to a watch on a child, and has_data's Process arm mattered only to a registration made after the end. #655 took the answer from the post. A post fires a poll, which owes it a look, and the ring's own submitter writes the completion after reading ops::has_data again; an object not ready at the look is armed again (kernel/src/inbox/mod.rs, Submitter::look). Nothing this branch added became unnecessary. What each line is for moved: - has_data's Process arm, ProcessObject::finished, is what the look of every watch on a process reads, not the late registration's alone. - read_watch's Process arm is what lets a watch on a live child register: inbox::arm refuses NotSupported a poll with no readiness and no watch. - read_posts_are_readiness keeps a Process in main's false arm, with no change here: the kernel holds the fact the look reads, so the post is not the readiness. - close_ends_polls answers false for a Process, as before: ops::close cancels every poll on the read watch of a kind it answers true for. publish_exit stores `finished` before it posts, as it already did for SYS_PROCESS_WAIT's predicate; the look rests on the same order. The prose that had the post answer goes: read_watch's comment, and the sentence of process.rs's header in which the publish answered the watch. The watch's field doc is main's sentence with the reason for its own Arc. process_lifecycle's first arm watches each child once, not again each round. On #655's kernel a watch replaces its handle's earlier poll, so the re-watch made every completion the newest registration's; watched once, the arm also holds that a standing poll outlives two other children's ends. The track's stage 1 is cut to what is left of it: init's waiter threads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
… and by no close The model held a process's watch and its handles and scripted nothing that armed on one: publish_exit posted Watch::Process to no waiter, so no law could red on what answers a watch on a child. Op::Watch is OP_WATCH on a holder's handle to a process, cut where the kernel's registration is: the poll registered on the object's watch, then the look inbox::arm owes it, which answers an end published in the window. L14, checked at every state: a wait or a watch on a process is answered only with that process's exit published. a_watch_on_a_process_is_answered_by_its_end_and_by_no_close runs every ordering of a parent's watch on its child, the child closing its own `self`, and a kill of the child. L4 holds the watch answered once the end is published, registered before it or after; L14 holds it unanswered until then. mutate-close-ends-a-process-watch is its control, in src/ci.rs's CONTROLS: the model's close posts the watch of the process its handle named, as ops::close does for a kind close_ends_polls answers true for, which is what main answers for a Process. Under it the child's close answers its parent's watch with the child still running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
`Poller` hands a completion's token to its caller and never its result, and hands out a refusal's token as it does a ready one's. So an end that answered a watch on a child `-NotFound`, as a watch whose source is gone, passed every arm: each saw a token at the right time. The kill arm takes one raw ring of its own (`watch_result_across`), submits the watch, sees nothing complete, kills the child, and reads the one completion whole: its result is `READABLE`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Main moved during the round: #657 (8f6abbc) and #649 (c59e09e) landed after b102097 merged 5daab30. No conflict, and no file is changed on both sides since 5daab30; the `rust` gitlink is the same on both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
And says what the kill arm now reads: the completion's result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…hrough close_ends_polls answers false for a Process so that one holder's close ends no other's watch, and a close has no way to end only the polls made through its own handle. A watch whose handle closes is therefore kept until the child ends, and answered -NotFound then. Measured at 81e6853 in one QEMU guest, with a scratch arm that is in no commit (x-scratch-closed-handle.patch, posted on the pull request): a watch on a duplicate of a held child's handle, the duplicate closed: the watch on handle 4102: Ok(0) after its close: Ok(0) after the child's end: Ok(1), token 7 result -1 The same arm with a pipe made after the close read the same -1: a handle carries its slot's generation, so the pipe's ends were 16392 and 11 where the closed handle was 12296, and the look found the closed one stale. A poll does not move to whatever takes its slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
Evidence for The gates (
The model, and the lint control of its new feature ( The T14 stagings ( The closed handle, measured at The patches
|
|
T14 at
Readbacks and judge logs: |
|
Review of #648 at Rulings asked for
BLOCKER
NOTE
REMOVE
SEND BACK |
Review of #648 at 7bf868e, BLOCKER: `Op::Watch`, L14, `a_watch_on_a_process_is_answered_by_its_end_and_by_no_close`, the `mutate-close-ends-a-process-watch` feature and its `CONTROLS` row modelled the change and compiled none of it. `toyos-proclife` depends on `toyos-abi` alone: only `Op::Watch` armed a `Watch::Process`, and only the model's own `publish_exit`, its own look and the feature's arm released one. So the QEMU mutations m1-m5 and the whole-change control each left that test green, and its control redded only when the model itself was edited. What the deleted test claimed is held where the kernel's lines run: `process_lifecycle`'s four watch arms, their QEMU mutations and the T14. `toyos-proclife/src/lib.rs` loses the `OP_WATCH` clause of `Watch::Process`'s doc, which nothing in the crate names any more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Review of #648 at 7bf868e, REMOVE: `close_when_it_ends`'s "It parks in the kernel for the process's life and costs nothing until then" was a rewrapped remnant of the deleted reason, and none of the three comment kinds; `process_lifecycle`'s header listed the four watch arms that their own doc comments already state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…tch defect names its twin Review of #648 at 7bf868e, NOTE 1: the remainder's exit named only the deletion, which a conversion dropping what `close_when_it_ends` decides would still meet. It now names each behaviour: a `restart` row started again on the ports it kept; a row without one having its acceptors closed so a client's next connect is `Gone`; a swap's expected end leaving them open. And what reads each: `src/metalswap.rs`'s `judge` reads the swap through `toyos-metal --swap`; `git grep` over `tests/` and `src/` for init's restart and close lines ("started again", "its ports are closed", `ServerGone`, "sshd: no network") finds no test of the other two. `netd_gone_mid_bind` closes a port of its own and never reaches init's supervision. NOTE 2: `a-watch-outlives-the-close-of-the-process-handle-it-was-made-through` names `a-close-of-one-handle-ends-every-rings-poll-on-its-object`, whose first exit alternative would keep the very poll this one's exit ends: one change, a close ending the polls made through its own handle and no other's, answers both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
#651 deletes retired syscalls, SYS_DEBUG actions and inbox ops. It shares no file with this branch; nothing `process_lifecycle` imports from `toyos_abi::inbox` moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
Evidence at
|
|
T14 at
Readbacks and judge logs: |
|
Review of #648 at Round 1's BLOCKER
Round 1's NOTEs and REMOVEs: all closed at this head.
Rulings
BLOCKER: none. NOTE: none. REMOVE
LAND AFTER NAMED CHANGES |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…hers) into wt/toyos-resident One conflict: issues/hardware/the-t14-boots-toyos-unattended.md, which this branch deletes and main modified. Every hunk of main's side since the merge base 4f2bea1 is accounted for: - e9f67e7 (#639) appended one line: "`smp_failed_ap_leaves_no_hole` is deleted; issues/build/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md records the commit that restores it." It qualified the sentinel's finding 2, which named that test as the roster's gate. No file replacing the track plans a sentinel or names the test; the gap a sentinel was for (the span before clock::init) is stated in kernel/src/deadline.rs's header, and the restore the line pointed at stays where it was recorded, in smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md ("`git revert aedcf17` brings it back"). So the file stays deleted and the line has nothing left to qualify. The deleted track's one other citation, from #638: issues/build/hard-lockup-bound-ms-is-read-by-nothing-but-its-own-assertion.md named it as owner. Its owner is now issues/hardware/a-frozen-toyos-waits-for-a-hand-on-the-power-button.md, the track #638's review named. Its two exits disagreed: that track's first step gives HARD_LOCKUP_BOUND_MS a reader (the detector's bound on a boot that names no boot-deadline=), and the issue's exit deleted the constant. The reader stands: the issue now closes on that step, with the constant's doc saying what it bounds, and says deleting it is not the exit, since the step would declare it again. issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md merged cleanly; main's two hunks (create_boot_image's per-image GUIDs, and root_read_ticks) are in it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Stage 1 of
issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md, the kernel's half, as ruled: anOP_WATCHREADABLEon aProcesshandle (WAIT) is answered once the exit is published, closing one handle ends no other handle's watch, and the ABI gains nothing. Nothing undertoyos-abi/,toyos/oruserland/libc/changes.A process that holds a child's handle now waits for its end in the poller it already waits in, beside its pipes and ports. On
mainthat watch is refused-NotSupported, and the only wait that blocks is a thread parked inSYS_PROCESS_WAITper child.init's waiter threads are the other half of the stage and are not in this pull request. The track's stage 1 is cut to them.
What changed, per decision
ops::read_watchanswers aProcesswith its watch, andops::has_datawithProcessObject::finished(kernel/src/object/ops.rs).has_dataagain. Sofinishedis the answer of every watch on a process.inbox::armrefuses-NotSupporteda poll with no readiness and no watch.publish_exitstoresfinishedbefore it posts, as it already did forSYS_PROCESS_WAIT's predicate. The look rests on the same order, and nothing else posts that watch.read_posts_are_readinesskeeps aProcessinmain'sfalsearm, unchanged: the kernel holds the fact the look reads, so a post is not the readiness.ops::close_ends_pollsanswersfalsefor aProcess.ops::closecancels every poll on the read watch of a kind it answerstruefor, in every ring.self. With a read watch andmain'strue, a child closing its ownselfwould answer its parent's watch as gone while the child runs. So would any other holder of a handle to it.ProcessObject's watch is anArc<Watch>, as a port's is (kernel/src/object/process.rs).WatchRefhas no lifetime, soread_watchanswers a share.SYS_PROCESS_WAITand the test kernel's spawn hold arm on the same watch through it.close_when_it_ends's doc said a thread was needed because the kernel answered a process's end "to nothing a poll can watch". That paragraph is deleted. The threads are unchanged.close_when_it_ends, and what reads each:restartrow that ends is started again on the ports it kept;Gone;src/metalswap.rs'sjudgereads this one, on the T14 throughtoyos-metal --swap.git grepovertests/andsrc/for init's "started again" and "its ports are closed", forServerGoneand for sshd's "no network on this machine" finds none.netd_gone_mid_bindcloses a port of its own and never reaches init.issues/kernel/a-watch-outlives-the-close-of-the-process-handle-it-was-made-through.md. A close that may end no other handle's watch cannot end its own handle's either, so that watch is kept until the child ends and answered-NotFoundthen. Measured; see "Not sure of". It namesissues/kernel/a-close-of-one-handle-ends-every-rings-poll-on-its-object.md, whose first exit would keep the very poll this one's exit ends: one change answers both.What the merges changed
The branch was one commit,
60ec86df3, on649ea51d4(#641).1a3636989).-NotFoundat the look.close_ends_pollsa boundary: theselfhandle above. It also made a watch on a child complete after the whole subtree below that child has ended, because readiness is the published exit and publication climbs child before parent. This branch adds no test of that order;process_treeand the model's L9 hold it.process_lifecycleand itsAsHandleimport; the arms here never used either.b102097ca, eachmaindeleting what a hunk stood beside:close_ends_polls: inbox: a watch is answered after a look at its object, never by a post #655 deleted the log's and the keyboard's close actuators, whose two arms theProcessarm sat between.process_lifecycle.rs's imports.Tests
process_lifecycle, a member of the T14's shared boot, gains four arms. Every child is held on its stdin, so an end is ordered by the test and by no clock; every blocking wait isu64::MAX, bounded by the harness's hang ceiling.each_end_completes_its_own_watch: three children in onePoller, each watched once under its index, released in the order 1, 2, 0. Before each release a non-blocking submit finds nothing. After it a blocking wait answers exactly that child's token, andtry_waitreads its code.a_watch_on_an_ended_child_completes_at_once: a child already waited for and a held one, watched together. One non-blocking submit answers the ended child's token alone.a_kill_completes_a_watch: a raw ring of one watch on a held child. Nothing completes; the child is killed; the one completion's result word isREADABLE, andtry_waitreads 137.Pollerhands out a completion's token and never its result, so this arm reads the completion itself (watch_result_across).closing_one_handle_ends_no_other_watch: the child's handle is watched and a duplicate of it closed. A non-blocking submit finds nothing; after the release the watch answers andtry_waitreads 17.ops::read_watch,has_dataandclose_ends_polls, andinbox::armthat reads them, name the kernel and compile in no host crate.kernel-loomcompilespolls.rsagainst a fake look.Gates at
209891cacOne script, from the clean committed head; the tree is clean after (
gates-results-209891cac.txt).cargo run -- --ci hostcargo run -- --build-onlycargo test --test toyos-build, the QEMU suiteprocess_lifecycleis not among themcargo test -p toyos-proclifeHigh-risk checks
This is the kernel's object and wait path. Every run below is one script that checks its patch, applies it, runs, reverses it and shows the tree clean. The patches, scripts and result files: #648 (comment) for
7bf868ef4, #648 (comment) for209891cac.process_lifecyclein one QEMU guestprocess_lifecyclehas no QEMU registration, so a harness patch that is in no commit (local-qemu.patch) bootstests/testcaseswith 8 vCPUs and runs that one member. A guest exit code is the image having built and booted. Exit iscargo test --test toyos-build -- zz_local_process_lifecycle's.The branch's kernel diff at
209891cacis byte for byte its kernel diff at7bf868ef4(git diff origin/main...209891cac -- kernelagainstgit diff c59e09ed6...7bf868ef4 -- kernel,cmpexit 0), and #651 changes nothing underkernel/src/object/. So the head and the whole-change control are measured at209891cac, andm1tom5stand from7bf868ef4.209891cacnc-whole:kernel/anduserland/initas onorigin/maina93067fc2, the test kept209891cac:270). The watch is refused-NotSupported, whose completion the first submit hands outm1:close_ends_pollsanswerstruefor aProcess7bf868ef4m2:has_dataanswersfalsefor aProcess7bf868ef4m3:read_watchanswersNonefor aProcess7bf868ef4m4:has_dataanswerstruefor aProcess7bf868ef4m5:publish_exitcancels its watch's polls before it posts7bf868ef4m2's red is a hang, a completion verdict.m5is the mutation the kill arm's raw ring exists for: an end answered-NotFound.nc-wholeis byte for bytegit diff 209891cac a93067fc2 -- kernel userland/init.Oracles
The T14
The T14 booted four images of
209891caconce each, run by the orchestrator (the boots and judges): each image's sha256 checked before it was flashed, eachtoyos-metalexit 0;processexit 0 (PASS process_tree, 3 passed, 0 failed, 2 boots, the four arms' lines in order),spawn_child_ends_firstexit 0, and the control'sprocess_lifecycleexit 1 (exited 101 on the T14,child 0 is held, and its watch completedatprocess_lifecycle.rs:270:35). They were staged, each bycargo test --test toyos-build -- --metal --metal-readback <dir> <filter>, exit 2 (staged) each, the tree clean after;/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/648-r2/metal/request.txtlists the boots, the judges in order and what each must read and exit.process/proctreecaseprocess_treef04f5c2fa200a9de5815c5b806487a2636a28fd186f60fd3d93f4ee428e175efprocess/sharedprocess_lifecycle,std_process0886f151467517c8e0cb713bdfa877d18c8f0ea67e030eb9587b856a72e6045espawn_child_ends_first/shared-debugspawn_child_ends_first14dea088cca20e67e91bc87b54aa47011348d60e1406e18bd3625f78995bbde5process_lifecycle/sharednc-wholeprocess_lifecycle782f6b19cabc3449fbd842e61ffd9788efc122c6a5a13aeb49c83dec41820cadProcessObject::watch(), whose type changed. They areSYS_PROCESS_WAIT, whichstd_processandprocess_treerun, and the test kernel's spawn hold, whichspawn_child_ends_firstruns.test_rs_process_lifecycle exited 101 on the T14.Not sure of
m1tom5) are QEMU's alone, at7bf868ef4.81e68535cin one QEMU guest with a scratch arm that is in no commit: a watch on a duplicate of a held child's handle answers nothing at the submit after the duplicate's close, and answers token 7 with result -1 once the child ends. Filed. No committed arm closes a watched handle.issues/kernel/a-close-of-one-handle-ends-every-rings-poll-on-its-object.md's exit, a completion's result reachingPoller's caller, is what would let them read it.m2's wait is not shown parked. Its guest is "still talking" at the ceiling because on a shared boot the kernel itself prints on a cadence (GUEST_QUIET's doc,tests/common/qemu.rs), which says nothing of the test's thread. By reading, the submitter parks: the look arms a new poll on a watch nothing posts again.Gonerests on agit grep, not on a run.Net lines
git diff --shortstat origin/main...209891cac: 6 files, +178 −24.Arc(kernel +16 −9); one paragraph of init's doc deleted (−4).🤖 Generated with Claude Code
https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm