offload_daemon: Implement sparse snapshot/restore

Copy only populated extents when writing the snapshot file and when
filling the restore memfd, leaving unwritten ranges as holes. Both
the on-disk snapshot and the restored guest RAM stay sparse, so that
untouched guest pages cost no disk space or host memory.

This brings the offload daemon closer to be at feature parity with CH's
internal implementation of snapshot/restore. The only missing piece is
on-demand paging at this point.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-7
This commit is contained in:
Sebastien Boeuf
2026-06-10 02:18:30 -07:00
parent df5d2d6003
commit 28b6b5d467

View File

@@ -17,7 +17,7 @@
use std::ffi::{CString, NulError};
use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::io::{self, Read, Write};
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
@@ -30,6 +30,7 @@ use vm_migration::MigratableError;
use vm_migration::protocol::{Command, Request, Response, Status};
use vmm::VmMigrationConfig;
use vmm::migration::SNAPSHOT_STATE_FILE;
use vmm::sparse::copy_region;
use vmm_sys_util::errno;
use vmm_sys_util::sock_ctrl_msg::ScmSocket;
@@ -308,32 +309,15 @@ fn dump_memory_slots(
}
fn dump_fd_to_path(file: &File, src_offset: u64, size: u64, path: &Path) -> Result<()> {
let mut out = OpenOptions::new()
let out = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(path)
.map_err(Error::WriteFile)?;
out.set_len(size).map_err(Error::WriteFile)?;
// dup so our own offset doesn't disturb CH's view of the shared fd.
// SAFETY: dup() has no preconditions; result is checked.
let raw = unsafe { libc::dup(file.as_raw_fd()) };
if raw < 0 {
return Err(Error::CopyMemory(io::Error::last_os_error()));
}
// SAFETY: `raw` is a fresh fd we now own.
let mut src = unsafe { File::from_raw_fd(raw) };
src.seek(SeekFrom::Start(src_offset))
.map_err(Error::CopyMemory)?;
let mut remaining = size;
let mut buf = vec![0u8; 1 << 20];
while remaining > 0 {
let want = (remaining as usize).min(buf.len());
src.read_exact(&mut buf[..want])
.map_err(Error::CopyMemory)?;
out.write_all(&buf[..want]).map_err(Error::CopyMemory)?;
remaining -= want as u64;
}
// Keep `out` sparse: holes are left as holes (read back as zero).
copy_region(file, src_offset, &out, 0, size).map_err(Error::CopyMemory)?;
out.sync_all().map_err(Error::WriteFile)?;
Ok(())
}
@@ -479,21 +463,10 @@ fn create_memfd_with_contents(
name: &str,
) -> Result<File> {
// Size the memfd to cover the range CH maps at `file_offset`.
let mut memfd = create_empty_memfd(file_offset + size, name)?;
memfd
.seek(SeekFrom::Start(file_offset))
.map_err(Error::CopyMemory)?;
let mut src = File::open(src_path).map_err(Error::ReadFile)?;
let mut remaining = size;
let mut buf = vec![0u8; 1 << 20];
while remaining > 0 {
let want = (remaining as usize).min(buf.len());
src.read_exact(&mut buf[..want])
.map_err(Error::CopyMemory)?;
memfd.write_all(&buf[..want]).map_err(Error::CopyMemory)?;
remaining -= want as u64;
}
memfd.seek(SeekFrom::Start(0)).map_err(Error::CopyMemory)?;
let memfd = create_empty_memfd(file_offset + size, name)?;
let src = File::open(src_path).map_err(Error::ReadFile)?;
// Copy sparsely so the memfd keeps the snapshot's holes.
copy_region(&src, 0, &memfd, file_offset, size).map_err(Error::CopyMemory)?;
Ok(memfd)
}