a653rs_linux_core/
cgroup.rs

1//! Implementation of the Linux *cgroup* facility
2//!
3//! This module provides an interface for the Linux cgroup facility.
4//! Interfacing applications either create or import a cgroup, which
5//! will then be used to build a tree to keep track of all following
6//! sub-cgroups.
7//!
8//! This approach makes it possible to only manage a certain sub-tree
9//! of cgroups, thereby saving resources. Alternatively, the root cgroup
10//! may be imported, keeping track of all cgroups existing on the host system.
11use std::fs::{self};
12use std::io::BufRead;
13use std::path::{Path, PathBuf};
14use std::time::{Duration, Instant};
15
16use anyhow::{anyhow, bail, Ok};
17use itertools::Itertools;
18use nix::sys::statfs;
19use nix::unistd::Pid;
20use walkdir::WalkDir;
21
22const KILLING_TIMEOUT: Duration = Duration::from_secs(1);
23
24/// A single cgroup inside our tree of managed cgroups
25///
26/// The tree is not represented by a traditional tree data structure,
27/// as this is very complicated in Rust. Instead, the tree is "calculated"
28/// by the path alone.
29#[derive(Debug)]
30pub struct CGroup {
31    path: PathBuf,
32}
33
34impl CGroup {
35    /// Creates a new cgroup as the root of a sub-tree
36    ///
37    /// path must be the path of an already existing cgroup
38    pub fn new_root<P: AsRef<Path>>(path: P, name: &str) -> anyhow::Result<Self> {
39        trace!("Create cgroup \"{name}\"");
40        // Double-checking if path is cgroup does not hurt, as it is
41        // better to not potentially create a directory at a random location.
42        if !is_cgroup(path.as_ref())? {
43            bail!("{} is not a valid cgroup", path.as_ref().display());
44        }
45
46        let path = PathBuf::from(path.as_ref()).join(name);
47
48        if path.exists() {
49            bail!("CGroup {path:?} already exists");
50        } else {
51            // will fail if the path already exists
52            fs::create_dir(&path)?;
53        }
54
55        Self::import_root(&path)
56    }
57
58    /// Imports an already existing cgroup as the root of a sub-tree
59    pub fn import_root<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
60        trace!("Import cgroup {}", path.as_ref().display());
61        let path = PathBuf::from(path.as_ref());
62
63        if !is_cgroup(&path)? {
64            bail!("{} is not a valid cgroup", path.display());
65        }
66
67        Ok(CGroup { path })
68    }
69
70    /// Creates a sub-cgroup inside this one
71    pub fn new(&self, name: &str) -> anyhow::Result<Self> {
72        Self::new_root(&self.path, name)
73    }
74
75    /// Creates a threaded sub-cgroup inside this one
76    pub fn new_threaded(&self, name: &str) -> anyhow::Result<Self> {
77        let cgroup = Self::new_root(&self.path, name)?;
78        cgroup.set_threaded()?;
79        Ok(cgroup)
80    }
81
82    /// Moves a process to this cgroup
83    pub fn mv_proc(&self, pid: Pid) -> anyhow::Result<()> {
84        trace!("Move {pid:?} to {}", self.get_path().display());
85        if !is_cgroup(&self.path)? {
86            bail!("{} is not a valid cgroup", self.path.display());
87        }
88
89        fs::write(self.path.join("cgroup.procs"), pid.to_string())?;
90        Ok(())
91    }
92
93    /// Moves a thread to this cgroup
94    pub fn mv_thread(&self, pid: Pid) -> anyhow::Result<()> {
95        trace!("Move {pid:?} to {}", self.get_path().display());
96        if !is_cgroup(&self.path)? {
97            bail!("{} is not a valid cgroup", self.path.display());
98        }
99
100        fs::write(self.path.join("cgroup.threads"), pid.to_string())?;
101        Ok(())
102    }
103
104    /// Changes the cgroups type to "threaded"
105    fn set_threaded(&self) -> anyhow::Result<()> {
106        trace!("Change type of {} to threaded", self.get_path().display());
107        if !is_cgroup(&self.path)? {
108            bail!("{} is not a valid cgroup", self.path.display());
109        }
110
111        fs::write(self.path.join("cgroup.type"), "threaded")?;
112        Ok(())
113    }
114
115    /// Returns all PIDs associated with this cgroup
116    pub fn get_pids(&self) -> anyhow::Result<Vec<Pid>> {
117        if !is_cgroup(&self.path)? {
118            bail!("{} is not a valid cgroup", self.path.display());
119        }
120
121        let pids: Vec<Pid> = fs::read(self.path.join("cgroup.procs"))?
122            .lines()
123            .map(|line| Pid::from_raw(line.unwrap().parse().unwrap()))
124            .collect();
125
126        Ok(pids)
127    }
128
129    /// Returns all TIDs associated with this cgroup
130    pub fn get_tids(&self) -> anyhow::Result<Vec<Pid>> {
131        if !is_cgroup(&self.path)? {
132            bail!("{} is not a valid cgroup", self.path.display());
133        }
134
135        let pids: Vec<Pid> = fs::read(self.path.join("cgroup.threads"))?
136            .lines()
137            .map(|line| Pid::from_raw(line.unwrap().parse().unwrap()))
138            .collect();
139
140        Ok(pids)
141    }
142
143    /// Checks whether this cgroup is populated
144    pub fn populated(&self) -> anyhow::Result<bool> {
145        if !is_cgroup(&self.path)? {
146            bail!("{} is not a valid cgroup", self.path.display());
147        }
148
149        Ok(fs::read_to_string(self.get_events_path())?.contains("populated 1\n"))
150    }
151
152    /// Checks whether this cgroup is frozen
153    pub fn frozen(&self) -> anyhow::Result<bool> {
154        if !is_cgroup(&self.path)? {
155            bail!("{} is not a valid cgroup", self.path.display());
156        }
157
158        // We need to check for the existance of cgroup.freeze, because
159        // this file does not exist on the root cgroup.
160        let path = self.path.join("cgroup.freeze");
161        if !path.exists() {
162            return Ok(false);
163        }
164
165        Ok(fs::read(&path)? == b"1\n")
166    }
167
168    /// Freezes this cgroup (does nothing if already frozen)
169    pub fn freeze(&self) -> anyhow::Result<()> {
170        trace!("Freeze {}", self.get_path().display());
171        if !is_cgroup(&self.path)? {
172            bail!("{} is not a valid cgroup", self.path.display());
173        }
174
175        // We need to check for the existance of cgroup.freeze, because
176        // this file does not exist on the root cgroup.
177        let path = self.path.join("cgroup.freeze");
178        if !path.exists() {
179            bail!("cannot freeze the root cgroup");
180        }
181
182        Ok(fs::write(path, "1")?)
183    }
184
185    /// Unfreezes this cgroup (does nothing if not frozen)
186    pub fn unfreeze(&self) -> anyhow::Result<()> {
187        trace!("Unfreeze {}", self.get_path().display());
188        if !is_cgroup(&self.path)? {
189            bail!("{} is not a valid cgroup", self.path.display());
190        }
191
192        // We need to check for the existance of cgroup.freeze, because
193        // this file does not exist on the root cgroup.
194        let path = self.path.join("cgroup.freeze");
195        if !path.exists() {
196            bail!("cannot unfreeze the root cgroup");
197        }
198
199        Ok(fs::write(path, "0")?)
200    }
201
202    /// Kills all processes in this cgroup and returns once this
203    /// procedure is finished
204    pub fn kill(&self) -> anyhow::Result<()> {
205        trace!("Kill {}", self.get_path().display());
206        if !is_cgroup(&self.path)? {
207            bail!("{} is not a valid cgroup", self.path.display());
208        }
209
210        // We need to check for the existance of cgroup.kill, because
211        // this file does not exist on the root cgroup.
212        let killfile = self.path.join("cgroup.kill");
213        if !killfile.exists() {
214            bail!("cannot kill the root cgroup");
215        }
216
217        // Emit the kill signal to all processes inside the cgroup
218        trace!("writing '1' to {}", killfile.display());
219        fs::write(killfile, "1")?;
220
221        // Check if all processes were terminated successfully
222        let start = Instant::now();
223        trace!("Killing with a {KILLING_TIMEOUT:?} timeout");
224        while start.elapsed() < KILLING_TIMEOUT {
225            if !self.populated()? {
226                trace!("Killed with a {KILLING_TIMEOUT:?} timeout");
227                return Ok(());
228            }
229        }
230
231        bail!("failed to kill the cgroup")
232    }
233
234    /// Returns the path of this cgroup
235    pub fn get_path(&self) -> PathBuf {
236        self.path.clone()
237    }
238
239    /// Returns the path of the event file, which may be polled
240    pub fn get_events_path(&self) -> PathBuf {
241        self.path.join("cgroup.events")
242    }
243
244    /// Kills all processes and removes the current cgroup
245    pub fn rm(&self) -> anyhow::Result<()> {
246        trace!("Remove {}", self.get_path().display());
247        if !is_cgroup(&self.path)? {
248            bail!("{} is not a valid cgroup", self.path.display());
249        }
250
251        // Calling kill will also kill all sub cgroup processes
252        self.kill()?;
253
254        // Delete all cgroups from deepest to highest.
255        // It is necessary to delete the outer-most directories first, because
256        // a non-empty directory may not be deleted.
257        trace!("Calling remove on {}", &self.path.display());
258        for d in WalkDir::new(&self.path)
259            .into_iter()
260            .flatten()
261            .filter(|e| e.file_type().is_dir())
262            .sorted_by(|a, b| a.depth().cmp(&b.depth()).reverse())
263        {
264            trace!("Removing cgroup {}", &d.path().display());
265            fs::remove_dir(d.path())?;
266        }
267
268        Ok(())
269    }
270
271    // TODO: Implement functions to fetch the parents and children
272}
273
274/// Returns the first cgroup2 mount point found on the host system
275pub fn mount_point() -> anyhow::Result<PathBuf> {
276    // TODO: This is an awful old function, replace it!
277    procfs::process::Process::myself()?
278        .mountinfo()?
279        .into_iter()
280        .find(|m| m.fs_type.eq("cgroup2")) // TODO A process can have several cgroup mounts
281        .ok_or_else(|| anyhow!("no cgroup2 mount found"))
282        .map(|m| m.mount_point.clone())
283}
284
285/// Returns the path relative to the cgroup mount to
286/// which cgroup this process belongs to
287pub fn current_cgroup() -> anyhow::Result<PathBuf> {
288    let path = procfs::process::Process::myself()?
289        .cgroups()?
290        .into_iter()
291        .next()
292        .ok_or(anyhow!("cannot obtain cgroup"))?
293        .pathname
294        .clone();
295    let path = &path[1..path.len()]; // Remove the leading '/'
296
297    Ok(PathBuf::from(path))
298}
299
300/// Checks if path is a valid cgroup by comparing the device id
301fn is_cgroup(path: &Path) -> anyhow::Result<bool> {
302    let st = statfs::statfs(path)?;
303    Ok(st.filesystem_type() == statfs::CGROUP2_SUPER_MAGIC)
304}
305
306#[cfg(test)]
307mod tests {
308    // The tests must be run as root with --test-threads=1
309
310    use std::{io, process};
311
312    use super::*;
313
314    #[test]
315    fn new_root() {
316        let name = gen_name();
317        let path = get_path().join(&name);
318        assert!(!path.exists()); // Ensure, that it does not already exist
319
320        let cg = CGroup::new_root(get_path(), &name).unwrap();
321        assert!(path.exists() && path.is_dir());
322
323        cg.rm().unwrap();
324        assert!(!path.exists());
325    }
326
327    #[test]
328    fn import_root() {
329        let path = get_path().join(gen_name());
330        assert!(!path.exists()); // Ensure, that it does not already exist
331        fs::create_dir(&path).unwrap();
332
333        let cg = CGroup::import_root(&path).unwrap();
334
335        cg.rm().unwrap();
336        assert!(!path.exists());
337    }
338
339    #[test]
340    fn new() {
341        let name1 = gen_name();
342        let name2 = gen_name();
343
344        let path_cg1 = get_path().join(&name1);
345        let path_cg2 = path_cg1.join(&name2);
346        assert!(!path_cg1.exists()); // Ensure, that it does not already exist
347
348        let cg1 = CGroup::new_root(get_path(), &name1).unwrap();
349        assert!(path_cg1.exists() && path_cg1.is_dir());
350        assert!(!path_cg2.exists());
351
352        let _cg2 = cg1.new(&name2).unwrap();
353        assert!(path_cg2.exists() && path_cg2.is_dir());
354
355        cg1.rm().unwrap();
356
357        assert!(!path_cg2.exists());
358        assert!(!path_cg1.exists());
359    }
360
361    #[test]
362    fn mv() {
363        let mut proc = spawn_proc().unwrap();
364        let pid = Pid::from_raw(proc.id() as i32);
365
366        let cg1 = CGroup::new_root(get_path(), &gen_name()).unwrap();
367        let cg2 = cg1.new(&gen_name()).unwrap();
368
369        cg1.mv_proc(pid).unwrap();
370        cg2.mv_proc(pid).unwrap();
371        proc.kill().unwrap();
372
373        cg1.rm().unwrap();
374    }
375
376    #[test]
377    fn get_pids() {
378        let mut proc = spawn_proc().unwrap();
379        let pid = Pid::from_raw(proc.id() as i32);
380
381        let cg1 = CGroup::new_root(get_path(), &gen_name()).unwrap();
382        let cg2 = cg1.new(&gen_name()).unwrap();
383
384        assert!(cg1.get_pids().unwrap().is_empty());
385        assert!(cg2.get_pids().unwrap().is_empty());
386
387        cg1.mv_proc(pid).unwrap();
388        let pids = cg1.get_pids().unwrap();
389        assert!(!pids.is_empty());
390        assert!(cg2.get_pids().unwrap().is_empty());
391        assert_eq!(pids.len(), 1);
392        assert_eq!(pids[0], pid);
393
394        cg2.mv_proc(pid).unwrap();
395        let pids = cg2.get_pids().unwrap();
396        assert!(!pids.is_empty());
397        assert!(cg1.get_pids().unwrap().is_empty());
398        assert_eq!(pids.len(), 1);
399        assert_eq!(pids[0], pid);
400
401        proc.kill().unwrap();
402
403        cg1.rm().unwrap();
404    }
405
406    #[test]
407    fn populated() {
408        let mut proc = spawn_proc().unwrap();
409        let pid = Pid::from_raw(proc.id() as i32);
410        let cg = CGroup::new_root(get_path(), &gen_name()).unwrap();
411
412        assert!(!cg.populated().unwrap());
413        assert_eq!(cg.populated().unwrap(), !cg.get_pids().unwrap().is_empty());
414
415        cg.mv_proc(pid).unwrap();
416        assert!(cg.populated().unwrap());
417        assert_eq!(cg.populated().unwrap(), !cg.get_pids().unwrap().is_empty());
418
419        proc.kill().unwrap();
420
421        cg.rm().unwrap();
422    }
423
424    #[test]
425    fn frozen() {
426        let mut proc = spawn_proc().unwrap();
427        let pid = Pid::from_raw(proc.id() as i32);
428        let cg = CGroup::new_root(get_path(), &gen_name()).unwrap();
429
430        // Freeze an empty cgroup
431        assert!(!cg.frozen().unwrap());
432        cg.freeze().unwrap();
433        assert!(cg.frozen().unwrap());
434
435        // Unfreeze the empty cgroup
436        cg.unfreeze().unwrap();
437        assert!(!cg.frozen().unwrap());
438
439        // Do the same with a non-empty cgroup
440        cg.mv_proc(pid).unwrap();
441        cg.freeze().unwrap();
442        assert!(cg.frozen().unwrap());
443        cg.unfreeze().unwrap();
444        assert!(!cg.frozen().unwrap());
445
446        proc.kill().unwrap();
447
448        cg.rm().unwrap();
449    }
450
451    #[test]
452    fn kill() {
453        let proc = spawn_proc().unwrap();
454        let pid = Pid::from_raw(proc.id() as i32);
455        let cg = CGroup::new_root(get_path(), &gen_name()).unwrap();
456
457        // Kill an empty cgroup
458        cg.kill().unwrap();
459
460        // Do the same with a non-empty cgroup
461        cg.mv_proc(pid).unwrap();
462        assert!(cg.populated().unwrap());
463        cg.kill().unwrap();
464
465        cg.rm().unwrap();
466
467        // TODO: Check if the previous PID still exists (although unstable
468        // because the OS may re-assign)
469    }
470
471    #[test]
472    fn is_cgroup() {
473        assert!(super::is_cgroup(&get_path()).unwrap());
474        assert!(!super::is_cgroup(Path::new("/tmp")).unwrap());
475    }
476
477    /// Spawns a child process of sleep(1)
478    fn spawn_proc() -> io::Result<process::Child> {
479        process::Command::new("sleep")
480            .arg("120")
481            .stdout(process::Stdio::null())
482            .spawn()
483    }
484
485    /// Returns the path of the current cgorup inside the mount
486    fn get_path() -> PathBuf {
487        super::mount_point()
488            .unwrap()
489            .join(super::current_cgroup().unwrap())
490    }
491
492    /// Generates a name for the current cgroup
493    fn gen_name() -> String {
494        loop {
495            let val: u64 = rand::random();
496            let str = format!("apex-test-{val}");
497            if !Path::new(&str).exists() {
498                return str;
499            }
500        }
501    }
502}