Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion nvx-rs/src/guest/cgroup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ impl ExecutionCgroup {

/// Returns the placement that makes a [spawned](super::spawn::Command::cgroup) child join
/// the execution's cgroup before its program runs, so that the program and its descendants
/// start in it.
/// start in it. The child joins in one of its own launch stages, so the cgroup may not hold
/// it until its launch reports: see [`Command::cgroup`](super::spawn::Command::cgroup).
pub fn placement(&self) -> io::Result<Cgroup> {
Cgroup::open_in(&self.directories.execution)
}
Expand All @@ -296,6 +297,9 @@ impl ExecutionCgroup {
///
/// It reads `pids.current`, which must be one canonical decimal and a newline: another form
/// is `EPROTO`, and one of more than 31 bytes `EOVERFLOW`.
///
/// A child that the caller spawned with the cgroup's [`placement`](Self::placement) and whose
/// launch has yet to report may not be in the cgroup yet: the check does not see it then.
pub fn is_occupied(&self, scope: Scope) -> io::Result<bool> {
let descriptor = open_file(self.directory(scope), PIDS_CURRENT, OpenFlags::O_RDONLY)?;
let mut buffer = [0u8; COUNT_CAPACITY];
Expand All @@ -322,6 +326,9 @@ impl ExecutionCgroup {
/// Kills every process in `scope` with `SIGKILL`, through `cgroup.kill`, which also kills a
/// process that one of them forks meanwhile. It does not wait for them to end: see
/// [`settle`](Self::settle).
///
/// A child that the caller spawned with the cgroup's [`placement`](Self::placement) and whose
/// launch has yet to report may not be in the cgroup yet: the kill does not reach it then.
pub fn kill(&self, scope: Scope) -> io::Result<()> {
write_in(self.directory(scope), KILL, b"1")
}
Expand Down
13 changes: 8 additions & 5 deletions nvx-rs/src/guest/exec/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@
//! Settlement goes through the cgroup that the execution's options name, if any, in the scope
//! that they give: once the program's status is known, the binding kills the cgroup's processes,
//! and reports the execution settled once the cgroup is empty, which it checks at most every 10
//! ms. From then on, the cgroup is the caller's again: neither the execution nor its drop kills
//! the cgroup's processes, so the caller may reuse the cgroup once the outcome is reusable,
//! whether or not it dropped the binding. A zombie still occupies the cgroup until its parent
//! collects it, so a caller whose executions' orphans it adopts, such as a PID 1, collects their
//! statuses between its calls.
//! ms with [`ExecutionCgroup::is_occupied`](crate::guest::cgroup::ExecutionCgroup::is_occupied),
//! from the `pids.current` of the cgroup that the scope covers. From then on, the cgroup is the
//! caller's again: neither the execution nor its drop kills the cgroup's processes, so the caller
//! may reuse the cgroup once the outcome is reusable, whether or not it dropped the binding.
//! `pids.current` counts a zombie until its parent collects it, so a caller whose executions'
//! orphans it adopts, such as a PID 1, collects their statuses between its calls. `cgroup.procs`
//! and the `populated` entry of `cgroup.events`, by contrast, drop a process as soon as it exits,
//! so a caller that watches those files can see the cgroup empty before the execution settles.
//! Without a cgroup, nothing proves that the program's descendants are gone, so the execution
//! never settles, and is fenced: the runner then gives up on the output streams that a process
//! may still hold the settlement time and the grace after the program's end, while it waits as
Expand Down
47 changes: 47 additions & 0 deletions nvx-rs/src/guest/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,16 @@ impl Command {

/// Makes the child join `cgroup` once it has placed its descriptors, before the rest of its
/// stages and its program, so that the program and its descendants start in it.
///
/// The child joins in one of its own launch stages, which [`spawn`](Self::spawn) does not
/// wait for, and which follows the release of its [barrier](Self::barrier), if it waits at
/// one. So until the child's [`LaunchReport`] arrives, the cgroup may not hold the child yet:
/// a check of the cgroup's occupancy, a kill of its processes, or its removal can miss the
/// child then, and so can a [settlement](crate::guest::cgroup::ExecutionCgroup::settle). A
/// caller that needs the child in the cgroup first waits for the report. Or it spawns the
/// child with a barrier instead of the cgroup, places it with
/// [`ExecutionCgroup::place`](crate::guest::cgroup::ExecutionCgroup::place), and then releases
/// the barrier.
pub fn cgroup(mut self, cgroup: Cgroup) -> Self {
self.spec.set_cgroup(cgroup.procs);
self
Expand Down Expand Up @@ -1033,6 +1043,43 @@ mod tests {
assert!(marker.exists());
}

#[test]
fn a_child_joins_its_cgroup_once_its_barrier_is_released() {
let directory = tempfile::tempdir().unwrap();
let cgroup = directory.path().join("cgroup");
std::fs::create_dir(&cgroup).unwrap();
let procs = cgroup.join("cgroup.procs");
std::fs::write(&procs, b"").unwrap();
// The child starts with a copy of `writer`, which it closes just before it waits at its
// barrier: once `reader` hangs up, every stage before the barrier has run.
let (reader, writer) = std::io::pipe().unwrap();
let mut spawned = shell("exit 0")
.barrier()
.cgroup(Cgroup::open(&cgroup).unwrap())
.spawn()
.unwrap();
drop(writer);
let (id, process) = (spawned.id(), spawned.process());
let barrier = spawned.take_barrier().unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while !sys::hung_up(std::os::fd::AsFd::as_fd(&reader)).unwrap() {
assert!(
std::time::Instant::now() < deadline,
"the child does not wait at its barrier"
);
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(!report_arrived(&spawned));
assert!(std::fs::read(&procs).unwrap().is_empty());
barrier.release().unwrap();
assert_eq!(spawned.into_report().wait().unwrap(), None);
assert_eq!(
std::fs::read(&procs).unwrap(),
format!("{id}\n").into_bytes()
);
assert_eq!(sys::wait(process), Some(ChildStatus::Exited(0)));
}

#[test]
fn a_dropped_barrier_cancels_the_launch() {
let directory = tempfile::tempdir().unwrap();
Expand Down
Loading