Repository navigation
perf(mm): batch the TLB flush of kernel-stack reclaim (C3) - #104
Open
YUZHEthefool wants to merge 3 commits into
Open
YUZHEthefool wants to merge 3 commits into
YUZHEthefool wants to merge 3 commits into
Conversation
C3 repair (#90). reclaim_kernel_stack ended every stack's RCU callback with a CR3-0 flush: one IPI to every other online CPU per thread lifetime, 86% of the broadcast shootdowns of the 16-vCPU thread profile. The callback still validates and clears the leaves under PT_LOCK, but a verified stack now goes to a flush batch (kstack_flush), and one flush covers up to 16 stacks. Until its flush a stack keeps its frames and, through the new RCU slot state FlushPending, its PID slot, so neither can be reused. A batch completes in the order flush, free, release, and only under RCU drain ownership: on a push into a full batch, in the drain epilogue once the batch is full or its oldest stack is 1000 ticks old, or on request. The flush runs outside PT_LOCK. A partially cleared stack that cannot be verified keeps its synchronous flush and its quarantine. /proc/schedstat gains a kstack_flush line. The ST-K2-P2 exact memory baseline (test op 6) also counts the pages waiting for their flush. Hosted tests cover the triggers, the completion order, a buddy rejection and the slot states. The kstack_flush_batch runtime test checks frames and PID slots before and after a completion at every CPU count, so the RuntimeTest total rises from 75 to 76. Plan item: N3
The test's exact checks hold in the boot suite, where no other CPU drains RCU and no tick ages the batch. The kernel shell's `test` command can also run it after boot, where another CPU may complete the batch or change the free-page count inside its window. Comment only. Plan item: N3
Public index for the private receipt archive of this branch: source identities, recorded results and their limits. The batch works as designed; host time per thread does not improve, and the index says so.
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plan item: N3
Problem and outcome
C3 (#90) found that every thread lifetime ends with one full TLB broadcast.
reclaim_kernel_stack, the RCU callback that frees a dead thread's kernel stack, clears the stack's eight leaves and then callsflush_all_address_spaces(): one IPI to every other online CPU, and a wait for each acknowledgement, inside thePT_LOCKsection. At 16 vCPUs that single call is 86% of all broadcast shootdowns and about 65% of all IPIs per thread (E2b and E2c, in the comments on #90).This PR makes one broadcast cover up to 16 reclaimed stacks. A stack's frames and its PID slot stay held until that broadcast, so nothing is reused early.
Measured outcome. The mechanism works as designed: 5.6–16 stacks per broadcast, IPIs per thread down 48–58%,
PT_LOCKhold time per thread down 38–72%. Host time per thread does not improve. On the unmodifiedthreadprofile the repaired kernel is 1.6% slower at 4 vCPUs and 3.1% slower at 16 vCPUs. The measurements point at wake-ups that the per-stack broadcast used to cause as a side effect. Details are under "Measurement" below.Related issue or audit finding
Related: #90 (C3), #62. Design: Zero-kernel/Nilix-docs-private#15 (
design/c3-kstack-reclaim-batching-design.md, option B2). It builds on the counters from #101.Design and scope
The callback.
reclaim_kernel_stackstill validates the eight leaves against the stack's physical identity and clears them underPT_LOCK. What it does next depends on the result:PT_LOCK, then free the block and the slotPT_LOCK, then quarantineThe batch (
kernel_core/kstack_flush.rs, new). It is a fixed array of 16{pid, phys}entries under a spin lock. While a stack waits:FlushPending, soallocate_kernel_stackanswersReclaimPendingfor that PID, as it already does between a thread's exit and its RCU callback. Process creation already skips such a PID.A batch completes in one of three places, all under RCU drain ownership:
kstack_flush::complete_now(), which only the runtime test calls.Completion runs one
flush_all_address_spaces(), outsidePT_LOCK. Then, for each stack, it returns the block to the buddy allocator and frees the slot. If the allocator rejects a block, that slot is quarantined instead, as before.RCU (
rcu.rs).FlushPendinghas three transitions, each with exactly one legal source state:Running → FlushPending,FlushPending → FreeandFlushPending → Quarantined. A failed transition panics.finish_runningleaves aFlushPendingslot occupied and charged.with_drain_ownershipgivescomplete_now()the drain owner's exclusivity.Observability.
/proc/schedstatgains one line afterpt_lock:stacks / batchesis the mean batch size. (Values from round 257 of a 16-vCPUcpustress run.)Tests.
FlushPendinglife cycle on a local callback pool, including every refused transition. The pool self-test, which also runs at boot, is extended.kstack_flush_batchis in scope at every CPU count. It frees three stacks, checks that their PIDs and frames are still held, completes the batch, and checks one batch, three stacks, at least one more broadcast, and the frames and PIDs returned. The pinned RuntimeTest total goes from 75 to 76; hostedkernel-corefrom 142 to 151.syscall_testoracle. ST-K2-P2(c) requires buddy free pages to return to an exact baseline after 200 thread exits. Deferred frees break that. Test op 6, which already reports kernel-stack page-table pages for this purpose, now also counts the pages of stacks waiting for their flush. This is kernel-side and exists insyscall_testbuilds only; the userspace test andsyscall_test.elfare unchanged.Where this differs from the design. Five points, each narrower or stricter than the proposal; the design PR gets an "as implemented" section:
FlushPending-only transition. The design widened the existingRunning-only one.process.rs, so the hosted tests run the code the kernel runs.NeedsCpus(2), and asserts on the batch's own counters.Not in this PR: C3's E3 and E4, the remaining 14% of broadcasts, and any tuning of the batch size or age.
Kernel safety and invariants
unsafe/ MMIO / page-table / usercopy justification: no newunsafe. The page-table walk and the leaf clearing are unchanged and still underPT_LOCK.flush_all_address_spaces()returns, and that call panics on a missing acknowledgement.FlushPendingfrom inside the callback until after the flush, so the PID cannot be allocated, and a kernel-stack virtual slot is a function of the PID.DRAIN_OWNER. A completion therefore cannot free a slot between its callback's hand-off and the drainer'sfinish_running.PT_LOCK. The broadcast and its acknowledgement wait leave thePT_LOCKsection on the common path.complete_now()spins for drain ownership. It needs process context, and once other CPUs drain it needs interrupts enabled. Only the boot-time runtime test calls it./proc/schedstatgains one trailing line.Validation evidence
Receipts are archived privately (Zero-kernel/Nilix-docs-private#16);
.validation/c3-kstack/README.mdis the public index. Everything below ran on3ae42c6; the branch headcc7a7b6adds only that index.Local (Fedora, nightly-2025-12-08), on
3ae42c6:Hosted negative controls (local edits, never committed): freeing before the flush, releasing the slot before the flush, and letting a push complete its own stack each fail the
kstack_flushtests.Dev host, QEMU 8.2 (TCG),
3ae42c6against its parent82beb71:kstack_flush_batchpasses in each; the parent is also GATE-QUALIFIED ×3kstack_flush_batch:PID 32766 was allocatable before its stack's flush; the batch then panics on the slot's statesyscall_test's 2 KiB/proc/schedstatreadfree pages+kstack tables 33596 -> 33612at iteration 0: two waiting stacksSTRESS-7 at
STRESS_CPUS=4, plus the four SMP profiles atSTRESS_CPUS=16. Each profile ran 900 s and is QUALIFIED (3). The configured heartbeat bound is 15000 ms.memoryandprocessboot one vCPU andblocktwo, as the gate defines.kstack_flush, last copycpu,smp,threadandprocess. Where they end slowly, the age bound completes them: 6.1 inmemory, 9.0 incombined.493452c(thread 182, combined 89).CI on
3ae42c6: PR CI passed (15 of 15 jobs). Extended run 43396656717 passed in 31 jobs; its two diagnostic strict jobs fail on the remaining deferrals with the dev host's counts, andkstack_flush_batchpasses in all eight strict gates.None of these is a strict pass.
Measurement
The mechanism. The E2 thread matrix (16 workers at 4, 8 and 16 vCPUs, 300 s, each run alone) on
diag/c3-kstackagainstdiag/c3-kstack-base: each side plus E2's diagnostic runner, not for merge. All 14 runs are QUALIFIED (3). Per thread, parent → repaired:PT_LOCKhold (ms)Host time per thread. Three sets of runs, all on QEMU 8.2 unless stated:
threadprofile, 4 vCPUsthreadprofile, 16 vCPUsWhere the time goes. Diagnostic kernels at the E2 matrix's clearest point (16 workers, 4 vCPUs), two runs each, none for merge:
A confounder found on the way. The kernel calibrates its LAPIC timer over 10 ms at boot. Seven of 43 dev-host boots in these batches came out 17–46% high, which slows the tick for the whole run and makes it 4–11% faster (at 16 vCPUs one such boot reached the stress runner in 97 s instead of 236 s). Those runs are marked and not counted; one of them is the seventh E2 pair (6.2, 16 vCPUs). I will report this and the host-side figures on #90 as E4 leads.
Merge status: held for a maintainer decision. The change is correct and does what the design says, but on its own it costs a little host time under TCG and gains none. The two options are to hold it until E3 has found the wake path the broadcast was covering and then re-measure, or to merge it as groundwork and accept about 3% on the 16-vCPU
threadprofile meanwhile. KVM and real hardware were not measured.Compatibility, performance, and rollout
/proc/schedstatsize. The new line is about 70 bytes. A 16-vCPU copy is 941 bytes.syscall_testreads the file with one 2048-byte read, and no gate runs it above 16 vCPUs; at 32 CPUs the file projects to about 1.6 KiB.PT_LOCKhold, but no host-time gain under TCG, and a loss of about 1.6% (4 vCPUs) to 3.1% (16 vCPUs) on thethreadprofile.Released kernel stack for PID nis now printed at completion, not in the callback.Checklist
unsafeblocks have localSAFETYreasoning and a minimized unsafe scope. (none added)generate_schedstatand op-6 doc comments)