Repository navigation
IPC phase 2: a connection carries whole messages - #1121
Conversation
Review r1 of #1121 at
|
d878eb1 to
2ee4db1
Compare
Review round 1 answered at
|
e396bb5 to
73cf522
Compare
Round 2 state, head
|
|
From the scheduler agent, about this PR's AMD
|
AMD ipcwake judged: neither the per-frame park hypothesis nor LLC placement
Next: one T14 and one AMD boot of a copy-split profile on |
Copy split judged; the second copy is gone (
|
Review r2 of #1121 at
|
|
Review r2, answered at Blocker 1, fixed as suggested. netstack makes both of a link's bells. Suggestions, all taken:
Separately, the AMD memcpy issue record is dropped, because #1133 fixes its cause. Checks:
|
|
wake.onto_busy placement A/B, T14, four boots, all passed. Head is this PR at
Evidence (bench and note lines only): https://github.com/ToyOSOrg/evidence/tree/9261cc8e8d211555f26bd786eb2de91f96eb146a/ipc-messages/placediag-t14 |
Each direction of a connection is a kernel message queue: the bytes of its messages in the frames #1074 takes as a pipe fills, and beside them the messages' lengths and their handles, in the order they were sent. A send admits the whole message under the queue's lock or refuses it with nothing of it queued and every handle back at its number; a receive takes the head message whole into the caller's buffers, its handles installed under the same lock, or leaves it queued. No stream position exists any more, so nothing pins a batch of handles at one. The bounds are the approved ones, per direction: 2 MiB of bytes, taken as used, 256 messages and 64 handles. Without room the send waits, or answers WouldBlock; a receiver's buffer too small for the head message is InvalidArgument and a table without room for its handles is ResourceExhausted, both leaving it queued; a peer gone is Gone. A connection polls writable once the largest message would fit, so a refused non-blocking send retried on readiness lands. ABI: SYS_MESSAGE_RECV takes one message. SYS_HANDLE_RECV, HandlesPending and MAX_QUEUED_BATCHES are gone, and plain read and write on a connection are refused PermissionDenied: a connection is never a byte stream. SDK: a frame is a 4-byte message type and its payload, the kernel reporting length and handle count, so IpcHeader goes. FrameRx is a buffer and one receive, non-blocking (pump) or blocking (recv), and replaces recv_header/recv_payload/recv_bytes/recv_handles and the partial-frame state machine. A send frames into an uninitialised buffer instead of a zeroed 8 KiB one. Every consumer moves to it in this commit. netstack's Client::gone asks with a non-blocking receive instead of a one-byte read. The link (#1106) rang its doorbell as a byte on its connection, the last such doorbell; it rings an event each way now, as the block service's do. The hello carries the bell the driver rings, the answer the bell the stack rings, and a bell whose waker is gone is the link's end, so the connection goes once the link is open. Deleted: kernel/pure/message.rs's batches and windows, HandleQueue, AtBatch, try_read_within, the pipe's written/read counters and Landing (a pipe's write landing is its own rule now), the connection arms of the pipe read and write paths, the acceptor's close_now of each queued connection (a pending connection's receiver goes with it). Tests: the pure queue (order, wholeness, each bound, a refused receive taking nothing, a 50,000-step random run against the order sent); the SDK's framing and refusals; messages_are_whole, replacing message_carries_its_handles, for the syscalls' refusals and the bounds as a sender meets them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The job detached hangupholder through a shell and took it as ready once the roster listed it. A process that has not yet listened for its quits is ended by the first one, so a stop asked in that window killed the holder before its first syscall, the stop found nothing to wait for, and the machine rebooted under the job: the run stalled at "the job to reach the forced reboot". Whole-message connections made the job's own requests fast enough to land in that window: red at 343a655 in both runs, where the base, 18060b5, passed. A second red, the forced reboot's last lines missing from the console, is the base's too, and is recorded on its own (issues/a-forced-reboots-last-userland-lines-miss-the-console.md). The job now detaches the holder through the session's launcher itself, with its stdout a pipe, and waits for the line the holder says once it listens; sessioncase's test-runner row lists hangupholder in place of the shell, which the job no longer starts. Keeping the shell for the first detach was tried: that run reached the same window at the sign-out, the holder killed after five syscalls. The session still parents what is detached in it, which is the claim; a detached program outliving its shell is process_tree's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
session_lifecycle's remaining red: the job's last line and the supervisor's forced-stop line, written just before the forced reboot, sometimes never reach the serial console, while the kernel's records of the forced stop do. It is red at #1108 18060b5 too (1 of 4 runs on the remote QEMU runner), so it predates whole messages, which make it likelier (3 of 6 at e396bb5) by shortening the time from the terminate to the stop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whole messages put a copy back into every receive whose caller keeps the payload itself. The kernel copied a frame into the receiver's FrameRx, and the caller then copied it out again, where the byte stream had read straight into the caller's buffer. osbench's ipc.reqrep rows pay it per frame. On the AMD they regressed: 7.6 -> 9.5 us at 4096 bytes and 31 -> 66 us at 65536, with one park per side per round trip and every pair inside one LLC (the diary-counting batch on ipc-messages-ab-base/-h1). The only reason for the intermediate buffer was the frame's 4-byte message type in front of its payload. SYS_MESSAGE_RECV now takes two places: the message's first bytes fill `head` and the rest `bytes`, and a message longer than both together is refused whole and stays queued, as before. The ABI gives the two places no meaning. The SDK receives the type into a word and the payload where the caller keeps it: FrameRx holds the two apart, Connection::recv_into takes a payload into the caller's own buffer with its handles, and osbench's peer and toyos::net's response_bytes use it, with no copy out. The send side is unchanged. It frames type and payload into one buffer, as it did before whole messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A NIC driver's hello carried the bell the driver rings, and netstack used it typed on every receive pass. A handle of another kind in that slot, even from a plain driver bug, ended the whole stack with a handle fault, and with it every stream and the healthy second link #1106's failover keeps. No check settles an event's kind, so netstack cannot refuse it. The serving side now makes every bell, as blockserve and netstack's own stream bells already do: Session::open makes both pairs and sends the driver its waker and its bell with MSG_OPENED (OPENED_HANDLES). The hello carries only the region, which Region::lent checks, and HELLO_HANDLES goes. netstack holds only events it made; only the driver trusts received bells, as diskserver does the block server's. The driver's departure is still its waker's last handle, so a hang-up keeps its meaning. Also the review's suggestions: the receive bounds' comment reads, the pipe-lock record is rewrapped with its page-population sentence on both copies, a double blank line goes, and response_bytes says its buffer must hold an error answer too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Landing check at
|
The rebase onto main brings #1123's paged objects, which free a frame with `pmm::free_drawn` as the shared regions now do, and #1104's clock page, which reads its object's one page: both take the commit's types. SYS_SHM_COMMIT is 160, past #1121's SYS_MESSAGE_RECV. The guest tests map with a protection, as main's `shm_map` asks, and a connect's answer carries main's three bells. The kernel's `Stat` size assertion, left from the removed `committed` field, goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rebase onto main brings #1123's paged objects, which free a frame with `pmm::free_drawn` as the shared regions now do, and #1104's clock page, which reads its object's one page: both take the commit's types. SYS_SHM_COMMIT is 160, past #1121's SYS_MESSAGE_RECV. The guest tests map with a protection, as main's `shm_map` asks, and a connect's answer carries main's three bells. The kernel's `Stat` size assertion, left from the removed `committed` field, goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… down memcpy was one rep movsb at every distance. With the destination 16 bytes above the source modulo 4096, an 8 KiB copy took 2595 ns on the AMD bench against 90 ns at distance 0, and the T14 lost up to twelvefold from 16 to 256 bytes (osbench copyat, measurement only). Two heap Vecs dlmalloc places one after the other sit at exactly 16: copyat's heap row read 2525 ns on the AMD bench, the rate #1121's IPC copy split saw. AMD's Family 17h judges store-to-load forwarding eligibility by address bits 11:0 alone (Software Optimization Guide for AMD EPYC 7002, 56305 rev. 3.02, section 2.12), so each load of a forward copy at such a distance matches an older store it does not depend on. Copied highest first, the store a load could match is 4 KiB minus the distance behind it. copy_forward now takes an SSE2 loop from the top for 64 bytes or more at a distance under 512 between disjoint ranges, and rep movsb otherwise. The overlap guard keeps memmove's forward branch lowest first. Host tests: a forward copy at every distance below 1 KiB and four line offsets, and an overlapping one a page apart; the latter reds without the guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rebase onto main brings #1123's paged objects, which free a frame with `pmm::free_drawn` as the shared regions now do, and #1104's clock page, which reads its object's one page: both take the commit's types. SYS_SHM_COMMIT is 160, past #1121's SYS_MESSAGE_RECV. The guest tests map with a protection, as main's `shm_map` asks, and a connect's answer carries main's three bells. The kernel's `Stat` size assertion, left from the removed `committed` field, goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… down memcpy was one rep movsb at every distance. With the destination 16 bytes above the source modulo 4096, an 8 KiB copy took 2595 ns on the AMD bench against 90 ns at distance 0, and the T14 lost up to twelvefold from 16 to 256 bytes (osbench copyat, measurement only). Two heap Vecs dlmalloc places one after the other sit at exactly 16: copyat's heap row read 2525 ns on the AMD bench, the rate #1121's IPC copy split saw. AMD's Family 17h judges store-to-load forwarding eligibility by address bits 11:0 alone (Software Optimization Guide for AMD EPYC 7002, 56305 rev. 3.02, section 2.12), so each load of a forward copy at such a distance matches an older store it does not depend on. Copied highest first, the store a load could match is 4 KiB minus the distance behind it. copy_forward now takes an SSE2 loop from the top for 64 bytes or more at a distance under 512 between disjoint ranges, and rep movsb otherwise. The overlap guard keeps memmove's forward branch lowest first. Host tests: a forward copy at every distance below 1 KiB and four line offsets, and an overlapping one a page apart; the latter reds without the guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase 2 of the owner-approved IPC migration, on
mainafter #1108 (phase 1, events andSYS_HANDLE_WAIT). Draft. The call (SYS_MESSAGE_CALL) is out of this PR by the orchestrator's decision. It goes to a follow-up that carriesparks_nextthrough a poll's delivery and is measured on the fs rows. The scheduler commitb24b37a56(#1111) that was cherry-picked for the call is gone with it.Problem
A connection was two byte pipes plus two handle queues that pinned each batch of handles at a stream position. Plain
readandwriteworked on it, and a write could land partly. The SDK rebuilt[type][len][handles]frames out of partial reads, which made a blocking receive two or three syscalls: header, payload, and the handle receive. A send over 64 bytes zeroed an 8 KiB buffer. The fs-rtt split (T14) puts about 5 µs of a 9.56 µs file round trip in this transport: the send body before the kick 1.84 µs, the post-resume finish 1.43 µs, and the extra frame reads 1.76 µs. This PR removes the extra reads; the call's follow-up targets the wake.Result
A connection carries whole messages (
a connection carries whole messages):WouldBlockwhen non-blocking. A buffer too small for the head message isInvalidArgument, and a handle table with no room isResourceExhausted; both leave the message queued. A peer gone isGone, after whatever it sent before going has been taken.SYS_MESSAGE_RECVtakes one message.SYS_HANDLE_RECV,HandlesPendingandMAX_QUEUED_BATCHESare gone. Plainreadandwriteon a connection answerPermissionDenied.IpcHeadergoes.FrameRxis a buffer with one receive,pump(never waits) orrecv(waits). It replacesrecv_header,recv_payload,recv_bytes,recv_handlesand the partial-frame state machine. A send frames into an uninitialised buffer.sdk/window,lib/*,system/*and the guest tests.SYS_MESSAGE_RECVtakes two places: the message's first bytes fillhead, the restbytes, and a message longer than both together is refused whole. The ABI gives the two places no meaning. The SDK receives the type word apart from the payload.Connection::recv_intotakes a payload straight into the caller's own buffer, so a caller that keeps its payload pays no copy out of aFrameRx; osbench's peer andtoyos::net::response_bytesuse it. AFrameRxis a caller's buffer too.Region::lentchecks, andMSG_OPENEDhands the driver the waker it rings the stack with and the bell the stack rings it on. netstack therefore holds no event a driver sent, so a driver that sends the wrong handle cannot end the stack (no check can settle an event's kind,issues/a-received-handle-has-no-knowable-type.md). A bell whose waker has gone is the link's end.Deleted
kernel/pure/message.rs's batches and windows,HandleQueue,AtBatchandtry_read_within.writtenandreadcounters andLanding.close_now: a pending connection's receiver goes with it.Connection::{read_nonblock, write_nonblock},IpcHeaderand the SDK's blockingrecv_*functions.EPROTOmapping.Checks
Head
b45676390, rebased ontomain4753fb875(#1123):91b68e796: whole messages.ab0209bca: thesession_lifecyclerepair.8aa8ccf2b: the console-loss issue record.aadce7fab: the receive into the caller's buffer.b45676390: review r2, netstack makes both of a link's bells, plus the review's suggestions.Renumbered after #1104 and #1123.
SYS_MESSAGE_RECVis 159: #1104 took 152 and #1123 took 153–158.SYSCALL_PROFILE_BINSis 161, and the compile guard checksSYS_MESSAGE_RECV. The ABI file conflicted in both rebases, and nothing else did. Atb45676390:cargo run -- --ci hostexit 0, 108/108;cargo run -- --build-onlyexit 0 (evidence).The QEMU and metal results below were taken at
a16f09c3a. The rebases add syscall numbers and bring in #1104's clock and #1123's paged objects. Host CI and the image build at the new head show that #1123's code compiles against whole messages, but no guest test has run atb45676390.Actuator members on QEMU at
a16f09c3a. The eightACTUATOR_TESTSmembers needSYS_DEBUG, and--debugboots no kernel that has it. A measurement-only commit (60f3dd99c, branchipc-messages-actuator-qemu, never to land) made--debugboot theboot-actuators,test-actuatorskernel instead. All eight exited 0:abuse_kernel_addr,handle_lifetime,shm_release_reclaims,spawn_returns_the_census,spawn_lands_claimed,spawn_child_ends_first,counters_silentandring0_timer_in_syscall. The control is review round 1's run on a kernel withoutSYS_DEBUG, where the six that check for it failed their own vacuity guards (evidence).The AMD memcpy issue record is dropped: #1133 finds and fixes its cause in libc.
The rebase conflicted only in netstack's link opening, where #1106's follow-ups give a link its node
ifacein place of its MAC:Session::opentakes theiface.Evidence: first round, review round 1, round 2, ipcwake, round 4, landing A/B and rebase, review round 2.
At
a16f09c3a:cargo run -- --ci host: exit 0, 107/107, andcargo run -- --build-only: exit 0, both run atccaf1b1cd, which differs only by the dropped issue file. QEMU x86-64 (remote runner) ata16f09c3a, the netstack registrations, which the link's bells and the rebase's conflict touch:traffic_leaves_by_the_second_link_while_the_first_is_down(#1106's failover, new onmain),a_link_ends_under_a_held_stream_and_comes_back,netstack_holders_exported_to_children,stream_holders,a_client_killed_mid_connect_gives_its_place_backandudp_receive_times_out: 6/6 green.Landing A/B on metal (base
18060b521, headc715ebfb5, osbench, four interleaved boots per bench, all passed. It measures this PR's delta on its pre-rebase base, and the rebase does not change that delta except in netstack's link opening, which osbench does not drive; rule stated before the run: mean p50 per arm within 5% of base onipc.reqrep.{8,4096,65536},ipc.reqrep_handle.8and everyfs.*.direct.*row):ipc.reqrep.8ipc.reqrep.4096ipc.reqrep.65536ipc.reqrep_handle.8fs.disk.direct.*(5 rows)ipc.connect_acceptipc.reqrep.65536regression is gone: 66 µs at whole messages without the receive into the caller's buffer, now 30.1/30.0 µs against base's 31.6/31.6.pingpong.futex.same_cpu1.30×,pingpong.pipe.same_cpu1.31×; cross-core and tmpfs 1.0×). Round 1's base bootbase-6, a tree without whole messages, shows the same ratios (fs direct 1.22–1.26×, futex same-CPU 1.32×, pipe same-CPU 1.35×, cross-core 1.01×). That the mode is unrelated is an inference from those matching signatures; the rule as written is not met, and one more AMD pair from the same images would settle it.At
c715ebfb5(before the rebase; its one conflict was netstack's link opening, whose registrations are rerun above; the rest ofmain's changes merged without conflict and are covered by host CI and the build):cargo run -- --ci host: exit 0, 107/107.--debugshared boot:messages_are_whole(with the split receive's new check),connect_before_serve,netstack_gone_mid_bind,pipe_other_end_gone,abuse_shared_grant,abuse_connect_flood,cpal_drop_unreleased,mutual_killandwindow_refusal, all green.Earlier results, at the trees named:
Host:
cargo run -- --ci hostat343a655b1: exit 0, 107/107. The test repair touches only a guest test and a test image, which host CI does not build.Pure and SDK tests:
QEMU x86-64 registrations (remote runner,
343a655b1), 18 of 19 green:libc_children,memory_floor_holds_the_controls,usbd_drives_the_spare.libc_children'stty_interruptstalled earlier on trees carrying the scheduler commit and passed in this run; the stall also reproduces on main (1 of 4), filed as issues: libc_children's tty_interrupt can stall at the nested shell's line #1127, so neither run attributes it.The red was
session_lifecycle; see the repair below.QEMU x86-64, the
--debugshared boot (runner,d878eb102, then2ee4db1e3): every shared member green but the six actuator members, which need aSYS_DEBUGkernel. Those differ from343a655b1by the call and the scheduler commit; neither is something a shared member drives, apart from the call test now gone.session_lifecyclerepair (edc8770e4):343a655b1twice, base green.18060b5211 red of 4, head 3 red of 6.issues/a-forced-reboots-last-userland-lines-miss-the-console.md.Metal, T14 and AMD, every boot passed (images at
4fc78ac97,245f7b9af,2269ea100):messages_are_wholeandconnect_before_serveexit 0 on both.4fc78ac97), two boots per arm:ipc.reqrep.87222 → 6462 ns,fs.disk.direct.seek9010 → 7740 ns).ipc.reqrep.8,ipc.reqrep_handle.8and the fs rows.ipc.reqrep.65536doubles (31.1/51.0 → 66.3/66.2 µs), andipc.reqrep.4096sits at base's slow mode on all four boots. Fixed by the receive into the caller's buffer; see the landing A/B above.Why metal: the A/B is timing, which QEMU cannot establish, and the members are the kernel boundary's refusals and handle moves, on both benches.
Risks and open items
wake.onto_busystaging. Whole-message trees had 3 of 6 boots refused before. A placement A/B (head and base, two boots each) refused on none, so the refusal is not shown to be this PR's. On both head boots, though, osbench's bench thread migrated to CPU 0, the IRQ CPU, during staging (base: 0 of 2), and one head boot's row went unmeasured. That migration is attributed to the scheduler moving an unpinned bench thread, an inference handed to the scheduler track (evidence).copy_from_slice) is 4K store-to-load aliasing in libc's copy, found and fixed by libc: a forward copy a short way above its source modulo 4 KiB copies down #1133.session_lifecyclefails about half the time at the head (3 of 6 runs; base 1 of 4) on the pre-existing console loss above. Its fix belongs to the console/stop path. It is not a required merge check:main's ruleset requires onlyhostandimage, and the guest suite runs nightly.MAX_FRAME_LENstays 8192 under the kernel's 16 KiB message bound.FrameRxsizedMAX_FRAME_LENis 8 KiB, zeroed once, at the blocking client sites that take answers of any length.🤖 Generated with Claude Code