Skip to content

Commit 3e239a7

Browse files
committed
Shutdown VCPU threads so they can thread::join
This continues the work from the prior commit toward the goal of releasing all resources with a clean shutdown. The VCPU threads in non-test configurations were running with an unbreakable wait, so they only way to get past them was with exit(): // Wait indefinitely // The VMM thread will kill the entire process let barrier = Barrier::new(2); barrier.wait(); Here that is changed, along with several other entangled issues. One was that Drop methods were used to try and clean up the Vmm and the VcpuHandles, but these were not being called. The reason is that there was a cyclic dependency: threads held onto the Vmm, so the Vmm could not exit VCPU threads in its Drop method (as it wouldn't be dropped so long as the VCPU threads were around). Another complexity surrounds defining the difference between an exited thread (from which an exit event can be read out of the channel) and a finished thread (which tears down the channel). Trying to send a message to a thread which has finished will panic, and some CPUs might be in the exit state while others may not...and reading the "exit state" by taking the message out of the channel removes it. It's a bit complicated. This commit tries to move toward simplification by being explicit about the Vmm.stop() call being responsible for exiting the VCPUs, and making the VcpuHandle's Drop method only do the simple task of joining an exited thread.
1 parent 0aeca43 commit 3e239a7

File tree

2 files changed

+115
-49
lines changed

2 files changed

+115
-49
lines changed

src/vmm/src/lib.rs

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use std::collections::HashMap;
3636
use std::fmt::{Display, Formatter};
3737
use std::io;
3838
use std::os::unix::io::AsRawFd;
39-
use std::sync::mpsc::RecvTimeoutError;
39+
use std::sync::mpsc::{RecvTimeoutError, TryRecvError};
4040
use std::sync::Mutex;
4141
use std::time::Duration;
4242

@@ -317,12 +317,26 @@ impl Vmm {
317317

318318
/// Sends an exit command to the vCPUs.
319319
pub fn exit_vcpus(&mut self) -> Result<()> {
320-
self.broadcast_vcpu_event(
321-
VcpuEvent::Exit,
322-
VcpuResponse::Exited(FC_EXIT_CODE_GENERIC_ERROR),
323-
)
324-
.map_err(|_| Error::VcpuExit)
320+
//
321+
// We actually send a "Finish" event. If a VCPU has already exited, this is
322+
// the only message it will accept...but runinng and paused will take it as well.
323+
// It breaks out of the state machine loop so that the thread can be joined.
324+
//
325+
for handle in &self.vcpus_handles {
326+
handle
327+
.send_event(VcpuEvent::Finish)
328+
.map_err(|_| Error::VcpuMessage)?;
329+
}
330+
331+
// The actual thread::join() that runs to release the thread's resource is done in
332+
// the VcpuHandle's Drop trait. We can trigger that to happen now by clearing the
333+
// list of handles (Vmm's Drop will also asserts this list is empty).
334+
//
335+
self.vcpus_handles.clear();
336+
337+
Ok(())
325338
}
339+
326340
/// Returns a reference to the inner `GuestMemoryMmap` object if present, or `None` otherwise.
327341
pub fn guest_memory(&self) -> &GuestMemoryMmap {
328342
&self.guest_memory
@@ -344,6 +358,8 @@ impl Vmm {
344358
pub fn stop(&mut self) {
345359
info!("Vmm is stopping.");
346360

361+
self.exit_vcpus().unwrap(); // exit all not-already-exited VCPUs, join their threads
362+
347363
if let Some(observer) = self.events_observer.as_mut() {
348364
if let Err(e) = observer.on_vmm_stop() {
349365
warn!("{}", Error::VmmObserverTeardown(e));
@@ -706,7 +722,24 @@ fn construct_kvm_mpidrs(vcpu_states: &[VcpuState]) -> Vec<u64> {
706722

707723
impl Drop for Vmm {
708724
fn drop(&mut self) {
709-
let _ = self.exit_vcpus();
725+
//
726+
// !!! This used to think it exited the cpus, which would do a thread join and tie up
727+
// the vcpu threads:
728+
//
729+
// let _ = self.exit_vcpus();
730+
//
731+
// But since the main firecracker process used exit() to terminate in mid-stack, the
732+
// destructors for Vmm and other classes would not run. Even after mitigating that by
733+
// running the exit from a top-level main() wrapper, the Vmm's Drop still was not
734+
// happening because of a cycle: VCPU threads held references to the Vmm, so you couldn't
735+
// have the Vmm drop be how the threads were exiting. Further, the VCPU threads would
736+
// not exit cleanly--they'd just block on a barrier--and could not be thread::join'd.
737+
//
738+
// To be more explicit and untangle those problems, exit_vcpus() is moved into Vmm::stop().
739+
// So now, this Drop method just asserts that happened. It won't trigger if there's
740+
// another situation where there's no Vmm Drop...but a Valgrind run will pick up the leak.
741+
//
742+
assert!(self.vcpus_handles.is_empty());
710743
}
711744
}
712745

@@ -718,20 +751,49 @@ impl Subscriber for Vmm {
718751

719752
if source == self.exit_evt.as_raw_fd() && event_set == EventSet::IN {
720753
let _ = self.exit_evt.read();
754+
755+
let mut opt_exit_code: Option<ExitCode> = None;
756+
721757
// Query each vcpu for the exit_code.
758+
//
759+
for handle in &self.vcpus_handles {
760+
match handle.response_receiver().try_recv() {
761+
Ok(VcpuResponse::Exited(status)) => {
762+
if opt_exit_code.is_none() {
763+
opt_exit_code = Some(status);
764+
} else {
765+
warn!("Multiple VCPU exit states detected, using first exit code");
766+
}
767+
}
768+
Err(TryRecvError::Empty) => {} // Nothing pending in channel
769+
Ok(_response) => {
770+
//
771+
// !!! This removes a response from the VCPU channel. Is there
772+
// anything to lose from that, given that we are exiting?
773+
}
774+
Err(e) => {
775+
panic!("Error while looking for VCPU exit status: {}", e);
776+
}
777+
}
778+
}
779+
780+
// !!! The caller of this routine is receiving the exit code to bubble back up
781+
// to the main() function to return cleanly. However, it does not have clean
782+
// access to the Vmm to shut it down (here we have it, since it is `self`). It
783+
// might seem tempting to put the stop() code in the Drop trait...but since
784+
// stopping the Vmm involves shutting down VCPU threads which hold references
785+
// on the Vmm, that presents a cycle. The solution of the moment is to do
786+
// everything here...which means this is the only process_exitable() handler
787+
// that will actually work with an exit code (all other Subscriber trait
788+
// implementers must use process())
789+
//
790+
self.stop();
791+
722792
// If the exit_code can't be found on any vcpu, it means that the exit signal
723793
// has been issued by the i8042 controller in which case we exit with
724794
// FC_EXIT_CODE_OK.
725-
let exit_code = self
726-
.vcpus_handles
727-
.iter()
728-
.find_map(|handle| match handle.response_receiver().try_recv() {
729-
Ok(VcpuResponse::Exited(exit_code)) => Some(exit_code),
730-
_ => None,
731-
})
732-
.unwrap_or(FC_EXIT_CODE_OK);
733-
self.stop();
734-
Some(exit_code)
795+
//
796+
Some(opt_exit_code.unwrap_or(FC_EXIT_CODE_OK))
735797
} else {
736798
error!("Spurious EventManager event for handler: Vmm");
737799
None

src/vmm/src/vstate/vcpu/mod.rs

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
// found in the THIRD-PARTY file.
77

88
use libc::{c_int, c_void, siginfo_t};
9-
#[cfg(not(test))]
10-
use std::sync::Barrier;
119
#[cfg(test)]
1210
use std::sync::Mutex;
1311
use std::{
@@ -332,6 +330,7 @@ impl Vcpu {
332330
.expect("failed to send save not allowed status");
333331
}
334332
Ok(VcpuEvent::Exit) => return self.exit(FC_EXIT_CODE_GENERIC_ERROR),
333+
Ok(VcpuEvent::Finish) => return StateMachine::finish(),
335334
// Unhandled exit of the other end.
336335
Err(TryRecvError::Disconnected) => {
337336
// Move to 'exited' state.
@@ -396,6 +395,7 @@ impl Vcpu {
396395
StateMachine::next(Self::paused)
397396
}
398397
Ok(VcpuEvent::Exit) => self.exit(FC_EXIT_CODE_GENERIC_ERROR),
398+
Ok(VcpuEvent::Finish) => StateMachine::finish(),
399399
// Unhandled exit of the other end.
400400
Err(_) => {
401401
// Move to 'exited' state.
@@ -404,41 +404,38 @@ impl Vcpu {
404404
}
405405
}
406406

407-
#[cfg(not(test))]
408407
// Transition to the exited state.
409408
fn exit(&mut self, exit_code: u8) -> StateMachine<Self> {
410409
self.response_sender
411410
.send(VcpuResponse::Exited(exit_code))
412411
.expect("vcpu channel unexpectedly closed");
413412

414-
if let Err(e) = self.exit_evt.write(1) {
415-
METRICS.vcpu.failures.inc();
416-
error!("Failed signaling vcpu exit event: {}", e);
417-
}
418-
419-
// State machine reached its end.
420413
StateMachine::next(Self::exited)
421414
}
422415

423-
#[cfg(not(test))]
424416
// This is the main loop of the `Exited` state.
425417
fn exited(&mut self) -> StateMachine<Self> {
426-
// Wait indefinitely.
427-
// The VMM thread will kill the entire process.
428-
let barrier = Barrier::new(2);
429-
barrier.wait();
430-
431-
StateMachine::finish()
432-
}
418+
//
419+
// !!! This signaled `EventFd` is not used by the main protocol. Is it specific to test?
420+
// What does the test actually want to know by seeing it set?
421+
//
422+
if let Err(e) = self.exit_evt.write(1) {
423+
METRICS.vcpu.failures.inc();
424+
error!("Failed signaling vcpu exit event: {}", e);
425+
}
433426

434-
#[cfg(test)]
435-
// In tests the main/vmm thread exits without 'exit()'ing the whole process.
436-
// All channels get closed on the other side while this Vcpu thread is still running.
437-
// This Vcpu thread should just do a clean finish without reporting back to the main thread.
438-
fn exit(&mut self, _: u8) -> StateMachine<Self> {
439-
self.exit_evt.write(1).unwrap();
440-
// State machine reached its end.
441-
StateMachine::finish()
427+
// !!! Stylistically we might like to force a transition to an exit state for all VCPUs
428+
// before allowing them to accept a Finish and terminate the state loop. But you run into
429+
// trouble with sending Exit to already exited VCPUs. This is because right now, reading
430+
// (and removing) an exited event from the channel is how the main thread discovers exits
431+
// arising from the VCPU itself. So by the time clean shutdown is performed, one can't
432+
// tell if a VCPU is in the exit state or not. For the moment, the main benefit is that
433+
// the exit state is able to refuse to process any other events besides Finish.
434+
//
435+
match self.event_receiver.recv() {
436+
Ok(VcpuEvent::Finish) => StateMachine::finish(),
437+
_ => panic!("exited() state of VCPU can only respond to Finish events (for thread join)")
438+
}
442439
}
443440

444441
#[cfg(not(test))]
@@ -558,6 +555,8 @@ impl Drop for Vcpu {
558555
pub enum VcpuEvent {
559556
/// The vCPU will go to exited state when receiving this message.
560557
Exit,
558+
/// Actual thread (channel) ends on finish--hence no response.
559+
Finish,
561560
/// Pause the Vcpu.
562561
Pause,
563562
/// Event to resume the Vcpu.
@@ -825,15 +824,20 @@ mod tests {
825824
}
826825
}
827826

828-
// In tests we need to close any pending Vcpu threads on test completion.
827+
// Wait for the Vcpu thread to finish execution
829828
impl Drop for VcpuHandle {
830829
fn drop(&mut self) {
831-
// Make sure the Vcpu is out of KVM_RUN.
832-
self.send_event(VcpuEvent::Pause).unwrap();
833-
// Close the original channel so that the Vcpu thread errors and goes to exit state.
834-
let (event_sender, _event_receiver) = channel();
835-
self.event_sender = event_sender;
836-
// Wait for the Vcpu thread to finish execution
830+
//
831+
// !!! Previously, this Drop code would attempt to send messages to the Vcpu
832+
// to put it in an exit state. We now assume that by the time a VcpuHandle is
833+
// dropped, other code has run to get the state machine loop to finish so the
834+
// thread is ready to join. The strategy of avoiding more complex messaging
835+
// protocols during the Drop helps avoid cycles which were preventing a truly
836+
// clean shutdown.
837+
//
838+
// If the code hangs at this point, that means that a Finish event was not
839+
// sent by Vmm.stop().
840+
//
837841
self.vcpu_thread.take().unwrap().join().unwrap();
838842
}
839843
}

0 commit comments

Comments
 (0)