From cc016073fc88ada9e9b2a5dafc48e6f8f5f244f4 Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 10 Nov 2026 17:30:07 +0800 Subject: [PATCH 1/4] vmm: Do not mark the copy-on-write restore mapping MADV_HUGEPAGE A copy-on-write restore maps the snapshot file MAP_PRIVATE over guest RAM, and marked that mapping MADV_HUGEPAGE when thp is on. A private copy of a file page is always 4 KiB, so the hint buys no huge pages, but it makes each fault on the mapping read 2 MiB around the faulting page, including the faults the restore path itself takes. Measured with 16 copy-on-write clones of a 2 GiB guest, comparing thp=on with thp=off, which differ only in this hint for such a VM: - a restore on an idle host took p50 5.8 ms / p99 6.4 ms instead of 9.9 / 11.3 ms; - a restore while other clones fault in their memory took p50 9.4 ms / p99 14.3 ms instead of 49.9 / 97.6 ms; - reads from the snapshot memory files dropped by about 26%, at twice the number of major faults. Drop the hint from the overlay. The reserve policy is still applied. Signed-off-by: CMGS --- docs/snapshot_restore.md | 5 +++-- vmm/src/memory_manager.rs | 24 ++++++------------------ 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/docs/snapshot_restore.md b/docs/snapshot_restore.md index a5558854c2..e75d3cd016 100644 --- a/docs/snapshot_restore.md +++ b/docs/snapshot_restore.md @@ -211,8 +211,9 @@ Current constraints for `memory_restore_mode=copyonwrite`: `id`+`size` zone is fine), resizable RAM (`hotplug_size`, virtio-mem), KSM (`mergeable=on`), `--pvmemcontrol`, device passthrough (`--device`/`--user-device`/`--vdpa`), and snapshot ranges that are not - page-aligned single-region extents. Each region's `reserve` policy and the - THP policy are re-applied to the mapped region. + page-aligned single-region extents. Each region's `reserve` policy is + re-applied to the mapped region. THP is not: a private copy of a file page + is always 4 KiB, so `MADV_HUGEPAGE` would only enlarge read-around. - A snapshot memory file shorter than the saved ranges is rejected up front (it would otherwise fault `SIGBUS` at run time). - `prefault` is rejected. diff --git a/vmm/src/memory_manager.rs b/vmm/src/memory_manager.rs index d06ce097d1..afb0a0000b 100644 --- a/vmm/src/memory_manager.rs +++ b/vmm/src/memory_manager.rs @@ -1050,7 +1050,7 @@ impl MemoryManager { "snapshot memory file is shorter than the saved ranges", ))); } - do_mmap_cow_saved_regions(&guest_memory, &memory_file, saved_regions, self.thp)?; + do_mmap_cow_saved_regions(&guest_memory, &memory_file, saved_regions)?; // Landlock applies after the restore, so the pagemap is opened here. self.cow_source = match PageMap::open() { Ok(pagemap) => Some(CowSource { @@ -4157,13 +4157,11 @@ fn is_restore_cow_compatible(guest_memory: &GuestMemory, saved_regions: &MemoryR } // Maps each saved range over its guest RAM window, re-applying the owning -// region's reserve policy and the THP policy. Ranges must have passed -// `is_restore_cow_compatible`. +// region's reserve policy. Ranges must have passed `is_restore_cow_compatible`. fn do_mmap_cow_saved_regions( guest_memory: &GuestMemory, memory_file: &File, saved_regions: &MemoryRangeTable, - thp: bool, ) -> Result<(), Error> { let mut file_offset: u64 = 0; for range in saved_regions.ranges() { @@ -4196,16 +4194,6 @@ fn do_mmap_cow_saved_regions( if ret == libc::MAP_FAILED { return Err(Error::SnapshotMmap(io::Error::last_os_error())); } - if thp { - // SAFETY: ret/length name the private mapping just installed above. - let adv = unsafe { libc::madvise(ret, length, libc::MADV_HUGEPAGE) }; - if adv != 0 { - warn!( - "Restore (mode=copyonwrite): MADV_HUGEPAGE failed: {}", - io::Error::last_os_error() - ); - } - } file_offset = file_offset.checked_add(range.length).ok_or_else(|| { Error::SnapshotMmap(io::Error::other("snapshot range file offset overflow")) })?; @@ -4453,7 +4441,7 @@ mod tests { }); assert!(is_restore_cow_compatible(&gm, &table)); - do_mmap_cow_saved_regions(&gm, &file, &table, false).unwrap(); + do_mmap_cow_saved_regions(&gm, &file, &table).unwrap(); assert_eq!(gm.read_obj::(GuestAddress(0)).unwrap(), 0xab); assert_eq!(gm.read_obj::(GuestAddress(page - 1)).unwrap(), 0xab); @@ -4481,7 +4469,7 @@ mod tests { gpa: 0, length: 16 * page, }); - do_mmap_cow_saved_regions(&gm, &file, &ranges, false).unwrap(); + do_mmap_cow_saved_regions(&gm, &file, &ranges).unwrap(); let source = CowSource { file, ranges, @@ -4551,7 +4539,7 @@ mod tests { } #[test] - fn cow_restore_honors_region_reserve_and_thp() { + fn cow_restore_honors_region_reserve() { let page = get_page_size(); let mut file = tempfile::tempfile().unwrap(); file.write_all(&vec![0xcdu8; page as usize]).unwrap(); @@ -4573,7 +4561,7 @@ mod tests { gpa: 0, length: page, }); - do_mmap_cow_saved_regions(&gm, &file, &table, true).unwrap(); + do_mmap_cow_saved_regions(&gm, &file, &table).unwrap(); assert_eq!(gm.read_obj::(GuestAddress(0)).unwrap(), 0xcd); } } From 788646ec51f4aad7c9a6ed09b4fd075256705147 Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 10 Nov 2026 17:44:15 +0800 Subject: [PATCH 2/4] docs: Name the large-folio case for the copy-on-write restore mapping Signed-off-by: CMGS --- docs/snapshot_restore.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/snapshot_restore.md b/docs/snapshot_restore.md index e75d3cd016..494fe6f912 100644 --- a/docs/snapshot_restore.md +++ b/docs/snapshot_restore.md @@ -212,8 +212,10 @@ Current constraints for `memory_restore_mode=copyonwrite`: (`mergeable=on`), `--pvmemcontrol`, device passthrough (`--device`/`--user-device`/`--vdpa`), and snapshot ranges that are not page-aligned single-region extents. Each region's `reserve` policy is - re-applied to the mapped region. THP is not: a private copy of a file page - is always 4 KiB, so `MADV_HUGEPAGE` would only enlarge read-around. + re-applied to the mapped region. THP is not. A private copy of a file page + is always 4 KiB. On a filesystem with large folios, `MADV_HUGEPAGE` would let + read faults map 2 MiB file folios, but it also makes each fault read about + 4 MiB from the snapshot file, which slows down the restore. - A snapshot memory file shorter than the saved ranges is rejected up front (it would otherwise fault `SIGBUS` at run time). - `prefault` is rejected. From 21898e09650f804b35ceac706d3c224672102d6e Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 10 Nov 2026 19:34:03 +0800 Subject: [PATCH 3/4] docs: Note that copy-on-write restore does not apply thp memory.md says guest memory is labelled MADV_HUGEPAGE when thp is on, which no longer holds for memory restored with memory_restore_mode=copyonwrite. Signed-off-by: CMGS --- docs/memory.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/memory.md b/docs/memory.md index 17f0b91859..97a8e5b1ad 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -221,6 +221,9 @@ there will be fewer virtualisation related page faults. Unlike using `hugepages=on` a specific number of huge pages do not need to be allocated by the kernel. +A restore with `memory_restore_mode=copyonwrite` does not apply this option to +the restored memory. + By default this option is turned on. _Example_ From 75ce725b03780658bc8c890798049bdd7a58c6a8 Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 10 Nov 2026 19:37:44 +0800 Subject: [PATCH 4/4] docs: State the trade-off of not applying THP to copy-on-write restores Distinguish the private anonymous copies a guest write creates from the read-only PMD-sized file folios that MADV_HUGEPAGE allows, record what dropping the hint costs and saves as measured, and qualify the base page size and the readahead amount. Signed-off-by: CMGS --- docs/snapshot_restore.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/snapshot_restore.md b/docs/snapshot_restore.md index 494fe6f912..e6e6920f4d 100644 --- a/docs/snapshot_restore.md +++ b/docs/snapshot_restore.md @@ -212,10 +212,14 @@ Current constraints for `memory_restore_mode=copyonwrite`: (`mergeable=on`), `--pvmemcontrol`, device passthrough (`--device`/`--user-device`/`--vdpa`), and snapshot ranges that are not page-aligned single-region extents. Each region's `reserve` policy is - re-applied to the mapped region. THP is not. A private copy of a file page - is always 4 KiB. On a filesystem with large folios, `MADV_HUGEPAGE` would let - read faults map 2 MiB file folios, but it also makes each fault read about - 4 MiB from the snapshot file, which slows down the restore. + re-applied to the mapped region. THP is not. A page the guest writes becomes + a private anonymous copy of one host base page (4 KiB on x86_64) either way. + On a filesystem with large folios, `MADV_HUGEPAGE` lets read faults map + PMD-sized file folios (2 MiB on x86_64), which speeds up random reads of + read-mostly guest memory (by about 3.5% in one measurement). But a read fault + that misses the page cache can then request readahead of two PMD-sized + folios, about 4 MiB, which made the restore about 1.5 times slower on an idle + host and about 5 times slower with 16 VMs restoring at once. - A snapshot memory file shorter than the saved ranges is rejected up front (it would otherwise fault `SIGBUS` at run time). - `prefault` is rejected.