Skip to content

Commit 4434d2c

Browse files
committed
Add --cleanup-level switch for choice of fast exit
While cleanly exiting threads and running all destructors in Rust is good for peace of mind and Valgrind, it is slower than just calling exit() and letting the OS clean up. In order to prevent the new graceful exit mechanics from slowing down typical runs, this adds a new switch `--cleanup-level`. The default is `--cleanup-level 1`, which will exit at basically the same time that Firecracker would quit previously. It uses libc::exit() to do so (since that's what it used before), but there may be reasons to change to Rust's std::process:exit(), if the code that runs is important. The new clean exit is done by `--cleanup-level 2`. This will join all threads and exit at the topmost main() level, so that all the Firecracker code is off the stack. An added mode which doesn't run any cleanup at all is given for `--cleanup-level 0`. This may or may not be useful, but it would be the fastest way to quit. Because a strange URL had been added to the API thread for internal use in clean shutdown, this hides that facility unless cleanup-level 2 is being used.
1 parent 3e239a7 commit 4434d2c

File tree

5 files changed

+140
-15
lines changed

5 files changed

+140
-15
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
- Added option to configure block device flush.
1010
- Added `--new-pid-ns` flag to the Jailer in order to spawn the Firecracker
1111
process in a new PID namespace.
12+
- Added `--cleanup-level` parameter to Firecracker so you can ask to gracefully
13+
shut down threads and release all resources, or exit abruptly. (Higher
14+
cleanup levels make shutdown slower, but help with tools like Valgrind.)
1215

1316
### Fixed
1417

src/api_server/src/parsed_request.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use micro_http::{Body, Method, Request, Response, StatusCode, Version};
2424

2525
use logger::{error, info};
2626
use vmm::rpc_interface::{VmmAction, VmmActionError};
27+
use vmm::{CleanupLevel, CLEANUP_LEVEL};
2728

2829
pub(crate) enum ParsedRequest {
2930
GetInstanceInfo,
@@ -58,7 +59,18 @@ impl ParsedRequest {
5859

5960
match (request.method(), path, request.body.as_ref()) {
6061
(Method::Get, "", None) => parse_get_instance_info(),
61-
(Method::Get, "shutdown-internal", None) => Ok(ParsedRequest::ShutdownInternal),
62+
(Method::Get, "shutdown-internal", None) => {
63+
//
64+
// This isn't a user-facing API, and was added solely to facilitate clean shutdowns.
65+
// Calling it manually will cause problems, so only enable it if the command line option
66+
// facilitating "Valgrind-level-cleanliness" is being used.
67+
//
68+
let cleanup_level = *CLEANUP_LEVEL.read().unwrap();
69+
match cleanup_level {
70+
CleanupLevel::Valgrind => Ok(ParsedRequest::ShutdownInternal),
71+
_ => Err(Error::InvalidPathMethod("shutdown-internal".to_string(), Method::Get))
72+
}
73+
},
6274
(Method::Get, "balloon", None) => parse_get_balloon(path_tokens.get(1)),
6375
(Method::Get, "machine-config", None) => parse_get_machine_config(),
6476
(Method::Get, "mmds", None) => parse_get_mmds(),

src/firecracker/src/api_server_adapter.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use vmm::{
2727
rpc_interface::{PrebootApiController, RuntimeApiController, VmmAction},
2828
vmm_config::instance_info::InstanceInfo,
2929
Vmm,
30+
CleanupLevel, CLEANUP_LEVEL
3031
};
3132

3233
struct ApiServerAdapter {
@@ -251,16 +252,19 @@ pub(crate) fn run_with_api(
251252
&mut event_manager,
252253
);
253254

255+
// Note: By default, this point won't ever be reached...because the system can just
256+
// call exit() and have the OS terminate all the threads. It's faster. But when the
257+
// `--cleanup-level 2` is specified, this code runs.
258+
//
259+
assert!(*CLEANUP_LEVEL.read().unwrap() == CleanupLevel::Valgrind);
260+
254261
// We want to tell the API thread to shut down for a clean exit. But this is after
255262
// the Vmm.stop() has been called, so it's a moment of internal finalization (as
256263
// opposed to be something the client might call to shut the Vm down). Since it's
257264
// an internal signal implementing it with an HTTP request is probably not the ideal
258265
// way to do it...but having another way would involve waiting on the socket or some
259266
// other signal. This leverages the existing wait.
260267
//
261-
// !!! Since the code is only needed for a "clean" shutdown mode, a non-clean mode
262-
// could not respond to the request, making this effectively a debug-only feature.
263-
//
264268
let mut sock = UnixStream::connect(bind_path).unwrap();
265269
assert!(sock.write_all(b"GET /shutdown-internal HTTP/1.1\r\n\r\n").is_ok());
266270

src/firecracker/src/main.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use seccomp::{BpfProgram, SeccompLevel};
1616
use utils::arg_parser::{ArgParser, Argument};
1717
use utils::terminal::Terminal;
1818
use utils::validators::validate_instance_id;
19+
use vmm::{CleanupLevel, CLEANUP_LEVEL};
1920
use vmm::default_syscalls::get_seccomp_filter;
2021
use vmm::resources::VmResources;
2122
use vmm::signal_handler::{mask_handled_signals, SignalManager};
@@ -142,6 +143,15 @@ fn main_exitable() -> ExitCode {
142143
Argument::new("version")
143144
.takes_value(false)
144145
.help("Print the binary version number and a list of supported snapshot data format versions.")
146+
)
147+
.arg(
148+
Argument::new("cleanup-level")
149+
.takes_value(true)
150+
.default_value("1")
151+
.help(
152+
"How much cleanup on exit (0: abruptly call sys::exit | 1: minimal shutdown functions (default) \"
153+
| 2: wait for all threads to exit and all destructors to run (for use with Valgrind)."
154+
),
145155
);
146156

147157
let arguments = match arg_parser.parse_from_cmdline() {
@@ -216,6 +226,15 @@ fn main_exitable() -> ExitCode {
216226
panic!("Could not create seccomp filter: {}", err);
217227
});
218228

229+
// The lazy_static! CLEANUP_LEVEL is only written here, but can be read from anywhere.
230+
let cleanup_level_str = arguments.single_value("cleanup-level").unwrap();
231+
{
232+
let mut cleanup_level = CLEANUP_LEVEL.write().unwrap();
233+
*cleanup_level = CleanupLevel::from_string(&cleanup_level_str).unwrap_or_else(|err| {
234+
panic!("Invalid value for cleanup-level: {}", err);
235+
});
236+
}
237+
219238
let vmm_config_json = arguments
220239
.single_value("config-file")
221240
.map(fs::read_to_string)

src/vmm/src/lib.rs

Lines changed: 98 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ use std::sync::mpsc::{RecvTimeoutError, TryRecvError};
4040
use std::sync::Mutex;
4141
use std::time::Duration;
4242

43+
use lazy_static::lazy_static; // for CLEANUP_LEVEL arg to be globally visible
44+
use std::sync::RwLock;
45+
4346
#[cfg(target_arch = "x86_64")]
4447
use crate::device_manager::legacy::PortIODeviceManager;
4548
use crate::device_manager::mmio::MMIODeviceManager;
@@ -93,6 +96,62 @@ pub const FC_EXIT_CODE_BAD_CONFIGURATION: u8 = 152;
9396
/// Command line arguments parsing error.
9497
pub const FC_EXIT_CODE_ARG_PARSING: u8 = 153;
9598

99+
#[derive(Debug)]
100+
/// Possible errors that could be encountered while processing a cleanup level value
101+
pub enum CleanupLevelError {
102+
/// Failed to parse to `u8`.
103+
Parse(std::num::ParseIntError),
104+
/// Cleanup level is an `u8` value, other than 0, 1 or 2.
105+
Level(u8),
106+
}
107+
108+
impl Display for CleanupLevelError {
109+
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
110+
match *self {
111+
CleanupLevelError::Parse(ref err) => {
112+
write!(f, "Could not parse to 'u8': {}", err)
113+
},
114+
CleanupLevelError::Level(arg) => write!(
115+
f,
116+
"'{}' isn't a valid value for 'cleanup-level'. Must be 0, 1 or 2.",
117+
arg
118+
),
119+
}
120+
}
121+
}
122+
123+
/// Possible values for cleanup level.
124+
#[repr(u8)]
125+
#[derive(Clone, Copy, Debug, PartialEq)]
126+
pub enum CleanupLevel {
127+
/// Abruptly call std::exit, so even successful shutdown is like a crash (fastest)
128+
Abrupt = 0,
129+
/// Normal shutdown, but don't worry about freeing memory or joining threads (let OS do it)
130+
Default = 1,
131+
/// Make sure all threads join and exit code bubbles up to main, for sanitizer accounting.
132+
Valgrind = 2,
133+
}
134+
135+
impl CleanupLevel {
136+
/// Converts from a cleanup level value of type String to the corresponding CleanupLevel variant
137+
/// or returns an error if the parsing failed.
138+
pub fn from_string(cleanup_value: &str) -> std::result::Result<Self, CleanupLevelError> {
139+
match cleanup_value.parse::<u8>() {
140+
Ok(0) => Ok(CleanupLevel::Abrupt),
141+
Ok(1) => Ok(CleanupLevel::Default),
142+
Ok(2) => Ok(CleanupLevel::Valgrind),
143+
Ok(level) => Err(CleanupLevelError::Level(level)),
144+
Err(err) => Err(CleanupLevelError::Parse(err)),
145+
}
146+
}
147+
}
148+
149+
lazy_static! {
150+
/// Static instance for conveying the command-line `--cleanup-level` setting to the VMM.
151+
pub static ref CLEANUP_LEVEL: RwLock<CleanupLevel> = RwLock::new(CleanupLevel::Default);
152+
}
153+
154+
96155
/// Errors associated with the VMM internal logic. These errors cannot be generated by direct user
97156
/// input, but can result from bad configuration of the host (for example if Firecracker doesn't
98157
/// have permissions to open the KVM fd).
@@ -353,12 +412,23 @@ impl Vmm {
353412
.map_err(Error::I8042Error)
354413
}
355414

356-
/// Waits for all vCPUs to exit. Does not terminate the Firecracker process.
357-
/// (See notes in main() about why ExitCode is bubbled up for clean shutdown.)
358-
pub fn stop(&mut self) {
415+
/// This stops the VMM. Based on the setting of `--cleanup-level`, it may also exit the
416+
/// Firecracker process entirely. It does so by default (cleanup-level of 1), but for
417+
/// sanity checking and Valgrind use it's important to offer a higher level of cleanliness
418+
/// which can gracefully exit all threads and bubble the exit_code up to main().
419+
///
420+
pub fn stop(&mut self, exit_code: ExitCode) -> ExitCode {
359421
info!("Vmm is stopping.");
360422

361-
self.exit_vcpus().unwrap(); // exit all not-already-exited VCPUs, join their threads
423+
let cleanup_level = *CLEANUP_LEVEL.read().unwrap();
424+
425+
if cleanup_level == CleanupLevel::Abrupt {
426+
//
427+
// Most severe form of exiting (also the fastest), does not run any Rust shutdown
428+
// that happens with `std::process::exit`. Similar to crashing.
429+
//
430+
unsafe { libc::exit(i32::from(exit_code)); }
431+
}
362432

363433
if let Some(observer) = self.events_observer.as_mut() {
364434
if let Err(e) = observer.on_vmm_stop() {
@@ -370,6 +440,23 @@ impl Vmm {
370440
if let Err(e) = METRICS.write() {
371441
error!("Failed to write metrics while stopping: {}", e);
372442
}
443+
444+
if cleanup_level == CleanupLevel::Default {
445+
//
446+
// This runs as much shutdown code as Firecracker had before the inclusion of the
447+
// ability to do CleanupLevel::Valgrind.
448+
//
449+
// !!! This preserves the usage of libc::exit() vs. std::process::exit(), just to
450+
// keep it the same. But was that initial choice intentional?
451+
//
452+
unsafe { libc::exit(i32::from(exit_code)); }
453+
}
454+
455+
assert!(cleanup_level == CleanupLevel::Valgrind); // bubble up exit_code to main()
456+
457+
self.exit_vcpus().unwrap(); // exit all not-already-exited VCPUs, join their threads
458+
459+
return exit_code
373460
}
374461

375462
/// Saves the state of a paused Microvm.
@@ -777,6 +864,12 @@ impl Subscriber for Vmm {
777864
}
778865
}
779866

867+
// If the exit_code can't be found on any vcpu, it means that the exit signal
868+
// has been issued by the i8042 controller in which case we exit with
869+
// FC_EXIT_CODE_OK.
870+
//
871+
let exit_code = opt_exit_code.unwrap_or(FC_EXIT_CODE_OK);
872+
780873
// !!! The caller of this routine is receiving the exit code to bubble back up
781874
// to the main() function to return cleanly. However, it does not have clean
782875
// access to the Vmm to shut it down (here we have it, since it is `self`). It
@@ -787,13 +880,7 @@ impl Subscriber for Vmm {
787880
// that will actually work with an exit code (all other Subscriber trait
788881
// implementers must use process())
789882
//
790-
self.stop();
791-
792-
// If the exit_code can't be found on any vcpu, it means that the exit signal
793-
// has been issued by the i8042 controller in which case we exit with
794-
// FC_EXIT_CODE_OK.
795-
//
796-
Some(opt_exit_code.unwrap_or(FC_EXIT_CODE_OK))
883+
Some(self.stop(exit_code)) // may exit abruptly, depending on CLEANUP_LEVEL
797884
} else {
798885
error!("Spurious EventManager event for handler: Vmm");
799886
None

0 commit comments

Comments
 (0)