Repository navigation
perf: replace the frame bitmap scan with a buddy allocator + per-CPU cache - #2
Merged
Merged
Conversation
…cache frame_alloc() was a linear scan of a 131k-bit map under one global vmm_lock, with no coalescing. It is now a buddy allocator (orders 0-9, per-order free lists, node pointers stored in the free frame itself) fronted by a 32-frame per-CPU cache. frame_alloc()/frame_free() keep their signatures, so every caller — COW faults, clone, munmap, shm, loader, task teardown — is untouched. Measured on a live guest under a desktop+terminal workload: 234 of 234 allocations served from the per-CPU cache, zero descents into the buddy free lists, no vmm_lock on the fast path. Two constraints here are load-bearing and easy to break: - The bitmap is authoritative for "this frame is on a free list", not just init state. phys_reserve_region() runs after the lists exist (the boot self-test forces the first allocation, still inside phys_init), and a reservation can start in the MIDDLE of a coalesced block — which frame_order[] cannot describe, since only a block's aligned head carries its order. So it rebuilds the lists from the bitmap instead of unlinking frame by frame. Dropping that rebuild hands framebuffer/MMIO frames to a process as ordinary writable memory, and reserved frames are pinned at refcount 255 so a stray frame_free() cannot release them either. - A frame parked in a cache carries BUDDY_CACHED (0xFE), and frame_free() claims a frame by writing that marker *under vmm_lock* before the frame is parked. That marker is what keeps a double free inert, including one racing on another core, without putting vmm_lock back on the cache's fast path. The bitmap allocator it replaces got the same protection from a refcount>0 test. The boot self-test no longer just checks that allocations succeed: it proves the free lists are a PARTITION of the frames the bitmap calls free — every free frame on exactly one list, at an aligned head with the right order, none twice, none overlapping a live/cached frame. That single scan catches a bad merge, a leaked frame and a frame left in a cache and a free list at once. It also probes over-free and compares the used-page count exactly rather than within a cache's worth of slack. Frame-allocator counters (cache hits, refills, spills, merges, over-free) are exposed in /proc/meminfo. Co-Authored-By: Claude Code <noreply@anthropic.com>
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.
What this changes
frame_alloc()was a linear scan of a 131k-bit map under one globalvmm_lock, with no coalescing, and everyfork()paid dozens of those scans. It is now a buddy allocator (orders 0–9, per-order free lists, node pointers stored in the free frame itself) fronted by a 32-frame per-CPU cache.frame_alloc()/frame_free()keep their signatures, so every caller — COW faults (idt.c),vmm_clone_address_space,vmm_free_address_space, SysV shm, the loader, task teardown — is untouched.frame_ref_count[]semantics (COW/shm sharing, 255 = pinned zero page) are preserved.Measured effect
On a live guest under a desktop + terminal workload:
234 of 234 allocations were served from the per-CPU cache, with zero descents into the buddy free lists and no
vmm_lockacquisition on the fast path — which was the bottleneck. The counter ledger reconciles exactly (inflows = hits + spill outflow + frames still parked).Reviewing this? These two constraints are load-bearing
Both were caught in review as regressions against the original bitmap allocator, so they are the places worth your attention:
1. The bitmap is authoritative for "this frame is on a free list" — not just init state.
phys_reserve_region()runs after the free lists exist (the boot self-test forces the first allocation, still insidephys_init), and a reservation can start in the middle of a coalesced block — whichframe_order[]cannot describe, since only a block's aligned head carries its order. So it rebuilds the lists from the bitmap rather than unlinking frame by frame, and pins reserved frames at refcount 255 so a strayframe_free()cannot release MMIO back into the allocator. Without this, framebuffer/MMIO frames stay allocatable and get handed to a process as ordinary writable memory. (Invisible on QEMU, where the fb sits above RAM; real on hardware where it lands inside RAM — the reasonmem.calready had a comment about it.)2. A frame parked in a cache carries
BUDDY_CACHED(0xFE), andframe_free()claims a frame by writing that marker undervmm_lockbefore parking it. That is what keeps a double free inert — including one racing on another core — without puttingvmm_lockback on the cache's fast path. The bitmap allocator got the same protection from arefcount > 0test; the first cut of this PR dropped it.Verification
make check-quick: 17/17 PASS on the exact committed tree.The boot self-test no longer just checks that allocations succeed. It proves the free lists are a partition of the frames the bitmap calls free — every free frame on exactly one list, at an aligned head carrying the right order, none twice, none overlapping a live/cached frame. One scan catches a bad merge, a leaked frame, and a frame left in a cache and a free list at once. It also probes over-free and compares the used-page count exactly rather than within a cache's worth of slack.
Current output:
selftest PASS (partition ok, max order 0x6, 0xF merges, 0x1 over-free caught)—max order 0x6means a 64-frame (256 KB) block genuinely reappeared after an aligned run was freed, so merging is exercised rather than assumed.Frame-allocator counters (
cache_hits,buddy_allocs,refill_frames,cached_frees,spilled_frees,coalesces,oom,overfree,cache_frames) are exposed in/proc/meminfo.Known gaps
-smp 4and the fork/thread/cond suites drive the per-CPU cache across 4 cores, so there is empirical coverage — but nothing deliberately proves the partition invariant on an AP. Flagging rather than overstating.phys_reserve_region()after the lists exist costs an O(frames) rebuild, once, at boot. The alternative — deferring the first build until afterpaging_init— would mean the self-test cannot run inphys_init./proc/meminfobugs found nearby and deliberately left alone:VFS_TMPFS_MAX_BYTESrenders empty (TmpfsUsed: 0 B of B), and the tmpfs line is spliced into the middle of theHeapFreeparenthetical.Files
src/sys/vmm.csrc/include/vmm.hframe_alloc_stats_t+phys_get_stats(); header comment correctedsrc/sys/vfs.c/proc/meminfolines for the frame counterssrc/sys/mem.cphys_reserve_regioncall site about reserving after the builddocs/kernel-locking.md🤖 Generated with Claude Code