cgroups_fs/
lib.rs

1//! Cgroup-fs is a minimal wrapper around Linux Control Groups (cgroups) filesystem (usually
2//! mounted as `/sys/fs/cgroup`).
3//!
4//! # Examples
5//!
6//! ## Get memory usage from root cgroup
7//!
8//! ```
9//! let root_cgroup = cgroups_fs::CgroupName::new("");
10//! let root_memory_cgroup = cgroups_fs::Cgroup::new(&root_cgroup, "memory");
11//! println!(
12//!     "Current memory usage is {} bytes",
13//!     root_memory_cgroup.get_value::<u64>("memory.usage_in_bytes").unwrap()
14//! );
15//! ```
16//!
17//! ## Measure memory usage of a child process
18//!
19//! Read [the `CgroupsCommandExt` documentation].
20//!
21//! [the `CgroupsCommandExt` documentation]: trait.CgroupsCommandExt.html#impl-CgroupsCommandExt
22#![cfg(target_os = "linux")]
23#![deny(missing_docs)]
24
25use std::fs;
26use std::io;
27use std::os::unix::process::CommandExt;
28use std::path::{Path, PathBuf};
29
30use nix;
31
32/// A common structure holding a cgroups name (path).
33#[derive(Debug)]
34pub struct CgroupName {
35    mount_point: PathBuf,
36    name: PathBuf,
37}
38
39impl CgroupName {
40    /// Defines a new cgroups name.
41    ///
42    /// Notes:
43    /// * It does not create any cgroups. It is just an API abstraction layer. Learn more about
44    /// [`Cgroup::new`], [`Cgroup::create`], [`Cgroup::remove`], and [`AutomanagedCgroup::init`]
45    /// methods.
46    ///
47    /// [`Cgroup::new`]: struct.Cgroup.html#method.new
48    /// [`Cgroup::create`]: struct.Cgroup.html#method.create
49    /// [`Cgroup::remove`]: struct.Cgroup.html#method.remove
50    /// [`AutomanagedCgroup::init`]: struct.AutomanagedCgroup.html#method.init
51    pub fn new<P>(name: P) -> Self
52    where
53        P: AsRef<Path>,
54    {
55        Self {
56            // TODO: auto-discover the cgroups FS mount-point
57            mount_point: "/sys/fs/cgroup".into(),
58            name: name.as_ref().to_path_buf(),
59        }
60    }
61}
62
63/// A controller of a specific cgroups namespace.
64///
65/// This type supports a number of operations for manipulating with a cgroups namespace.
66#[derive(Debug)]
67pub struct Cgroup {
68    root: PathBuf,
69}
70
71impl Cgroup {
72    /// Defines a cgroup relation.
73    ///
74    /// Notes:
75    /// * It does not create any cgroups. It is just an API abstraction layer. Learn more about
76    /// [`Cgroup::create`], [`Cgroup::remove`], and [`AutomanagedCgroup::init`] methods.
77    ///
78    /// [`Cgroup::create`]: #method.create
79    /// [`Cgroup::remove`]: #method.remove
80    /// [`AutomanagedCgroup::init`]: struct.AutomanagedCgroup.html#method.init
81    pub fn new(cgroup_name: &CgroupName, subsystem: &str) -> Self {
82        Self {
83            root: cgroup_name
84                .mount_point
85                .join(subsystem)
86                .join(&cgroup_name.name),
87        }
88    }
89
90    /// Creates a cgroups namespace.
91    ///
92    /// Notes:
93    /// * Keep in mind the usual filesystem permissions (owner, group, and mode bits).
94    pub fn create(&self) -> io::Result<()> {
95        fs::create_dir(&self.root).map_err(|error| {
96            io::Error::new(
97                error.kind(),
98                format!(
99                    "Cgroup cannot be created due to: {} (tried creating {:?} directory)",
100                    error, self.root
101                ),
102            )
103        })
104    }
105
106    /// Removes a cgroups namespace.
107    ///
108    /// Notes:
109    /// * This method will fail if there are nested cgroups.
110    /// * Keep in mind the usual filesystem permissions (owner, group, and mode bits).
111    pub fn remove(&self) -> io::Result<()> {
112        fs::remove_dir(&self.root).map_err(|error| {
113            io::Error::new(
114                error.kind(),
115                format!(
116                    "Cgroup cannot be removed due to: {} (tried removing {:?} directory)",
117                    error, self.root
118                ),
119            )
120        })
121    }
122
123    /// Sets a binary or string value to the cgroup control file.
124    pub fn set_raw_value<V>(&self, key: &str, value: V) -> io::Result<()>
125    where
126        V: AsRef<[u8]>,
127    {
128        let key = self.root.join(key);
129        fs::write(&key, value).map_err(|error| {
130            io::Error::new(
131                error.kind(),
132                format!(
133                    "Cgroup value under key {:?} cannot be set due to: {}",
134                    key, error
135                ),
136            )
137        })
138    }
139
140    /// Sets a value to the cgroup control file.
141    pub fn set_value<V>(&self, key: &str, value: V) -> io::Result<()>
142    where
143        V: Copy + ToString,
144    {
145        self.set_raw_value(key, value.to_string())
146    }
147
148    /// Gets a string value from cgroup control file.
149    pub fn get_raw_value(&self, key: &str) -> io::Result<String> {
150        let key = self.root.join(key);
151        fs::read_to_string(&key).map_err(|error| {
152            io::Error::new(
153                error.kind(),
154                format!(
155                    "Cgroup value under key {:?} cannot be read due to: {}",
156                    key, error
157                ),
158            )
159        })
160    }
161
162    /// Gets a value from cgroup control file.
163    pub fn get_value<T>(&self, key: &str) -> io::Result<T>
164    where
165        T: std::str::FromStr,
166    {
167        self.get_raw_value(key)?
168            .trim_end()
169            .parse()
170            .map_err(|_| io::Error::new(io::ErrorKind::Other, "could not parse the value"))
171    }
172
173    fn tasks_absolute_path(&self) -> PathBuf {
174        self.root.join("tasks")
175    }
176
177    /// Attaches a task (thread) to the cgroup.
178    pub fn add_task(&self, pid: nix::unistd::Pid) -> io::Result<()> {
179        fs::write(self.tasks_absolute_path(), pid.to_string()).map_err(|error| {
180            io::Error::new(
181                error.kind(),
182                format!(
183                    "A task cannot be added to cgroup {:?} due to: {}",
184                    self.root, error
185                ),
186            )
187        })
188    }
189
190    /// Lists tasks (threads) attached to the cgroup.
191    pub fn get_tasks(&self) -> io::Result<Vec<nix::unistd::Pid>> {
192        Ok(fs::read_to_string(self.tasks_absolute_path())
193            .map_err(|error| {
194                io::Error::new(
195                    error.kind(),
196                    format!(
197                        "Tasks cannot be read from cgroup {:?} due to: {}",
198                        self.root, error
199                    ),
200                )
201            })?
202            .split_whitespace()
203            .map(|pid| nix::unistd::Pid::from_raw(pid.parse().unwrap()))
204            .collect())
205    }
206
207    /// Sends a specified Unix Signal to all the tasks in the Cgroup.
208    pub fn send_signal_to_all_tasks(&self, signal: nix::sys::signal::Signal) -> io::Result<usize> {
209        let tasks = self.get_tasks()?;
210        let tasks_count = tasks.len();
211        for task in tasks {
212            nix::sys::signal::kill(task, signal).ok();
213        }
214        Ok(tasks_count)
215    }
216
217    /// Kills (SIGKILL) all the attached to the cgroup tasks.
218    ///
219    /// WARNING: The naive implementation turned out to be not reliable enough for the fork-bomb
220    /// use-case. To implement a reliable `kill_all` method, use `freezer` Cgroup. It is decided to
221    /// move such extensions into a separate crate (to be announced).
222    #[deprecated(
223        since = "1.0.1",
224        note = "please, use `freezer` cgroup to implement `kill_all_tasks` reliably (https://gitlab.com/dots.org.ua/ddots-runner/blob/d967ee3ba9de364dfb5a2e1a4f468586efb504f8/src/extensions/process.rs#L132-166)"
225    )]
226    pub fn kill_all_tasks(&self) -> io::Result<()> {
227        for _ in 0..100 {
228            if self.send_signal_to_all_tasks(nix::sys::signal::Signal::SIGKILL)? == 0 {
229                break;
230            }
231            std::thread::sleep(std::time::Duration::from_micros(1));
232        }
233        Err(io::Error::new(
234            io::ErrorKind::Other,
235            "child subprocess(es) survived SIGKILL",
236        ))
237    }
238}
239
240/// An automatically managed controller of a specific cgroups subsystem.
241///
242/// It is a wrapper around [`Cgroup`] type which automatically creates (on [`init`]) and removes
243/// (on [`drop`]) a cgroup in a given subsystem.
244///
245/// Since it is a wrapper, all the methods from [`Cgroup`] type are directly available for
246/// `AutomanagedCgroup` instances.
247///
248/// [`Cgroup`]: struct.Cgroup.html
249/// [`init`]: struct.AutomanagedCgroup.html#method.init
250/// [`drop`]: struct.AutomanagedCgroup.html#impl-Drop
251#[derive(Debug)]
252pub struct AutomanagedCgroup {
253    inner: Cgroup,
254}
255
256impl AutomanagedCgroup {
257    /// Inits a cgroup, which means that it creates a cgroup in a given subsystem.
258    ///
259    /// Notes:
260    /// * If there is an existing cgroup, it will be recreated from scratch. If that is not what
261    ///   you what, consider using [`Cgroup`] type instead.
262    /// * The cgroup will be automatically removed once the `AutomanagedCgroup` instance is
263    ///   dropped.
264    ///
265    /// [`Cgroup`]: struct.Cgroup.html
266    pub fn init(cgroup_name: &CgroupName, subsystem: &str) -> io::Result<Self> {
267        let inner = Cgroup::new(cgroup_name, subsystem);
268        if let Err(error) = inner.create() {
269            match inner.get_tasks() {
270                Err(_) => return Err(error),
271                Ok(tasks) => {
272                    if !tasks.is_empty() {
273                        return Err(error);
274                    }
275                }
276            }
277            inner.remove().ok();
278            inner.create()?;
279        }
280        Ok(Self { inner })
281    }
282}
283
284impl std::ops::Deref for AutomanagedCgroup {
285    type Target = Cgroup;
286
287    fn deref(&self) -> &Self::Target {
288        &self.inner
289    }
290}
291
292impl AsRef<Cgroup> for AutomanagedCgroup {
293    fn as_ref(&self) -> &Cgroup {
294        &self
295    }
296}
297
298impl Drop for AutomanagedCgroup {
299    fn drop(&mut self) {
300        drop(self.inner.remove());
301    }
302}
303
304/// This trait is designed to extend `std::process::Command` type with helpers for Cgroups.
305pub trait CgroupsCommandExt {
306    /// Specifies the Cgroups the executed process will be put into on start.
307    fn cgroups(&mut self, cgroups: &[impl AsRef<Cgroup>]) -> &mut Self;
308}
309
310impl CgroupsCommandExt for std::process::Command {
311    /// Specifies the Cgroups the executed process will be put into on start.
312    ///
313    /// # Example
314    ///
315    /// ```no_run
316    /// let my_cgroup = cgroups_fs::CgroupName::new("my-cgroup");
317    /// let my_memory_cgroup = cgroups_fs::AutomanagedCgroup::init(&my_cgroup, "memory").unwrap();
318    ///
319    /// use cgroups_fs::CgroupsCommandExt;
320    /// let output = std::process::Command::new("echo")
321    ///     .arg("Hello world")
322    ///     .cgroups(&[&my_memory_cgroup])
323    ///     .output()
324    ///     .expect("Failed to execute command");
325    ///
326    /// println!(
327    ///     "The echo process used {} bytes of RAM.",
328    ///     my_memory_cgroup.get_value::<u64>("memory.max_usage_in_bytes").unwrap()
329    /// );
330    /// ```
331    fn cgroups(&mut self, cgroups: &[impl AsRef<Cgroup>]) -> &mut Self {
332        let tasks_paths = cgroups
333            .iter()
334            .map(|cgroup| cgroup.as_ref().tasks_absolute_path())
335            .collect::<Vec<PathBuf>>();
336        unsafe {
337            self.pre_exec(move || {
338                let pid = std::process::id().to_string();
339                for tasks_path in &tasks_paths {
340                    fs::write(tasks_path, &pid)?;
341                }
342                Ok(())
343            })
344        }
345    }
346}