CgroupManager
CgroupManager manages cgroup v2 filesystem operations. It creates,
configures, and removes cgroups under a parent directory.
use ktstr::prelude::*;
pub struct CgroupManager {
parent: PathBuf,
}
Construction
use std::collections::BTreeSet;
let cgroups = CgroupManager::new("/sys/fs/cgroup/ktstr");
let mut controllers = BTreeSet::new();
controllers.insert(Controller::Cpuset);
controllers.insert(Controller::Cpu);
cgroups.setup(&controllers)?; // create parent dir, enable cpuset + cpu controllers
new() sets the parent path. setup() takes a
&BTreeSet<Controller> (variants: Cpuset, Cpu, Memory,
Pids, Io), creates the parent directory if it does not exist,
and enables the requested controllers on every ancestor from
/sys/fs/cgroup down to the parent by writing to each level’s
cgroup.subtree_control. An empty set creates the directory and
returns without touching subtree_control. The deterministic
BTreeSet iteration order keeps the rendered subtree_control
write stable between runs.
Methods
parent_path() -> &Path – returns the parent cgroup directory path.
create_cgroup(name) – creates a child cgroup directory. Idempotent:
no error if the directory already exists. Supports nested paths
(e.g. "nested/deep"). For nested paths, enables +cpuset on
intermediate cgroups’ subtree_control.
remove_cgroup(name) – drains tasks from the child cgroup to the
cgroup filesystem root, then removes the directory. No error if the
cgroup does not exist.
set_cpuset(name, cpus) – writes cpuset.cpus for a child cgroup.
The BTreeSet<usize> is formatted as a compact range string via
TestTopology::cpuset_string() (e.g. "0-3,5,7-9").
clear_cpuset(name) – writes an empty string to cpuset.cpus,
which inherits the parent’s cpuset.
move_task(name, pid) – writes a single PID to the child cgroup’s
cgroup.procs.
move_tasks(name, pids) – moves all PIDs from a slice into the
child cgroup. Tolerates ESRCH (task exited between listing and
migration) with a warning. Retries EBUSY up to 3 times with 100ms
backoff for transient rejections from sched_ext BPF
cgroup_prep_move callbacks. Propagates EBUSY after retries
exhausted. Propagates all other errors immediately.
drain_tasks(name) – moves all tasks from a child cgroup to the
cgroup filesystem root (/sys/fs/cgroup) by reading cgroup.procs
and writing each PID to the root’s cgroup.procs. Drains to root
because the parent has subtree_control set and the kernel’s
no-internal-process constraint rejects writes to a cgroup with
active controllers.
cleanup_all() – recursively removes all child cgroups under the
parent (depth-first), draining tasks at each level. Keeps the parent
directory itself.
Timeout protection
All cgroup filesystem writes use a 2-second timeout. The write runs in a spawned thread; if it does not complete within the timeout, the caller gets an error. This prevents test hangs when cgroup operations block in the kernel (e.g. during scheduler reconfigurations).
Usage in scenarios
Scenarios access CgroupManager through Ctx.cgroups. The typical
pattern is:
fn custom_scenario(ctx: &Ctx) -> Result<AssertResult> {
let mut guard = CgroupGroup::new(ctx.cgroups);
guard.add_cgroup("cg_0", &cpuset)?;
let mut h = WorkloadHandle::spawn(&config)?;
ctx.cgroups.move_tasks("cg_0", &h.worker_pids_for_cgroup_procs()?)?;
h.start(); // workers block until start() is called
// ... run workload ...
// `guard` drops at end of scope and removes cg_0 even on error.
Ok(result)
}
Bypass CgroupGroup only when you need to hand the
cgroup’s lifetime to a different owner; the RAII wrapper is the default
because it removes the cgroup on every error path, not just the happy
path.
See also: CgroupGroup for RAII cleanup, WorkloadHandle for worker lifecycle, TestTopology for cpuset generation.