virtio-devices: Resume vhost-user devices after snapshot

For vhost-user devices, a snapshot involved the vrings to be stopped,
but they couldn't be resumed. This commit aims at fixing this by saving
the vrings base so that it can be safely reset after the snapshot has
succeeded.

This will allow VMs where snapshots are taken from internal CH
implementation and from an offload daemon to be resumed once the
snapshot is complete.

Signed-off-by: Sebastien Boeuf <sboeuf@meta.com>
Assisted-by: Claude:claude-opus-4-8
This commit is contained in:
Sebastien Boeuf
2026-07-22 06:35:59 -07:00
parent aa19811139
commit b47c26fa56
3 changed files with 80 additions and 18 deletions

View File

@@ -111,6 +111,8 @@ pub enum ActivateError {
BadActivate,
#[error("Failed to clone EventFd")]
CloneEventFd(#[source] io::Error),
#[error("Failed to create EventFd")]
CreateEventFd(#[source] io::Error),
#[error("Failed to spawn thread")]
ThreadSpawn(#[source] io::Error),
#[error("Failed to setup vhost-user-fs daemon")]

View File

@@ -265,6 +265,7 @@ pub const DEFAULT_VIRTIO_FEATURES: u64 = (1 << VIRTIO_F_RING_INDIRECT_DESC)
const HUP_CONNECTION_EVENT: u16 = EPOLL_HELPER_EVENT_LAST + 1;
const BACKEND_REQ_EVENT: u16 = EPOLL_HELPER_EVENT_LAST + 2;
const RESUME_EVENT: u16 = EPOLL_HELPER_EVENT_LAST + 3;
#[derive(Default)]
pub struct Inflight {
@@ -287,6 +288,7 @@ pub struct VhostUserEpollHandler<S: VhostUserFrontendReqHandler> {
pub inflight: Option<Inflight>,
/// Flag set by the worker when the vhost-user backend is no longer reachable.
pub disconnected: Arc<AtomicBool>,
pub resume_evt: EventFd,
}
impl<S: VhostUserFrontendReqHandler> VhostUserEpollHandler<S> {
@@ -306,6 +308,8 @@ impl<S: VhostUserFrontendReqHandler> VhostUserEpollHandler<S> {
helper.add_event(backend_req_handler.as_raw_fd(), BACKEND_REQ_EVENT)?;
}
helper.add_event(self.resume_evt.as_raw_fd(), RESUME_EVENT)?;
helper.run(paused, paused_sync, self)?;
Ok(())
@@ -415,6 +419,26 @@ impl<S: VhostUserFrontendReqHandler> EpollHelperHandler for VhostUserEpollHandle
Ok(())
}
}
RESUME_EVENT => {
let _ = self.resume_evt.read();
self.vu
.lock()
.unwrap()
.resume(
&self.mem.memory(),
&self.queues,
self.virtio_interrupt.as_ref(),
self.acked_features,
&self.backend_req_handler,
self.inflight.as_mut(),
)
.map_err(|e| {
EpollHelperError::HandleEvent(anyhow!(
"failed to resume vhost-user backend for socket {}: {e:?}",
self.socket_path
))
})
}
_ => Err(EpollHelperError::HandleEvent(anyhow!(
"Unknown event for vhost-user thread"
))),
@@ -472,6 +496,7 @@ pub struct VhostUserCommon {
pub disconnected: Arc<AtomicBool>,
saved_dirty_log: Option<MemoryRangeTable>,
dirty_logging: bool,
resume_evt: Option<EventFd>,
}
impl VhostUserCommon {
@@ -527,6 +552,13 @@ impl VhostUserCommon {
)
.map_err(ActivateError::VhostUserSetup)?;
let resume_evt = EventFd::new(libc::EFD_NONBLOCK).map_err(ActivateError::CreateEventFd)?;
self.resume_evt = Some(
resume_evt
.try_clone()
.map_err(ActivateError::CloneEventFd)?,
);
Ok(VhostUserEpollHandler {
vu: vu.clone(),
mem,
@@ -541,6 +573,7 @@ impl VhostUserCommon {
backend_req_handler,
inflight,
disconnected: self.disconnected.clone(),
resume_evt,
})
}
@@ -706,31 +739,22 @@ impl VhostUserCommon {
}
fn resume_internal(&mut self) -> result::Result<(), MigratableError> {
// Skip the resume_vhost_user call if the backend is disconnected. Process the queue
// interrupts to kick any paused workers.
if self.disconnected.load(Ordering::Relaxed) {
return Err(MigratableError::DeviceDisconnected(
self.socket_path.clone(),
));
}
if let Some(vu) = &self.vu
&& let Err(e) = vu.lock().unwrap().resume_vhost_user()
{
if e.is_transport_lost() {
self.disconnected.store(true, Ordering::Relaxed);
return Err(MigratableError::DeviceDisconnected(
self.socket_path.clone(),
));
}
let Some(evt) = &self.resume_evt else {
return Ok(());
};
return Err(MigratableError::Resume(anyhow!(
"Error resuming vhost-user backend for socket {}: {e:?}",
evt.write(1).map_err(|e| {
MigratableError::Resume(anyhow!(
"Error signaling vhost-user resume for socket {}: {e}",
self.socket_path
)));
}
Ok(())
))
})
}
pub fn resume(&mut self) -> result::Result<(), MigratableError> {

View File

@@ -67,6 +67,7 @@ pub struct VhostUserHandle {
acked_features: u64,
vrings_info: Option<Vec<VringInfo>>,
queue_indexes: Vec<u16>,
vring_bases: Option<Vec<u64>>,
}
impl VhostUserHandle {
@@ -172,6 +173,9 @@ impl VhostUserHandle {
return Err(Error::VringBasesCountMismatch(bases.len(), queues.len()));
}
// May run more than once on the same handle (resume after snapshot)
self.queue_indexes.clear();
self.vu
.set_features(acked_features)
.map_err(Error::VhostUserSetFeatures)?;
@@ -427,6 +431,7 @@ impl VhostUserHandle {
acked_features: 0,
vrings_info: None,
queue_indexes: Vec::new(),
vring_bases: None,
};
vhost_user
.vu
@@ -485,6 +490,7 @@ impl VhostUserHandle {
acked_features: 0,
vrings_info: None,
queue_indexes: Vec::new(),
vring_bases: None,
})
.map_err(Error::VhostUserConnect);
@@ -580,7 +586,7 @@ impl VhostUserHandle {
Ok(())
}
pub fn resume_vhost_user(&mut self) -> Result<()> {
fn resume_vhost_user(&mut self) -> Result<()> {
if self.ready {
self.enable_vhost_user_vrings(self.queue_indexes.clone(), true)?;
}
@@ -641,9 +647,39 @@ impl VhostUserHandle {
.check_device_state()
.map_err(Error::VhostUserCheckDeviceState)?;
// Store the bases in case we need to resume after snapshot
self.vring_bases = Some(vring_bases.clone());
Ok((state, vring_bases))
}
/// Resume the vhost-user backend from the device thread.
/// Initialize the vrings from the bases captured earlier, or simply
/// enable them if they haven't been stopped.
pub fn resume<S: VhostUserFrontendReqHandler>(
&mut self,
mem: &GuestMemoryMmap,
queues: &[(u16, Queue, EventFd)],
virtio_interrupt: &dyn VirtioInterrupt,
acked_features: u64,
backend_req_handler: &Option<FrontendReqHandler<S>>,
inflight: Option<&mut Inflight>,
) -> Result<()> {
let Some(vring_bases) = self.vring_bases.take() else {
return self.resume_vhost_user();
};
self.setup_vhost_user(
mem,
queues,
virtio_interrupt,
acked_features,
backend_req_handler,
inflight,
Some(&vring_bases),
)
}
pub fn restore_state<C>(&mut self, state: &VhostUserState<C>) -> Result<()> {
state.validate()?;
if let Some(backend_state) = &state.backend_state {