vmm: support migration of paused VMs

This extends migration to also support paused VMs, preserving the
paused state on the destination.

Changes:
- Add CompletePaused protocol command that finalizes migration without
 resuming the VM on the destination
- Skip the pause step during migration if the VM is already paused
- On migration failure, only restore the running state if
  the VM was originally running (not paused)

Signed-off-by: Nguyen Dinh Phi <phind.uet@gmail.com>
This commit is contained in:
Nguyen Dinh Phi
2026-04-24 13:18:05 +08:00
committed by Rob Bradford
parent e2b9fa261b
commit 23fc9ca258
2 changed files with 38 additions and 12 deletions
+12 -1
View File
@@ -103,6 +103,7 @@ use crate::bitpos_iterator::BitposIteratorExt;
/// Configured --> Configured: Memory /// Configured --> Configured: Memory
/// Configured --> StateReceived: State /// Configured --> StateReceived: State
/// StateReceived --> Completed: Complete /// StateReceived --> Completed: Complete
/// StateReceived --> Completed: CompletePaused
/// ``` /// ```
/// ///
/// [live-migration protocol]: super::protocol /// [live-migration protocol]: super::protocol
@@ -115,10 +116,14 @@ pub enum Command {
Config, Config,
State, State,
Memory, Memory,
/// Finalizes the migration and resumes the VM on the guest. /// Finalizes the migration and resumes the VM on the destination.
/// Sent when the source VM was running at migration time.
Complete, Complete,
Abandon, Abandon,
MemoryFd, MemoryFd,
/// Finalizes the migration without resuming the VM on the destination.
/// Sent when the source VM was paused at migration time.
CompletePaused,
} }
#[repr(C)] #[repr(C)]
@@ -161,10 +166,16 @@ impl Request {
Self::new(Command::MemoryFd, length) Self::new(Command::MemoryFd, length)
} }
/// Finalizes the migration and resumes the VM on the destination.
pub fn complete() -> Self { pub fn complete() -> Self {
Self::new(Command::Complete, 0) Self::new(Command::Complete, 0)
} }
/// Finalizes the migration without resuming the VM on the destination.
pub fn complete_paused() -> Self {
Self::new(Command::CompletePaused, 0)
}
pub fn abandon() -> Self { pub fn abandon() -> Self {
Self::new(Command::Abandon, 0) Self::new(Command::Abandon, 0)
} }
+26 -11
View File
@@ -986,6 +986,10 @@ impl Vmm {
StateReceived { StateReceived {
state_receive_begin, state_receive_begin,
} => match req.command() { } => match req.command() {
Command::CompletePaused => {
debug!("Migration (incoming): Receiving final state of a paused VM");
Ok(Completed)
}
Command::Complete => { Command::Complete => {
// The unwrap is safe, because the state machine makes sure we called // The unwrap is safe, because the state machine makes sure we called
// vm_receive_state before, which creates the VM. // vm_receive_state before, which creates the VM.
@@ -1361,7 +1365,9 @@ impl Vmm {
mem_send, mem_send,
)?; )?;
let downtime_begin = Instant::now(); let downtime_begin = Instant::now();
vm.pause()?; if vm.get_state() != VmState::Paused {
vm.pause()?;
}
// Send last batch of dirty pages: final iteration // Send last batch of dirty pages: final iteration
{ {
@@ -1391,6 +1397,7 @@ impl Vmm {
#[cfg(all(feature = "kvm", target_arch = "x86_64"))] #[cfg(all(feature = "kvm", target_arch = "x86_64"))]
hypervisor: &dyn hypervisor::Hypervisor, hypervisor: &dyn hypervisor::Hypervisor,
send_data_migration: &VmSendMigrationData, send_data_migration: &VmSendMigrationData,
initial_vm_state: VmState,
) -> result::Result<(), MigratableError> { ) -> result::Result<(), MigratableError> {
// State machine that is updated with more context as we progress. // State machine that is updated with more context as we progress.
let mut ctx = OngoingMigrationContext::new(); let mut ctx = OngoingMigrationContext::new();
@@ -1461,9 +1468,11 @@ impl Vmm {
vm.start_migration()?; vm.start_migration()?;
if send_data_migration.local { if send_data_migration.local {
// Now pause VM // Now pause VM (skip if already paused, e.g. migrating a paused VM)
let downtime_begin = Instant::now(); let downtime_begin = Instant::now();
vm.pause()?; if vm.get_state() != VmState::Paused {
vm.pause()?;
}
ctx.set_vm_paused( ctx.set_vm_paused(
downtime_begin, downtime_begin,
// No memory was transferred // No memory was transferred
@@ -1509,10 +1518,15 @@ impl Vmm {
// When this returns, we know the VM was resumed (if it was running // When this returns, we know the VM was resumed (if it was running
// before the migration) and that the receiving VMM acquired disk // before the migration) and that the receiving VMM acquired disk
// locks again. // locks again.
let complete_req = if initial_vm_state == VmState::Running {
Request::complete()
} else {
Request::complete_paused()
};
let (_, complete_duration) = measure_ok(|| { let (_, complete_duration) = measure_ok(|| {
migration_transport::send_request_expect_ok( migration_transport::send_request_expect_ok(
&mut socket, &mut socket,
Request::complete(), complete_req,
MigratableError::MigrateSend(anyhow!("Error completing migration")), MigratableError::MigrateSend(anyhow!("Error completing migration")),
) )
})?; })?;
@@ -2578,13 +2592,10 @@ impl RequestHandler for Vmm {
.as_mut() .as_mut()
.ok_or_else(|| MigratableError::MigrateSend(anyhow!("VM is not running")))?; .ok_or_else(|| MigratableError::MigrateSend(anyhow!("VM is not running")))?;
// Only running VMs can be migrated: Future work can fix this to allow let initial_vm_state = vm.get_state();
// also the migration of paused VMs while preserving the state in success if initial_vm_state != VmState::Running && initial_vm_state != VmState::Paused {
// and error case. See #7815.
if vm.get_state() != VmState::Running {
return Err(MigratableError::MigrateSend(anyhow!( return Err(MigratableError::MigrateSend(anyhow!(
"VM is not in running state: {:?}", "VM is not running or paused: {initial_vm_state:?}"
vm.get_state()
))); )));
} }
@@ -2594,6 +2605,7 @@ impl RequestHandler for Vmm {
#[cfg(all(feature = "kvm", target_arch = "x86_64"))] #[cfg(all(feature = "kvm", target_arch = "x86_64"))]
self.hypervisor.as_ref(), self.hypervisor.as_ref(),
&send_data_migration, &send_data_migration,
initial_vm_state,
) )
.map_err(|migration_err| { .map_err(|migration_err| {
error!("Migration failed: {migration_err:?}"); error!("Migration failed: {migration_err:?}");
@@ -2606,7 +2618,10 @@ impl RequestHandler for Vmm {
return e; return e;
} }
if vm.get_state() == VmState::Paused // Only resume if the VM was originally running; a VM that was already
// paused before migration should remain paused after failure.
if initial_vm_state == VmState::Running
&& vm.get_state() == VmState::Paused
&& let Err(e) = vm.resume() && let Err(e) = vm.resume()
{ {
return e; return e;