Repository navigation
mm: the vm_* lifecycle calls, std and libc on them, and the hot paths the T14 measured (memory stage 3a) - #1008
Conversation
d851d99 to
d4e282f
Compare
9ceccdc to
46c3fe0
Compare
Review r1: #1008 at
|
46c3fe0 to
592d491
Compare
d4e282f to
1877d8b
Compare
|
Review round 1 answered: the clear contract was rewritten with #1003's fix; the clock page's PermissionDenied is tested and its control reds; libc pthread stacks have a guard page; 206_libc_refusals is green on the T14. Per the coordinator's classification, the x86 all-CPU shootdown is fixed before landing (36e0bab, targeted, loom-modelled, both halves controlled), and the T14's measurements added two hot-path fixes: the black box's per-fault write-back and lazy segment copies at spawn. Evidence: https://github.com/ToyOSOrg/evidence/tree/dced920e2be8b42b56c8c2b54daa83ab9a36b8d3/r2. |
1877d8b to
0c066de
Compare
592d491 to
c7d963d
Compare
Review r2: #1008 at
|
Review of #1008: targeted x86 shootdown, black-box write-back, lazy segment copiesScope: three of #1008's commits at I read:
The evidence's Targeted shootdownArchitectural assessmentThe design is sound and the right size.
That is the Dekker pairing the loom model checks, and the control ( I checked these points, and each holds:
FindingsBLOCKER 1. A CPU leaves the running set before it stops using the space's tables.
Fix, which costs nothing: clear BLOCKER 2. BLOCKER 3. Fix: make the claim a count. For example, compare the issuer census ( Owed and recorded, not blocking. The PCID property itself has no automated test, because TCG has no PCID. That property is that a switched-out CPU keeps entries that the flush on entry must remove. The track issue records the metal case and its exit, which waits on pinned threads. The loom model is the proof of the protocol. The INVPCID type 3 semantics are the SDM's. Run Suggestions
Black-box write-back on faultsThe narrowing is correct. BLOCKER 4. The SAFETY comment is false on the path this change speeds up. Fix: either skip the seal for user-mode page faults, as the cheapest option. Such a fault is served or ends its process, and a fault inside its handler is a kernel-mode fault that seals itself. Or give each CPU a record slot. At minimum, correct the comment and record the race and the cross-CPU line traffic in The gain is inferred from the 256 lines and is not measured. It belongs in the stack-head re-measurement. Lazy segment copiesThe change is correct, and it restores
A suggestion: the source object now stays alive and charged to the spawner for as long as any child runs a range of it, while the child is also promised the range in full (#1010). Two commitments then exist for the same image while a child lives, until shared text (stage 4 and the VerdictThe design and the two performance fixes are right. Fix the leave ordering, the CPU-index panic, the QEMU upper bound in the test, and the black box's single-writer claim. SEND BACK |
0c066de to
78e83ba
Compare
c7d963d to
5cd37f6
Compare
… T14 notes; masked
|
Response to the shootdown review (SEND BACK, four blockers), at
Evidence: https://github.com/ToyOSOrg/evidence/tree/0966b10a03bba923c274e82daa560bbb57044a52/r3. |
5cd37f6 to
7125a0e
Compare
78e83ba to
2d271c9
Compare
…awn snapshot, spawn_children_cap; masked
Review r4: #1008 at
|
| p50 | |
|---|---|
| head | 217,579 ns |
base 35e930136 |
816,571 ns |
dadeebe0a |
6.41 ms |
The regression is gone; the head is now 3.8× faster than the base. One run each.
Other T14 rows. proc.spawn_wait.tiny is 54.6 µs against 759 µs, and thread.spawn_join 30.5 µs against 190 µs.
For the orchestrator. The PR body still says "Metal is staged, not run" and still quotes dadeebe0a's 6.4 ms. Its "confirms the inferred cause" for first touch compares builds that differ in more than the black-box change, so it is an inference, not a confirmation.
SEND BACK: the four blockers above. All of them are small.
…s and control; masked
|
Review r4, in
I did not take the two suggestions: reading the header and TLS from the snapshot, and a segment-only snapshot. Evidence: https://github.com/ToyOSOrg/evidence/tree/505932afaa9e65cee3e9e590e5206c7b867b4708/r5 |
Review r5: #1008 at
|
7125a0e to
5016e2d
Compare
b4fac47 to
5bcbc6f
Compare
c424089 to
ce4942a
Compare
The calls a process changes its own memory with, without the charge, which lands with the ledger. `SYS_MMAP` and `SYS_MUNMAP` become `SYS_VM_RESERVE` and `SYS_VM_RELEASE`, and three calls join them: - `vm_reserve(len, prot, at)` places a range anywhere at an alignment, or fixed in place of what the pages held; any protection but `None` commits it. - `vm_protect` commits reserved private pages and changes protection in place: a committed page keeps its frame and contents under `None` (its leaf is held, not present). An object's pages take nothing past the protection they were mapped with, which the inventory now keeps per object range. Every change to a present leaf shoots down, a widening included. - `vm_discard` frees the frames and keeps the ranges: a private page reads zeros at its next touch, as `MADV_DONTNEED` does. - `vm_decommit` frees frames and commitment; the pages are reserved again. All are all-or-nothing over pages every one of which a range holds (`BadAddress` otherwise), and discard and decommit free through the one clear primitive, which ends every futex wait on a frame before it goes. `VmProt` is a value with no writable and executable member; `ReadExec` is `NotSupported` until executable private memory lands with its cache maintenance. Consumers: - std's dlmalloc reserves through `vm_reserve` and trims with `free_part`. - std's thread stacks are a reservation of the size asked, rounded to pages, with one guard page below and the stack committed before the thread starts; the 2 MiB floor is gone, so a smaller size is honoured. - libc's `mprotect` and `madvise(MADV_DONTNEED)` work; `PROT_EXEC` is still `ENOTSUP`. Its allocator trims too. - Guest tests move to the new calls; `mmap_stress` now covers the lifecycle, with a child that faults on each kind of unreachable page, and a 1 TiB reservation. - osbench's notes no longer say ToyOS commits in 2 MiB pages. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
- `mmap_stress` asks `vm_protect` to make the clock page, the one object every process maps read-only, writable, and is answered `PermissionDenied`: the bound an object range keeps is now tested where it is applied. With the bound ignored the test reds. - libc's pthread stacks are a reservation of the size asked, in whole pages, above one guard page, committed before the thread starts and released when it is reaped, as std's are; they no longer come from the heap. - `private_prot` is `vm_prot`: it parses the word for object ranges too. `clear`'s contract was rewritten with the inventory stage's fix. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
Every change to a present user leaf interrupted every other CPU and waited for all of them, so `vm_protect`, a `vm_release` of touched pages and V8-style protection toggles cost an IPI round on the whole machine even for a single-threaded process. Now `kernel::tlb_tags` keeps, per PCID, the CPUs running the space and when it last changed, and per CPU when it last flushed every user translation: - A change stamps the space and reaches the CPUs running it; the initiator flushes its own entries by page for up to 64 pages, and a change no other CPU runs sends no IPI and waits for nothing. - A CPU that ran the space before keeps its entries under PCID, so instead of being reached it flushes every user translation before it next enters a space that changed since its own last flush. - The two sides pair through a `SeqCst` fence each: either the entering CPU sees the stamp, or the change sees the CPU. `kernel-loom`'s `tlb_tags` checks it, and reds with the entering side's fence removed (a CI control). Kernel-half changes and PCID reclaims still reach every CPU. AArch64 broadcasts its invalidations in hardware and is unchanged. `tlb_shootdown_waits` now times its waits with a thread of its own spinning on each other CPU, touches the page a fixed mapping replaces (an untouched page has no translation to flush), and adds the other half: with no other thread running, a munmap reaches no other CPU, counted by the test kernel's `SPACE_REACHED` rather than timed. Each half reds under its mutation: a change that reaches no runner, and one that reaches every CPU. A CPU counts as running a space from before its root is loaded until after it is replaced, and one past the 256 the targets hold reaches every CPU, as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
Every exception entry sealed its registers into the black box before its handler ran and wrote the whole 16 KiB box back out of the caches: 256 `CLFLUSH`es. With user memory populated a 4 KiB page at a time, that ran on every first touch of every page, from every CPU at once, into one box. On the T14 at 3446e9c a first touch cost 6.1 us a page (osbench `mem.first_touch.4k`), against Linux's 0.93 us on the same machine; that the write-back is most of the difference is inferred, not yet measured. A Ring 3 page fault no longer seals. It is served, or it ends its process through the crash path, which reports; a fault inside its handler is a Ring 0 fault, which still seals before anything else. AArch64 records only a fault it does not serve, and is unchanged. The black box's single-writer claim is corrected: two CPUs taking a kernel fault or an exception at once still write the one box, recorded in `issues/two-cpus-faulting-at-once-write-one-black-box.md`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
A spawn copied every byte of its program's segments into the new process when it made it. On the T14 at 3446e9c osbench's large program, 32 MiB of data it never reads, took 6.6 ms to spawn and wait, against 0.72 ms on main, which paged the image in at faults, and 0.054 ms on Linux. A segment out of an image object is now a copied range (`kernel::vm`'s `Kind::Copy`): private, committed memory whose page starts as the object's bytes at its offset, as far as the segment's file bytes go, and zeros past them, copied when the page is first touched. That is the promise `SharedImage` already makes, a change after the hand-off reaching only pages not yet read. A discarded page is copied again at its next touch. A program from any other backing is copied in whole, as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
…pshot A spawn copied a program's segments page by page at first touch only when they came out of an image object; a program in the kernel's /tmp was copied in whole when the process was made. On the T14 at dadeebe osbench's large program, 32 MiB of data in /tmp it never reads, took 6.4 ms to spawn and wait against 0.74 ms at the base, which paged it in at faults. A copied range's source is now any `CopySource`: an image object, or a snapshot of a /tmp file (`file_cache::Snapshot`). A snapshot holds a reference to each of the file's pages as they are at the spawn, and a write to a shared page writes a copy of it, so a later write, truncation or delete of the file never reaches the child: it runs the bytes it was spawned from. Its reads take no lock, which matters because a first touch copies under the space's lock and a read(2) holds the file cache's lock while it copies into user memory. The snapshot itself is taken before the child's space is locked. Every backing now has a source, so the whole-copy path is gone. `spawn_from_tmp` overwrites and deletes the file the moment its child is spawned, then has the child read 256 KiB of its data; with the source reading the file live it reds. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
- spawn_from_tmp overwrites its child's file in place, then cuts it to nothing and deletes it. A create-and-truncate write replaced the file with a new one, so the test had only shown the child surviving its file's delete. The bytes written are named by what they are not, since 0xF4 is hlt only on x86. - A space counts the changes that reached another CPU only in the test kernel, which is the one reader of the count. - The black box's seal is one rule: every vector but a Ring 3 page fault. - A swapped binary's second window is closed: a /tmp spawn runs a snapshot. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q
ce4942a to
94838ad
Compare
|
Landing as part of the memory stack (#1003, already merged; then #1008 → #1010 → #1045), rebased onto 4dc75f5 with #1003's duplicated commits dropped and no conflicts. Each PR was reviewed LAND. #1045 passed review r2 and its T14 run at 1595bbb (273 passed; the 5 failures are known and unrelated), and its QEMU checks at fb0f316 and b3f98b4 passed. Host CI is green at 37b63fc. Owner items 52, 64 and 71 cover this stack. Machine-level accounting lands first; per-job accounting, the system-controls floor and the supervisor restart retry are recorded as next. |
Stage 3a of the backing-memory migration: the lifecycle calls, std and libc on top of them, and the hot paths the T14 measured. Stacked on #1003; lands with #1003 and #1010.
Commits
e9805513f: the lifecycle calls.SYS_MMAPandSYS_MUNMAPbecomevm_reserveandvm_release. They are joined byvm_protect,vm_discardandvm_decommit.VmProtmember is both writable and executable.VmAtisAnywhere { align }orFixed.vm_protectcommits reserved pages and changes protection in place. A committed page keeps its frame underNone. An object range never gets more than the protection it was mapped with.vm_discardfrees a range's frames and keeps the range.vm_decommitfrees both the frames and the commitment.mprotectandmadvise(MADV_DONTNEED)work.ReadExecisNotSupporteduntil ambient private RX lands.bf4deb3cd: review round 1.mmap_stresschecks this, and the check turns red with the bound ignored.f2e9370b1: on x86, a change reaches only the CPUs running the space (kernel::tlb_tags).d9fa4bfd1: a Ring 3 page fault no longer seals the black box. It used to: each seal wrote back the whole 16 KiB box, 256CLFLUSHes, on every first touch of every page. Such a fault is either served, or it ends its process through the crash path, which reports. A fault inside the handler is a Ring 0 fault, and that still seals.34380bb26: a spawn copies each page of its program at the page's first touch, out of the image object (Kind::Copy). It no longer copies every segment byte when the process is made.2d271c9f2: a spawn from/tmpdoes the same, out of a snapshot of the file (round 4 below).b4fac47ba: review round 4.spawn_from_tmpoverwrites its child's file in place, then cuts it and deletes it. A create-and-truncate write had replaced the file with a new one.issues/a-swapped-binary-lives-where-any-process-can-rewrite-it.md: a/tmpspawn now runs a snapshot, so the service-lifetime window is closed.The shootdown review's blockers
tlb::switching(to, load)wraps the write:switchcomes before it, andleftafter it.get(me / 64). A CPU beyond the 256 that the targets hold falls back to a machine-wide shootdown.tlb_shootdown_waitspart 3 counts instead of timing. The test kernel'sSPACE_REACHED(SYS_DEBUG29) counts changes that reached another CPU. With no other thread running, avm_releasemust leave the count unchanged. The 10 ms bound is gone.tlb_shootdown_waits.rs:162).:211).issues/two-cpus-faulting-at-once-write-one-black-box.md. The earlier per-fault write-back narrowing is replaced byd9fa4bfd1.Why the last three: the T14 at
3446e9c33osbench p50 on the same T14, against
main(bench2/metal-r2b-t14-3) and Linux:mem.first_touch.4kns/pagemem.anon.totalns/MiBmem.map_unmap_4knsmem.mprotect.4knsproc.spawn_wait.tinynsproc.spawn_wait.largensthread.spawn_join.defaultnsmem.realloc_grow.1m_256mnsRound 4 below has the T14 result at the stack head.
Round 5: the T14 at
53049d55c, and review round 4The T14 ran round 4's staged boots: 266 passed and 10 failed. Every red has a cause outside this stack:
klogd_hosted, from the AMD row's deliberate DMA fault at 00:0d.0;i219_messages_on_its_msi_vector, which reports "the MAC saw 0 frame(s)" on the wire;Passing rows include
spawn_from_tmp,tlb_shootdown_waits, the global-leaf rows,pmm_accounting, the audio rows and the ACPI rows. Evidence: https://github.com/ToyOSOrg/evidence/tree/505932afaa9e65cee3e9e590e5206c7b867b4708/r5osbench p50 on the T14, head
53049d55cagainst the base35e930136, one run each:proc.spawn_wait.largeproc.spawn_wait.tinythread.spawn_join.defaultmem.map_unmap_4kmem.first_touch.4kmem.map_touch_unmap.4kThe rows that are still slower are recorded with these numbers in
issues/user-memory-hot-paths-are-slower-than-2-mib-pages.md. Landing waits on the owner's items 52 (refusable kernel objects) and 64 (accepting the recorded regressions).Round 4: the T14 at
dadeebe0a, and the rebase ontomain35e930136The stack is rebased onto
main35e930136. One conflict, in libc'spthread.rsagainst #1011, was resolved by keeping the reserved stacks with #1011'sResultreturns. Evidence: https://github.com/ToyOSOrg/evidence/tree/e618281b0fd5528955d6a601b238efc3fde1e980/r4osbench p50 on the T14, head
dadeebe0aagainst its base028244feb, one run each, with Linux on the same machine:mem.first_touch.4kns/page3446e9c33)proc.spawn_wait.largensproc.spawn_wait.tinynsmem.map_unmap_4knsmem.mprotect.4knsthread.spawn_join.defaultnsmem.anon.totalns/MiBmem.realloc_grow.1m_256mmswork.build_shaped.j8ms/tmp, and a program there was still copied whole at spawn. This is fixed in mm: the vm_* lifecycle calls, std and libc on them, and the hot paths the T14 measured (memory stage 3a) #1008's new2d271c9f2: a spawn from/tmpcopies each page at its first touch, out of a snapshot of the file that holds a reference to each of its pages. A write to a shared page writes a copy of it, so a later write, truncation or delete never reaches the child. The snapshot's reads take no lock, because a first touch copies under the space lock andread(2)holds the file cache's lock while it copies into user memory.spawn_from_tmpoverwrites the file in place, cuts it to nothing and deletes it the moment its child is spawned, then has the child read 256 KiB of its data. With the source reading the file live, the child ends with a memory fault (controltmp-live-source).53049d55c: 218 µs against the base's 817 µs (round 5).mem.anon.total,realloc_grow(nomremap), and every row that copies through user memory, which is 15–40% slower:fs.tmpseq read and write,fs.served4 KiB reads and writes,ipc.reqrep,pipe.throughput.65536. These are now recorded with numbers inissues/user-memory-hot-paths-are-slower-than-2-mib-pages.md.The five unexplained T14 reds are
main's. Each is red the same way under QEMU at the head and at its base028244feb:test_rs_vector_state: 101, "astatof /system/bin/test_rs_vector_state was refused".209_libc_uname:unameanswers -1.210_stat_identity: thed_inorows on /system answer NO.test_rs_handle_lifetime: 101. Its "undefined" flag bit is 32, which has beenOpenFlags::EXCLUSIVEsincemain's d48ca18.test_rs_spawn_child_ends_first: 134.Everything else on the T14 passed, apart from the AMD rows and
klogd_hosted, as expected:tlb_shootdown_waits,pmm_accounting, osbench, the audio rows,countersand the ACPI rows.libc_children'sspawn_children_capis red onmainand green with the stack. Three runs each:main35e930136: 1, 1, 1. Child 240 ended by SIGSEGV twice, and the third run aborted.35e930136: 0, 0, 0.main's SIGSEGV is a demand fault that finds no free 2 MiB page and is left unhandled (process.rs'sPageAlloc::new(page_2m)returningNone). With the stack, a fault draws a frame that was promised when the memory was committed (alloc_promised), so it cannot fail. A spawn past what memory holds is refused at the call instead. The stack removes that path rather than record it.Checks
Round 4, at the stack head
53049d55conmain35e930136: https://github.com/ToyOSOrg/evidence/tree/e618281b0fd5528955d6a601b238efc3fde1e980/r4. Round 3, atdadeebe0aon028244feb: https://github.com/ToyOSOrg/evidence/tree/0966b10a03bba923c274e82daa560bbb57044a52/r3hosted_clang_hello: exit 0.libc_children×3: all passed.tlb_shootdown_waits×3;abuse_kernel_addr;global_leaf_discharge;global_leaf_flush.tlb.rsbyte-identical to the head's. Each turnstlb_shootdown_waitsred.global_leaf_user_flushis red under QEMU on the commit before the targeting too. Its metal row passed at3446e9c33, and it is in the stagedshared-debugboot.The T14's DMA fault at 00:0d.0 is the AMD bench's deliberate
test_rs_usbd_spare_fault(#1003's body has the trace).Risks
issues/user-memory-hot-paths-are-slower-than-2-mib-pages.md:reallochas nomremap.🤖 Generated with Claude Code
https://claude.ai/code/session_01PXEk8BBMDT8tXu4AnENB9Q