Skip to content

virtio-devices: balloon: Remap released copy-on-write memory - #7

Merged
CMGS merged 7 commits into
devfrom
balloon-cow-remap
Nov 10, 2026
Merged

CMGS merged 7 commits into
devfrom
balloon-cow-remap

Conversation

@CMGS

@CMGS CMGS commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes the virtio-balloon refault on copy-on-write restored guest RAM, for free page reporting and for inflated 2 MiB chunks.

memory_restore_mode=copyonwrite maps the snapshot file MAP_PRIVATE over guest RAM, but the region still records itself as anonymous. So the balloon (inflate and free page reporting) released it with MADV_DONTNEED, and every reused page faulted back in from the snapshot file with read-around.

  • The memory manager records that the overlay was mapped (cow_restored), and Balloon::new takes it.
  • When that is set, release_memory_range maps anonymous memory (MAP_FIXED, keeping the region's MAP_NORESERVE) over the 2 MiB-aligned part of the range, then applies MADV_DONTDUMP and MADV_NOHUGEPAGE. The sub-2 MiB head and tail keep MADV_DONTNEED, which bounds the mappings to one per 2 MiB of guest RAM. A failed mmap falls back to MADV_DONTNEED.
  • OnDemand restore and every other memory setup are unchanged.

Gates (node1, b645779): fmt, typos, 4 clippy variants, fuzz check, virtio-devices 173 tests incl. 22 balloon tests. On an earlier revision: the remap test failed with the fix disabled, arm64 clippy and balloon tests passed.

Hardware (testbed, 16 CoW clones under a 16 GiB cgroup cap):

  • Memory-file reads with free page reporting under churn: 1848 → 273 MiB/min (refaults about 23x fewer). With inflate: 1676 → 275 MiB/min. No change without page-cache pressure, idle restore time unchanged.
  • Integrity: inflate 1 GiB per clone under heavy load, deflate, reuse. 32/32 sha256 checks of a guest array match. Every sampled clone had a partly inflated chunk holding live array pages, which stayed file-backed.
  • Mappings: about 30 per VMM (max 41) under light churn, about 203 per VMM under heavy inflate/deflate churn. The bound is 2x the 2 MiB chunks per region, and ENOMEM from mmap falls back to MADV_DONTNEED.
  • The tested binary (0996dac5b) predates b645779, which is a behaviour-neutral simplification.

Squash commit message:

virtio-devices: balloon: Remap released copy-on-write memory

A VM restored with memory_restore_mode=copyonwrite maps the snapshot
file MAP_PRIVATE over its guest RAM. MADV_DONTNEED there only drops the
private copy: the next guest touch of a released page, even a write,
faults the page back in from the snapshot file with read-around. With
free page reporting on, every page the guest frees and reuses pays that
read.

Map anonymous memory over the 2 MiB-aligned part of each released range
instead, so a reused page is zero-filled without I/O. The head and tail
below 2 MiB keep MADV_DONTNEED. This covers free page reporting, which
reports pageblock-sized ranges. Inflate releases at most 256 pages (1
MiB) per batch, so it releases each batch with MADV_DONTNEED at once and
tracks the inflated 4 KiB pages per 2 MiB chunk across batches. A chunk
the guest inflates completely is remapped the same way, and a deflated
page clears its bit. The tracking costs at most one 64-byte bitmap per
partly inflated chunk. In the worst case, alternate 2 MiB blocks
reported, that gives one mapping per 2 MiB of guest RAM, which stays
under the default vm.max_map_count for guests up to about 100 GiB. The
new mapping keeps the region's MAP_NORESERVE, and a failed mmap falls
back to MADV_DONTNEED. Kernels before 6.12 can leave a hole if MAP_FIXED
fails after the unmap.

The remapped range gets MADV_DONTDUMP like the rest of guest RAM, and
MADV_NOHUGEPAGE: a private copy of a file page is 4 KiB today, and
hugepage memory never takes the copy-on-write restore path, so a reused
page keeps costing 4 KiB instead of a 2 MiB host page.

An overlay region still records itself as anonymous, so the memory
manager notes that the overlay was mapped and the balloon remaps only
then. OnDemand restore never remaps, which would drop the userfaultfd
registration.

Signed-off-by: CMGS <ilskdw@gmail.com>

CMGS added 5 commits November 10, 2026 15:10
A VM restored with memory_restore_mode=copyonwrite maps the snapshot
file MAP_PRIVATE over its guest RAM. MADV_DONTNEED there only drops
the private copy: the next guest touch of a released page, even a
write, faults the page back in from the snapshot file with
read-around. With free page reporting on, every page the guest frees
and reuses pays that read.

Map anonymous memory over the 2 MiB-aligned part of each released
range instead, so a reused page is zero-filled without I/O. The head
and tail below 2 MiB keep MADV_DONTNEED, which bounds the mappings to
one per 2 MiB of guest RAM, well under vm.max_map_count. The new
mapping keeps the region's MAP_NORESERVE, and a failed mmap falls back
to MADV_DONTNEED.

The remapped range gets MADV_DONTDUMP like the rest of guest RAM, and
MADV_NOHUGEPAGE: a private copy of a file page is 4 KiB today, and
hugepage memory never takes the copy-on-write restore path, so a
reused page keeps costing 4 KiB instead of a 2 MiB host page.

An overlay region still records itself as anonymous, so the memory
manager notes that the overlay was mapped and the balloon remaps only
then. OnDemand restore never remaps, which would drop the userfaultfd
registration.

Signed-off-by: CMGS <ilskdw@gmail.com>
…est RAM

Signed-off-by: CMGS <ilskdw@gmail.com>
CMGS added 2 commits November 10, 2026 19:03
Inflate ranges never exceed one 256 PFN batch, so they cannot hold an
aligned 2 MiB chunk; pass false there and keep the partial page helper
as upstream has it. Whole chunks filled by inflate are still remapped
by the chunk bitmap, and free page reporting still remaps directly.

Issue one MADV_DONTNEED when a range holds no full chunk instead of
two adjacent ones, use advise_memory_range for the remap advice, and
compare the chunk bitmap as an array. Fold the duplicate remap test
into release_cow_restored_remaps_aligned_part.

Signed-off-by: CMGS <ilskdw@gmail.com>
Error::MadviseFail displays only "Madvise fail.", so log it with Debug
to keep the errno, as the other advise_memory_range callers do.

Signed-off-by: CMGS <ilskdw@gmail.com>
@CMGS
CMGS merged commit e456597 into dev Nov 10, 2026
31 of 33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant