Repository navigation
An open file is written and stated through its paged object, one write at a time, against credit - #1258
Conversation
549c11c to
ee1f814
Compare
Review of #1258 at ee1f814 (base 67713cb)ArchitectureThe ownership matches the owner's decision:
Merges. This PR merges cleanly with #1225, with #1259 (map7b step 1, which does not touch the paged object) and with #1245 (
Required1. BLOCKER
2. BLOCKER
3. BLOCKER
4. BLOCKER
5. BLOCKER Atomicity is narrower than the manual says, and narrower than on main.
6. BLOCKER Known compromises that are not in
7. BLOCKER
8. BLOCKER The bulk-size measurement the brief requires is missing.
Optional
Evidence
The PR description's "admits … before any byte lands, or refuses it with nothing written" and "Reads stay atomic" are inaccurate (findings 5 and 7). Correcting them is the orchestrator's job. SEND BACK |
…ting them is bounded The round's ruling keeps POSIX's atomicity: writes to one file are atomic with respect to each other and to reads, a holder's, the server's or an append. Each object now has one write side. A holder's write copies its bytes into frames held aside first, then takes the side in one hold of the pager's lock, refused at once while anyone holds or waits for it, and is checked and admitted whole there; its copy into the object is the kernel's own. The server takes the side with a hold for its writes, truncations and own reads, which also answers the size the other writers left: an append through the server lands there, and a write refused part way cuts back only its own growth, never a holder's (finding 3). A holder's append reads the end under the side. A holder's read waits out any write, the server's too, and after three restarts queues for the side, which no writer takes before it, copies under it into frames held aside and copies out after giving it back: bounded, and nothing holding a file's writes off waits on a program's memory (finding 4). A host model drives a reader, two holders and the pager through every interleaving of the protocol. The span of a holder's write is clamped inside its 64 pages without overflow at the last pages a u64 reaches (finding 1). The server's write-back for room reaches files whose only dirty pages are holders' (finding 2). A page a holder's credit paid for gives the credit back where the server writes it or it goes before its write-back, so the pool holds one bulk (finding 6). Write holders are minted only where holders write, the last holder's notice waits for a holder's write under way, and the contracts say what holds (finding 7). The metadata-credit record says what a file open to write now holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…credit Every write and fstat of a served file was a request to its file server: a round trip of about 7 us on the T14 before any work, three copies of a 4 KiB write, and on DATA a reservation per call of 4 to 14 us. A holder of the file's paged object now writes it and reads its size, time and the rest of its stat with one system call, as it already read it. The kernel admits a holder's write whole or refuses it before any byte lands: every page of it present, or reading as zeros past the size or in an anonymous hole, and the credit and the dirty ceiling covering each page it turns dirty. Credit is pages the pager granted for which it holds room on the volume, so a full volume is still answered at the write: any refusal sends the holder to the server, whose write supplies, writes back or answers "no space". A holder's write never waits on its pager, so a read that meets one waits it out and reads again rather than ask the server, whose own read is not ordered against it. A fresh page whose copy faulted is removed rather than left dirty past the size. The object keeps the size and the modification time holders' writes move, and 64 bytes the pager writes and the kernel never reads. While a holder may have written a file, the file server answers its stat, listing, seek from the end and append from the object, and the volume adopts the object's size and time at each write-back and truncate, so a persisted size never covers a byte not yet written back; once no holder is left and nothing waits for a write-back, they are the volume's again. The first page a holder dirties after the pager last took the range posts one notice; a write-back, a sync, an fsync and the last holder's going take the range from the object, so a notice not yet read loses nothing. On DATA the credit is a bulk of 64 pages, held as a pool of the engine's blocks placed where each page is written back (`write_placed`, new: a reservation bound to no range of the file), made up after each of the server's own writes and taken back when the last holder goes. FAT grants none, and its holders' writes go to the server; MEMORY's objects are anonymous and need none. A sealed object refuses a holder's write as Busy (ETXTBSY), the agreed seal check. Records the per-capability turnstile, which still serialises a process's path calls, measured with this work's diagnosis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ting them is bounded The round's ruling keeps POSIX's atomicity: writes to one file are atomic with respect to each other and to reads, a holder's, the server's or an append. Each object now has one write side. A holder's write copies its bytes into frames held aside first, then takes the side in one hold of the pager's lock, refused at once while anyone holds or waits for it, and is checked and admitted whole there; its copy into the object is the kernel's own. The server takes the side with a hold for its writes, truncations and own reads, which also answers the size the other writers left: an append through the server lands there, and a write refused part way cuts back only its own growth, never a holder's (finding 3). A holder's append reads the end under the side. A holder's read waits out any write, the server's too, and after three restarts queues for the side, which no writer takes before it, copies under it into frames held aside and copies out after giving it back: bounded, and nothing holding a file's writes off waits on a program's memory (finding 4). A host model drives a reader, two holders and the pager through every interleaving of the protocol. The span of a holder's write is clamped inside its 64 pages without overflow at the last pages a u64 reaches (finding 1). The server's write-back for room reaches files whose only dirty pages are holders' (finding 2). A page a holder's credit paid for gives the credit back where the server writes it or it goes before its write-back, so the pool holds one bulk (finding 6). Write holders are minted only where holders write, the last holder's notice waits for a holder's write under way, and the contracts say what holds (finding 7). The metadata-credit record says what a file open to write now holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cd7b879 to
66702d6
Compare
|
Review r1, at 66702d6 (rebased onto main 45935f4):
S3: write holders are minted only where holders write (anonymous objects, or a volume that grants). The SDK still tries one refused write on a read-only holder; recorded under Risks. S4: New guest member: 🤖 Generated with Claude Code |
Review round 2 of #1258 at 66702d6 (base 45935f4)ArchitectureThe write side is the right mechanism.
Deadlock freedom holds. I traced every wait for the side:
Size of this round.
S1 below would remove the largest new mechanism. Round-1 findings
S3's remainder (Risks). Accepted. It affects only FAT: one refused system call before a request that costs far more. The SDK cannot see a handle's rights. S6. Adopted through the RequiredB1. BLOCKER The bug.
The size can reach
Two calls in safe Rust do it on /tmp, which is anonymous and needs no credit:
The append's page is then The same cause makes
Fix.
B2. BLOCKER The mechanism.
What a holder can see.
Fix.
B3. BLOCKER What changed.
The effect.
The manual's sentence. "A write of more than 256 KiB may be taken in parts, each whole" suggests the system takes the parts. In fact the program does. The owner's ruling and the brief do not cover this. Fix (preferred).
Otherwise. Take short writes to the owner, and have the manual say plainly that such a B4. BLOCKER The holding.
Why that matters.
Fix.
B5. BLOCKER: four comments and contracts are now false or redundant.
OptionalS1. After
EvidenceHost. Checked against
Controls. All seven are real patches, and each is red on its test's assertion. s2 failed to build at first; after a rebuild it is red. What no host test reaches. Nothing on the host reaches the kernel object's
B1's and B2's fixes each need a pure test. Model. It explores every interleaving of:
It asserts that no read is torn, at most one write holds the side, no interleaving deadlocks, and a queued read goes first. It does not model a queued read giving the side back at an absent page, which inspection covers.
Pending:
Landing conditions
For the orchestrator (PR description).
SEND BACK |
…the server's writes, and long writes go whole An append took the object's size and never checked the end against the last page, so a file grown to it by a positioned write let any process panic the kernel with an append; and an offset of u64::MAX meant append, so a positioned write there answered success. An append is now its own call, SYS_PAGED_APPEND, whose end is checked, as a positioned write's is, in the hold of the lock that reads the size; the SDK hands a holder only writes inside what a file holds. A holder's span is taken whole or refused: a write of more than 64 pages goes to the server, which writes it whole up to its window, as before this change, and no write answers a short count. While the pager holds the writes, a holder states the size and time of the moment it took them, and the server stamps its writes' time inside its hold, so fstat is whole against the server's writes too. A read that met three writes now asks the server, whose read holds them off; the queued read, its frames and its short answers go. The contracts, the module header and the manual say what holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's #1233 ends a holder's wait for an absent page where the object's version moved, and the read there answered WouldBlock. Here a holder's read that meets a write begins again instead, until it met READ_RESTARTS writes; its want of the page would then be the range it already named, which the pager answered before the write made the page absent again, so no notice would name it and the read would wait for ever. A read that begins again now asks anew (Want::anew), and main's model of the wait plays the restart. The pager's write is its hold, so main's tests take and give the write side where they began and ended a write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A read that copied across the start of a write counted it, and then counted it again when it found the write still under way, so two writes could send a read to the server where the contract says three. Every write a read meets now takes one path: it is counted, and the read waits for its end and begins again, asking anew, or at the third asks the server. The bound is named for what it counts, and the protocol model counts as the kernel does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A positioned holder's write was kept inside the file system's largest file by the SDK, but an append's end is known only in the kernel, which checked it against the last page a u64 reaches: a file grown to the largest size by a positioned write took an append past it, which every other path refuses and whose size the SDK then reads as malformed. The kernel now ends every holder's write at PAGED_HOLDER_END, 16 TiB, and refuses one past it WouldBlock as it does a long one, so the server writes or refuses it as it would any other. The SDK asserts the bound lies within the file system's largest file, and its own offset check goes: the kernel's covers positioned writes and appends alike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The benches are down, so the T14 boots #1258 was to land on are owed instead: its testcases shared boot and the osbench_disk A/B of main from before and after it, in the records of the file server's request cost and the memory filesystem's, and the bulk measurement in the pool's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 2 responseFixed in a5a4ea5; main merged at 16a50e2; two more fixes at 1b46c51 and b55f2b5; the head 1ee3e6d adds B1, fixed. An append is its own call, a5a4ea5 bounded positioned writes in the SDK at The pure test walks the pages below the reach and the last offsets a B2, fixed. B3, fixed the preferred way. B4 and S1, S1 taken. At the third write a holder's read meets, it answers B5, fixed.
One more fix, 1b46c51. A read that copied across the start of a write counted it twice: once at the copy, and Main's #1233, merged. It ends a holder's wait for an absent page where the object's version moved, and there the Evidence gap, closed. Description. The map7b note now names both places that hold pager frames across a user copy: a holder's write Guest. Run 43092689328 at b55f2b5, code-identical to the head, covers the five rows. Runs at superseded heads Metal. The benches are down, so nothing was staged. These are recorded as owed, in the PR body and as
The speed-up is not yet measured on metal. |
Review round 3 of #1258 at 1ee3e6d (code head b55f2b5, base e10b6fd)ArchitectureThe round makes the mechanism smaller, and the design is sound.
Round-2 findings
Changes since round 2
RequiredB6. BLOCKER: two comments are false after this round's changes.
OptionalS1.
A reach the pager sets for each object, at creation or with Evidence
For the orchestrator (PR description)
SEND BACK |
…ts say what holds Nothing showed the kernel admits a holder's write at all: every claim would hold if each were refused and sent to the server. pagerprobe's helper now writes through write holders its pager mints. On anonymous memory it checks a page's count, an append's new size, the size and moved time a statement answers, the bytes read back, WouldBlock with nothing changed for a 65-page span and for a write past a holder's reach, and PermissionDenied from a read-only holder; it then writes a page to an object granted a page of credit, whose Dirtied notice the pager waits for before it reads both objects back as their owner. The pager's quota grows by the three pages those writes install, with a dirty ceiling of one. Hold's contract says it waits only for a holder's write under way, and wait_over that a read whose version moved begins again. The kernel's reach for holders is worded as the ABI words it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 3 responseFixed at 4a2d78d. Evidence: B6, fixed.
S1, constant kept. The pure doc is now worded as the ABI's: "the kernel's reach for holders: one past it is refused and is the pager's to write, and a pager that mints write holders accepts that no holder grows an object past it." Holders' writes admitted (the orchestrator's requirement). The pagerprobe helper ( On anonymous memory, which needs no credit, the helper asserts:
It then writes a page to a non-anonymous object the pager granted one page of credit. The pager waits for that object's
To fit the writes, the pager's quota grows by those three pages, with a dirty ceiling of one and five objects. The probe's room assertions now expect those three pages. Built and controlled.
Guest.
Description. The body now says that whoever waits for the side waits only on a holder's copy, and that the server holds the side across its own disk I/O. |
Review round 4 of #1258 at 4a2d78d (base e10b6fd)ArchitectureNo mechanism changes this round. The head commit touches three files (+114/−20): two contract comments, the pure doc for the holder reach, and pagerprobe (about +100 net). pagerprobe is the right home for the admission evidence. It is a program with the pager role, so it mints holders whose writes only the kernel answers, with no file server and no SDK fallback in the way. Round-3 findings
Does the probe establish admission?Yes, once it runs green. I traced every check against the kernel at the head. State when the helper writes.
What each check catches.
The control.
So the red is specific to admission. The control is built ( RequiredB7. BLOCKER:
OptionalS1. The probe now carries claims that its launcher and labels do not name:
A holder-write regression will be reported as red in a test named after the pager's end. Either drop the inventory sentence from the launcher, since pagerprobe's header owns it, or name the holder writes in all three places. S2. The helper paragraph of the header (lines 18-25) lists each assertion, which the code and its messages already state. One sentence would do: "its writes through write holders land, are stated and read back; one past a holder's reach, or through a holder minted to read, is refused with nothing written." Evidence
What pagerprobe's run must showThis applies once the
For the orchestrator
SEND BACK |
…n is owed The 65-page write could not fail for a missing span limit: with one free slot left, the pager's lack of room refused it first, answering the same WouldBlock. It goes; the limit is the pure holder_span's, host-tested with its control red. pagerprobe's helper says in one sentence what its writes show, and its launcher, its description and its image name the holders' writes too. The record of the file server's request cost now owes pagerprobe's run and its control on the testcases shared boot's guest row, with what each must show, and names what no test shows yet: that an open on /tmp or /home mints a write holder that the SDK writes through first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 4 responseFixed at 230a2ad. B7, deleted, as the orchestrator decided. The 65-page write and its message are gone. The span limit is the pure S1, S2, taken.
The gap and the owed run. These are recorded in
The body's landing paragraph now says the run is owed and recorded, and no longer says the PR lands on it. Control branch. Checks.
|
B4. The memory track's step 2 bullets name every boot and group owed on metal and what main must show: the shared boots with `mapped_file` and the pager-end test, the A/B of the groups the fault and copy paths run under with the process-exit cost every end now pays, and the `map` group beside Linux with each row's ratio. It records the pager-wide wakes (a departure from §3/S4) and the revocation visit's cost with the rows that price them, names step 3 as R4's, keeps the cold DATA miss as a row still to write, and records the pinned-copy test owed with #1258. libc's `msync` reason is what is true: a file is mapped read-only only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 5 of #1258 at 230a2ad (since 4a2d78d)Scope.
Nothing else changed. B7 resolved. The 65-page check is deleted, and so is its S1 and S2 taken.
PR body. True:
Record. The control, the remaining gap and the metal rows are stated truly, with one exception. RequiredB8.
EvidenceThere has been no run since round 4, and none was needed: the change deletes a self-contained check and edits text. SEND BACK (one line of the record) |
…cess orders The record required the helper's credited write to print before the owner's read-back, but the pager reads back on the Dirtied notice the kernel posts inside that write, so either process may print first. The record now requires only the orders that hold, and places the credited write's line anywhere between the helper's admitted writes and the pager's end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 5 responseB8, fixed at 695146e. The record now requires only the orders that hold:
The helper's "a page written against credit" can fall anywhere between its writes admitted and |
|
Orchestrator verification of round 5 at 695146e: only |
Review r8 of #1258 at a2e4dfd (main d1cefba merged into 4240a3d)Scope.
Architectural assessmentThe composition is the right one. #1258's two flags, two counters and two walks give way to a second list in #1273's table.
The guarantees, traced.
Slot size. The log (
RequiredB1.
B2.
SuggestionsS1.
S2. Two windows in round 1's S6, as adopted, both from before the merge. The composition keeps them exactly.
PR description (the orchestrator's)
EvidenceThe author's r8 logs (07e063af), all rc 0 except the file server's clippy:
The tree tested.
The control branch. Guest.
Metal: none. It is not a landing condition while the benches are down. SEND BACK (B1: two tokens in a test; B2: one ABI kind and its mappings. Neither needs a guest run.) |
…he last holder is refused The seal refused a holder's write as `Busy`, which main's #1261 had made an inbox submitter's word, so one kind said two things and libc mapped them to one errno. The seal now answers `TextBusy` (12): libc gives ETXTBSY and std `ExecutableFileBusy`, and `Busy` keeps its one meaning. A holder's write that begins once the object's last holder went is refused as `Gone`: the pager may already have been told no holder is left, and the write would dirty what it was told no one holds. The no-holders notice is re-checked against writes under way as well as holders when it is taken. `owe_dirtied` goes; its two callers mark and unmark the dirtied list directly. The slot table's test marks slot 3 on both lists, so a removal that unmarks only one list fails it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main 3c5c555 (#1271) makes the testcases list a guest row, `testcases_members_exit_0`, which judges each member by its own exit; pagerprobe and the two served-file members this branch adds are members of that list. No file is changed on both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 8, at d1a8554; main 3c5c555 merged as 3581821.
Checks at d1a8554, all rc 0: every kernel library test (510, Guest runs:
43142627680 at a2e4dfd was already in progress, so it was not cancelled. |
B4. The memory track's step 2 bullets name every boot and group owed on metal and what main must show: the shared boots with `mapped_file` and the pager-end test, the A/B of the groups the fault and copy paths run under with the process-exit cost every end now pays, and the `map` group beside Linux with each row's ratio. It records the pager-wide wakes (a departure from §3/S4) and the revocation visit's cost with the rows that price them, names step 3 as R4's, keeps the cold DATA miss as a row still to write, and records the pinned-copy test owed with #1258. libc's `msync` reason is what is true: a file is mapped read-only only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r9 of #1258 at 3581821Scope. Only what round 8 changed:
I ran no tests or builds. The results below are the author's r8b logs. For each I checked the command, the exit code and the tree it ran on. Architectural assessmentRound 8 is small, and each change sits where it belongs.
Round 8's findingsB1: resolved.
B2: resolved, as the orchestrator decided.
S1: adopted.
S2: adopted, and the reasoning holds. I traced each order of a close against another thread's write under
The merge 3581821It composes.
RequiredB1.
SuggestionsNone. PR description and history (the orchestrator's)
Evidencer8b at d1a8554.
Guest. Both runs above are still Metal. None. It is not a landing condition while the benches are down. SEND BACK (B1: one record bullet, markdown only, finished once the orchestrator has judged the queued runs. Round 8's B1, B2, S1 and S2 are resolved.) |
B4. The memory track's step 2 bullets name every boot and group owed on metal and what main must show: the shared boots with `mapped_file` and the pager-end test, the A/B of the groups the fault and copy paths run under with the process-exit cost every end now pays, and the `map` group beside Linux with each row's ratio. It records the pager-wide wakes (a departure from §3/S4) and the revocation visit's cost with the rows that price them, names step 3 as R4's, keeps the cold DATA miss as a row still to write, and records the pinned-copy test owed with #1258. libc's `msync` reason is what is true: a file is mapped read-only only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…record says who measures again B19. The `Mappers` doc pointed at `Object::epoch`, which r8 replaced with `Object::link`. `Pages::object` said a released object's id may be reused because `release` takes its pages, but `release` went in the composition with #1273. The docs now say a node is seen at the epoch `link` answers, and an id is reused once its object is empty: `invalidate` marks its pages and the walk's end takes them. B20. The /tmp record now says that whichever of #1269 and #1258 lands second measures the combined slot again, updates the figures and deletes the sentence. #1258 rewrites the same paragraph for its own slot growth. S1, S2: `Entry.refs` says only what it keeps: the slot, free with the last reference. It also counts mappings, walks, queued removals and writes, which the old list missed. The record says every entry carries the 64 bytes of mapping state, not only a mapped object's. Doc comments and a record only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, and the control's run is recorded `a_served_file_reads_through_its_paged_object` asserted that an open to write alone carries no holder. That was main's rule, an open to read alone being answered with one; #1258 answers every open file with a holder, minted to write where opened to write, and the member never followed. The control's run of `testcases_members_exit_0` (guest-rows 43146956481) reddened it at that assertion, before it writes anything, so the head reds it as well. It now takes the writer's holder and drops it before the writer's close, so its reopen still follows the last close. The served-file record names the row and states the control's result: the pager member red, pagerprobe exiting 101 after 30.49 s of system calls, its 30-second wait for the helper's dirtying, and every member but the stale one green. The panic text was not kept; the serial record takes that run as its second instance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 9, at cb8701b.
Guest: 43152496728 at cb8701b runs the five rows and |
B4. The memory track's step 2 bullets name every boot and group owed on metal and what main must show: the shared boots with `mapped_file` and the pager-end test, the A/B of the groups the fault and copy paths run under with the process-exit cost every end now pays, and the `map` group beside Linux with each row's ratio. It records the pager-wide wakes (a departure from §3/S4) and the revocation visit's cost with the rows that price them, names step 3 as R4's, keeps the cold DATA miss as a row still to write, and records the pinned-copy test owed with #1258. libc's `msync` reason is what is true: a file is mapped read-only only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…record says who measures again B19. The `Mappers` doc pointed at `Object::epoch`, which r8 replaced with `Object::link`. `Pages::object` said a released object's id may be reused because `release` takes its pages, but `release` went in the composition with #1273. The docs now say a node is seen at the epoch `link` answers, and an id is reused once its object is empty: `invalidate` marks its pages and the walk's end takes them. B20. The /tmp record now says that whichever of #1269 and #1258 lands second measures the combined slot again, updates the figures and deletes the sentence. #1258 rewrites the same paragraph for its own slot growth. S1, S2: `Entry.refs` says only what it keeps: the slot, free with the last reference. It also counts mappings, walks, queued removals and writes, which the old list missed. The record says every entry carries the 64 bytes of mapping state, not only a mapped object's. Doc comments and a record only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r10 of #1258 at cb8701bScope. The delta 3581821..cb8701b: one commit, three files, +26 −10. It changes one guest member, adds one record bullet, adds one paragraph to the serial record, and corrects the PR body. What I checked:
I ran nothing. The member: right against #1258's holder semantics
The records and the bodyThe file-server record: true, and r9's B1 is resolved.
The PR body: true where I checked.
RequiredB1.
SuggestionsS1.
Evidence
SEND BACK (B1: two phrases in the serial record. Otherwise the delta is right, and nothing beyond the pending head run needs running.) |
B4. The memory track's step 2 bullets name every boot and group owed on metal and what main must show: the shared boots with `mapped_file` and the pager-end test, the A/B of the groups the fault and copy paths run under with the process-exit cost every end now pays, and the `map` group beside Linux with each row's ratio. It records the pager-wide wakes (a departure from §3/S4) and the revocation visit's cost with the rows that price them, names step 3 as R4's, keeps the cold DATA miss as a row still to write, and records the pinned-copy test owed with #1258. libc's `msync` reason is what is true: a file is mapped read-only only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…record says who measures again B19. The `Mappers` doc pointed at `Object::epoch`, which r8 replaced with `Object::link`. `Pages::object` said a released object's id may be reused because `release` takes its pages, but `release` went in the composition with #1273. The docs now say a node is seen at the epoch `link` answers, and an id is reused once its object is empty: `invalidate` marks its pages and the walk's end takes them. B20. The /tmp record now says that whichever of #1269 and #1258 lands second measures the combined slot again, updates the figures and deletes the sentence. #1258 rewrites the same paragraph for its own slot growth. S1, S2: `Entry.refs` says only what it keeps: the slot, free with the last reference. It also counts mappings, walks, queued removals and writes, which the old list missed. The record says every entry carries the 64 bytes of mapping state, not only a mapped object's. Doc comments and a record only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erial record says what its tail kept Guest-rows 43152496728 ran `testcases_members_exit_0` at cb8701b: all 242 members exited 0, the pager member and the two served-file tests among them. The served-file cost record states that run where it listed the run as owed, and what a green run does not show: the order of the probe's lines and the absence of a paged overdue report, which the T14's shared boot still owes. The serial record's instance said the verdict's 40 lines held only pagerprobe's backtrace and that `uart-0.log` stopped before the first member. They held the end of that backtrace and the pager member's own panic and backtrace, and `uart-0.log` stopped at 3.984 s, after the pager member started and before pagerprobe printed anything. What neither kept is pagerprobe's own panic message. Text only: the code is cb8701b's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 10, at 001f812: text only over cb8701b.
|
|
Orchestrator verification at 001f812: a text-only commit over cb8701b that touches two issue records and nothing else (
Queueing. #1272 follows once this merges and it is retargeted to main. |
#1258) #1258 gave a paged object a write side, holders' writes against credit, a seal, a pager's record and a notice of holders' dirtying. This branch makes a removed page revoking until no mapping reaches it. The resolution keeps both sides, and makes them meet where a page is revoking: - A revoking page takes no write. `admit` refuses it as it refuses an absent page. `holder_fits` finds it `Absent`, so a holder's write over it goes to the pager and `admit_fresh`'s checked install never meets it. - The room a holder's credit paid for a page comes back as the page is marked revoking, as #1258 gives it back for a page gone before its write-back. The marking resets the page's state, so the walk's end gives nothing back twice. `a_revoking_page_takes_no_write_and_gives_its_credit_back_once` holds this. - `resize` keeps #1258's seal and its trimming of the dirtied range, and marks the pages past the size revoking rather than giving their frames out. `fail` and `invalidate` keep this branch's revocation. #1258's tests of shrinks, fails and releases settle their walks as this branch's do. - `Want::anew` is #1258's, a fresh `NONE`. The model's fault that asks afresh asks its run after it. - The slot table's two lists: a release unmarks both, and a mapping's end goes through `say_no_holders`, which waits out holders' writes too. - `PagedHolderObject::write` and `stat` sit beside `maps` and `link`, and `Paged::map` beside `write`, `append` and `stat`. A map takes the holder through `syscall::paged::holder` with `READ`. #1258's ABI range of dirtied pages is named `abi::PagedRange` beside the kernel's `PagedRange`. - libc's map of a served file goes through `on_fd_held`, whose holder closure #1258 made answer a `Result` of the call's own: `toyos_fs_map`'s does. - A truncation and the pager's writes need its hold now. `mapprobe` holds around its write in `coherent` and its shrink in `shrunk`, mints its holders to read, and makes its pager with a dirty ceiling of 2. At 0, the `coherent` case's own write was refused `DirtyFull`. - The kernel's overdue report says "wait overdue" since #1258, and the mapped-file row reads it so. The `/tmp` record measures the combined slot, since this lands after #1258: - A slot is 272 bytes: a 248-byte entry and two 12-byte marks. - The object's 152 bytes include the 48 of mapping's walk state. - It was 216 on main. - The sentence on whichever lands second goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rames while it waits #1258's holder write copies its bytes from user memory into frames it drew from its pager's core, before it takes the write side. The source may be a page of a mapping that is absent. The copy then parked on the page's pager while it still held the frames, and frames are what that pager's supply draws. If holders' writes sourced from a mapping of the same pager drew the core dry, the pager could not supply the page they wait for, and none would end. This is the orchestrator's ruling for pinned copies, that no wait holds what its pager needs, and the staging copy now takes the same mode. It runs under `user_ptr::Pinned`, so an absent page stops it. The write gives every frame back and waits for the source pages holding nothing (`wait_in`). It then draws and copies again, against the object as it stands then. A copy that faults answers its own cause, `BadAddress` or `Io`, as every other copy since B5 does, where #1258 flattened it to `BadAddress`. `Pinned`, `Copies` and the fault handler's docs say what the mode guards: a copy that holds what no wait may hold, page pins or a pager's frames. `mapprobe`'s `written` case is the duty the memory record kept for whichever of #1258 and #1269 landed second. The case: - endows a mapper with a write holder of an empty object granted a page of credit; - the mapper writes it from the first page of its mapping, which no one has supplied; - the pager hears the request and supplies the run; - the mapper's write lands whole, and the pager reads the page back as the supplied bytes. The record's paragraph on the owed test goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1258 gave the pager one wait that reports itself overdue past the report time, `wait_changed`, and one draw of frames, `draw` with `materialize`. A fault's wait and a touched hole's zeroed frame each had a copy of those. They now call the pager's. The wait still requests the fault's run and lasts until the page is no longer absent, the version moved or the pager is gone. The hole's frame still comes from the pager's core, with the same refusal where there is no room. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…say what holds B1. In the combined layout, the walk state mapping needs adds 40 bytes to the object: 152 against main's 112. The record's 48 was r9's figure on the layout before #1258. The record now says 40 to the object, and with the list of mappers 56 to a slot. It also says that main's 216 was measured on x86-64. B2. `draw`'s doc no longer says its frames are completed outside the lock: `Index::zeroed` materializes its frame under it. The module header says a holder's write copies into frames drawn first, not held aside. S1. The comment over the holder write's staging copy repeated its doc and the module header, so it goes. `faulted()`'s doc and panic name a copy outside `Pinned`, which is what they mean now that `Pinned` guards a pager's frames as well as pins. S2. `mapprobe`'s `written` case says what it establishes: the write stops at the absent page, waits, draws and copies again, and lands whole with the supplied bytes. That the frames go back while it waits is the code's to show. Doc comments, a panic message and a record only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
Every write and
fstatof a served file is a request to its file server. On the T14 the round trip alone is about7 µs with nothing in it (the split on metal, PR #1225's evidence), a 4 KiB write copies its bytes three times, and on
DATA each write call reserves room on the volume, 4 to 14 µs more.
fs.mem.fstatreads 7.6 µs against Linux's0.21,
fs.disk.write_4k_at22.4 µs against 0.72,fs.mem.seq_write.4096438 MB/s against 3,605.Change
A holder of a file's paged object, which an open already hands the client for reads, now also writes the file and
reads its stat without a request:
SYS_PAGED_WRITE,SYS_PAGED_APPENDandSYS_PAGED_STAT. Every open of aregular file on a server with a pager is answered with a holder, minted to write where the file is opened to write
and holders can write it.
bytes into frames held aside first; then, in one hold of the pager's lock, it takes the side, is checked whole
(its span at most 64 pages and ending within a holder's reach, every page present or reading as zeros,
the credit and the dirty ceiling covering each page it turns dirty) and is admitted; its copy into the object is
the kernel's own, and the side is given back. It never waits: while anyone holds or waits for the side, or any
check fails, it is refused with nothing written, and the SDK sends the write to the server. A write longer than
64 pages therefore goes to the server, which writes it whole up to its window, as before this change; no write
answers a short count. The server takes the side with
Holdfor its own writes, truncations and reads, howevermany calls and waits they take, and is told the size the other writers left: its append lands there, and a
write it cuts back after a refusal loses only its own growth.
SYS_PAGED_APPENDreads the end under the side and checks it in that same hold of the lock; no offset value means "append". No
holder's write, positioned or appended, ends past
PAGED_HOLDER_END(16 TiB, the largest file the file systemhas, which the SDK asserts): one that would is refused as a long one is, and the server writes or refuses it as
it would any other.
server's included, and reads again where one began meanwhile. At the third write it meets it answers
WouldBlock, and the SDK reads through the server, whose read runs under its hold; a waiting server goes beforeany new holder's write, so the bound holds. While the server holds the side, a holder's
fstatanswers the sizeand time of the moment it took it, and the server stamps its writes' time inside its hold. Whoever waits for the
side waits only on a holder's copy inside the kernel, never on anything a holder controls; the server holds it
across its own disk I/O.
volume is still answered at the write and never at a later write-back. On DATA that room is a pool of the
engine's blocks, 64 pages a file, placed where each page is written back (a new engine reservation bound to no
range of the file,
write_placed); it is made up after each of the server's own writes and taken back when thelast holder goes. A page a holder paid for gives the credit back where the server writes it itself or the page
goes before its write-back, so the pool holds one bulk beside the pages owed. FAT grants none, so its holders
read and stat and its writes go to the server; MEMORY's objects are anonymous and need none. A sealed object
refuses a holder's write as
TextBusy, a kind of its own (ETXTBSY,ExecutableFileBusy).the server writes (kind, links, volume, node, the other times) that the kernel never reads. While a holder may
have written a file, the server answers its stat, listing and seek from the end from the object; the volume
adopts the object's size and time at each write-back and truncate, so a persisted size never covers a byte not
yet written back, and once no holder is left and nothing waits for a write-back they are the volume's again.
per write); a write-back, a sync, an fsync, a write-back for room and the last holder's going take the range from
the object, so nothing a holder wrote waits on a notice being read. The last holder's notice waits for a holder's
write still under way.
Main's #1233, merged here, ends a holder's wait for an absent page where the object's version moved. A read here
then begins again rather than answering, so a restarted read now asks for its pages anew: the range it named
before may have been supplied and then made absent again by the write it met, and the pager would otherwise never
hear of it.
The manual's memory and files pages and the interface page say the design and the guarantee.
Checks
Round 8 at d1a8554, and main merged at 3581821.
TextBusy(above). A holder's write that begins once the object's last holder went is refusedGone, and the no-holders notice is re-checked against writes under way as well as holders.owe_dirtiedwent; its two callers mark and unmark the list. The slot table's test marks slot 3 on both lists.sched-check, the kernel's three clippy shapes, the SDK's tests (215) and two clippy shapes, the ABI's tests and clippy, and thetestcasesimage, whose sysroot builds libc's and std's new arms for both architectures: rc 0 at d1a8554. The control for the slot table's test, a removal that unmarks only list 0, builds and reds it on the list-1 assertion. Logs: r8b/checks.cargo test --test toyos-checksrc 0 (r8b/merge).Main merged again at a2e4dfd (main d1cefba, with #1273's on-demand slots). Five files conflicted:
Slotsnow keeps a const number of mark lists; a removal unmarks every list. The pager's table has two,NO_HOLDERS(A pager's objects are charged as they are made, and /tmp holds its files by that #1273's) andDIRTIED. A holder's first dirtying marks the second andtake_dirtiedunmarks it,has_noticesasks both,noticestakes from both, and a release unmarks both. So no walk over every slot comes back but the pager's end.addmakes An open file is written and stated through its paged object, one write at a time, against credit #1258's entry, with its writers and record, in the slotmake_slotcharged for.holder_gonecounts down andsay_no_holdersmarks only once no holder's write is under way;noticessays no holders only while there are still neither holders nor writes under way, and a holder's write that begins once the last holder went is refusedGone(round 8), so no write dirties an object its pager was told no one holds.unref_lockedis A pager's objects are charged as they are made, and /tmp holds its files by that #1273's.pager_create,Pager::createand pagerprobe take no object count.Busycame from both sides. Main's A poll belongs to the handle it was made through, and a ring to its one submitter #1261 made it an inbox submitter's word (EBUSY,ResourceBusy), and it keeps that one meaning. The seal has its own kind,TextBusy(12): libc answers ETXTBSY and stdExecutableFileBusy(round 8).keepsand An open file is written and stated through its paged object, one write at a time, against credit #1258's record./tmprecord's per-file cost says so, measured as A pager's objects are charged as they are made, and /tmp holds its files by that #1273 measured it (r8/slot-sizes.log).At the merge: every kernel library test (510), the kernel's three clippy shapes, bcachefs (208) and the file server (132 + 3) with their clippy (main's four findings), the SDK, the ABI, and the
testcasesimage, rc 0 (r8).Main merged again at c809599 (64 commits). Four files conflicted, and one call stopped compiling.
write_runand gave reserved writes their own path: plan every piece, send each stretch of contiguous sectors once, then point the extents, eight fresh ones of a bucket a transaction. The pool's placed writes now take that same path (round 6, 66de9f9): they plan their pieces onto the pool's lowest unwritten blocks with a cursor and share the reserved write's send-then-point phase, which counts blocks written by sector.write_runis main's. A placed write checks the volume is writable before its bytes go, because they now go before any transaction. A test shows a placed write over contiguous pool blocks sends each stretch once before any pointer, and a refused send leaves the file and the pool as they were. Logs: ea5a9d8c/r6.The kernel's three clippy shapes and pure tests, the engine's and the file server's tests and clippy, and the SDK's and ABI's checks, all at the merge: rc 0 (the file server's clippy findings are main's four). Logs: 7700bf33/r2/step1-merge.
Host, every crate the change touches. The round-2 fixes at a5a4ea5, main merged at 16a50e2 (its kernel, file
server and engine checked before #1225 joined it, which touches only the SDK's
filesmodule), the read's countat 1b46c51, and a holder's reach at b55f2b5; 1ee3e6d adds only issue records, 4a2d78d pagerprobe's
holder writes and comments, 230a2ad pagerprobe's labels and records, 695146e a record line, c809599 main
merged, 66de9f9 the pool's writes on the reserved write's path (above), 4240a3d the data slot's
contract restated and the pool flag passed by its caller, and the head, a2e4dfd, main merged with #1273. Logs:
00bdb14a/r2 and
de2e2055/r3, each ending
with its exit code, the README saying which tree each set ran on.
cargo test -p kernel --lib paged::u64holds), credit a holder paid for, main's wait tests and model with a read that begins again, and a model driving a read, two holders' writes and the pager's through every interleavingcargo test --manifest-path system/fileserver/Cargo.tomlcargo test -p bcachefs --libtests::data::at the merge)cargo test -p toyos --lib fs::andfiles::,cargo test -p toyos-abikernel(host, x86-64, aarch64),toyos(two),bcachefs,toyos-abi, warnings deniedsystem/fileserver, every targetNegative controls, each a patch applied, built and run, then restored, every one red on its test's assertion:
an append's end left unchecked (B1, the pure span helper);
statanswering the live size while the server holds(B2); the server stamping after its hold (B2); a long span clamped rather than refused (B3); a holder's write
taking the side while the server waits for it (S1, the interleaving model); a restarted read that does not ask
anew (main's wait model, which then leaves a read waiting for a page nobody was asked for); and a holder's span
checked against the last page rather than its reach. Round 1's controls stand for what this round did not change.
Guest, all passed (run 43152496728, at cb8701b, whose code the head 001f812 keeps; log; earlier runs
were at superseded heads or ended in
startup_failurewhile Actions was degraded):a_row_reaches_its_own_state_and_no_other,the_data_partition_beside_the_stick_is_designated,the_log_lands_on_the_boot_stick(LOG on FAT: its holders cannot write, so every write goes to the server),nvme_disk_boots,virt_user_mode(aarch64: the paged object is shared kernel code), andtestcases_members_exit_0(below). 43142627680 passed the five rows at a2e4dfd; 43146950193, at 3581821,was cancelled while queued once the control showed the stale member it would red. Runs at superseded heads
(43076343465, 43090603584, 43092641282) were cancelled before they ran; 43092689328's cancel is refused by GitHub
as already complete though it reads queued. 43096096727 ended in
startup_failurewith no job.That holders' writes are admitted is pagerprobe's claim. Its helper writes through write holders its pager
mints. On anonymous memory it checks:
WouldBlockwith nothing changed for a write past a holder's reach;PermissionDeniedfrom a read-only holder.The 64-page span limit is the pure
holder_span's, host-tested with its control red; pagerprobe has no roomthat would let the limit alone refuse a longer span.
It then writes a page to an object granted a page of credit. Once the pager has that object's
Dirtiednotice,it checks
take_dirtiedand the sizeHoldanswers, and reads both objects back as their owner.a_pager_s_end_answers_its_waiting_holders_gone, which starts pagerprobe, and the served-file tests(
a_served_files_read_never_tears_against_its_write,a_served_files_writes_land_whole) are members of thetestcasesshared boot, which #1271 made the guest rowtestcases_members_exit_0, each member judged by its ownexit. Run 43152496728 at cb8701b, the head's code (001f812 adds only issue text), passed it: all 242
members exited 0 and the list's reboot ended the boot (log). A green run keeps no console, so the order of
pagerprobe's lines and the absence of a paged overdue report are not shown; the T14's shared boot owes the latter.
Its negative control is the kernel refusing every holder write: branch
fileops-step1-ctl-refuse, c97966e (thehead 3581821 and that one patch). Run 43146956481 there went red on two of 242 members
(r9/control-run):
a_pager_s_end_answers_its_waiting_holders_gone: pagerprobe exited 101 after 30.49 s of system calls, its30-second wait for the helper's dirtying of the credited object. The panic text was not kept
(
issues/a-red-run-keeps-no-serial-for-a-boot-whose-uart-is-qemus-stdio.md, which takes this run as aninstance), so the reason, the helper's first holder write answered
WouldBlock, is inferred from that timingand the code, not read.
a_served_file_reads_through_its_paged_object, at its first assertion, "an open to write alone carries noholder", before it writes anything; it writes only through the server. Not the patch's effect: the assertion
was main's rule, and this change answers an open to write with a holder too, so the head would red it as well. Round
9 (cb8701b) has it take the writer's holder and drop it before the writer's close; the member type-checks
against the ToyOS sysroot (r9/member), and the run at cb8701b passed it.
What the run must show and what the control showed are recorded in
issues/a-file-server-request-costs-three-to-twelve-times-a-kernel-file-call.md.So is the gap no test closes yet: that opening a file on
/tmpor/homemints a write holder, and that the SDKwrites through it before it asks the server, rests on inspection and on the owed
osbench_diskrows.Metal: none. The benches are down, so this change is not measured on metal: its speed-up is not yet measured, and
no metal row has run it. Owed on metal, recorded in the issue records:
testcasesshared boot on main after this change: every member passes, the two served-filetests and
a_pager_s_end_answers_its_waiting_holders_goneamong them; no paged overdue report;--fat32-checkclean (
issues/a-file-server-request-costs-three-to-twelve-times-a-kernel-file-call.md);osbench_diskon the T14, main from before this change and after, two boots a side in A, B, B, A order:fstatabout 1 µs or below on both volumes,
write_4k_ata few µs,seq_write.4096up (on /tmp well above 438 MB/s),DATA's 1 MiB sequential write, listings and path calls within main before's spread, no row beyond twice main
before's (the same record, and
issues/a-memory-filesystem-call-costs-toyos-up-to-35-times-linuxs.md);osbench_diskon the T14 at bulk 16 and 256 pages beside main's 64, scratch builds that never land, asthe measurement behind the bulk, not a bar
(
issues/a-reserved-data-block-holds-a-transactions-metadata-credit-until-written.md).Risks and what is left
The kernel's write side is exercised only in guests. The host tests reach the protocol on the pure object,
through every interleaving of the model, and the file server against a fake; nothing on the host runs the kernel
object's
holder_write,readorhold. On the guest, the two served-file tests and the pager member(
testcases_members_exit_0, 43152496728, green) show whole writes and reads, whole appends and a holder's writeadmitted against credit, under QEMU's concurrency on two vCPUs. Nothing has run them on metal, and a refused
write begun after the last holder went rests on inspection.
Copies that touch a program's memory while holding pager frames, for map7b's step 2. Two places:
holder_write's staging loop) while holding up to128 frames drawn from the pager's core, one for each 4 KiB of its bytes and one for each page it would
install; it holds no lock, pin or write side there;
read's copy step, as before this change) whileholding pins on up to 16 of the object's frames.
Once step 2 lets a caller's buffer be an absent page of a mapped file, either copy can park on that page while
holding those frames. Step 2's pinned-copy mode (stop, give back or unpin, fault in holding nothing, retry) is for
these two places. Before step 2 no user page is a parked paged page, so nothing here waits so.
A holder's write draws its frames from the pager's core: under memory pressure it is refused and goes to the
server, which answers as before.
The SDK cannot tell a holder minted only to read from one that writes, so on FAT, and on a file opened to write
where holders do not write, each write still makes one refused system call before it goes to the server.
Each DATA file a holder may write holds 64 reserved blocks and their metadata credit, 512 KiB, while it is open;
recorded in
issues/a-reserved-data-block-holds-a-transactions-metadata-credit-until-written.md.system/fileserverhas four clippy findings under the lint shapes, all in lines this PR does not change and thesame at main (
listing.rs, two inpages/tests.rsfrom earlier commits,writeback.rs); not fixed here.Path calls (open, stat by path, rename, unlink) are unchanged; they are the next steps (the server's resolution by
node, then the round trip itself). The per-capability turnstile no longer serialises a process's writes and
fstat, and stays for the rest (issues/a-process-s-threads-take-turns-..., recorded here, to be removed next).🤖 Generated with Claude Code